package api import ( "bytes" "context" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "log" "math/big" "mime" "net/http" "net/url" "os" "strconv" "strings" "sync" "time" "greencoast/internal/auth" "greencoast/internal/index" ) // BlobStore minimal interface for storage backends. type BlobStore interface { Put(hash string, r io.Reader) error Get(hash string) (io.ReadCloser, int64, error) Delete(hash string) error } type blobWalker interface { Walk(func(hash string, size int64, mod time.Time) error) error } type DiscordProvider struct { Enabled bool ClientID string ClientSecret string RedirectURI string } type AuthProviders struct { SigningSecretHex string Discord DiscordProvider } type Server struct { mux *http.ServeMux store BlobStore idx *index.Index coarseTS bool zeroTrust bool signingKey []byte // dev/testing flags allowUnauth bool devBearer string // require proof-of-possession on every auth’d call requirePoP bool // SSE in-process sseMu sync.Mutex sseSubs map[chan []byte]struct{} sseClosed bool // SSO state + PKCE verifier + device key binding stateMu sync.Mutex states map[string]stateItem // Nonce challenges for key-based login nonceMu sync.Mutex nonceExpiry map[string]time.Time // PoP replay cache replayMu sync.Mutex replays map[string]time.Time } type stateItem struct { Exp time.Time Verifier string // PKCE code_verifier DeviceKey string // "p256:" or "ed25519:" ReturnNext string // optional } func New(store BlobStore, idx *index.Index, coarseTS bool, zeroTrust bool, providers AuthProviders) *Server { key, _ := hex.DecodeString(strings.TrimSpace(providers.SigningSecretHex)) s := &Server{ mux: http.NewServeMux(), store: store, idx: idx, coarseTS: coarseTS, zeroTrust: zeroTrust, signingKey: key, allowUnauth: os.Getenv("GC_DEV_ALLOW_UNAUTH") == "true", devBearer: os.Getenv("GC_DEV_BEARER"), requirePoP: strings.ToLower(os.Getenv("GC_REQUIRE_POP")) != "false", // default true sseSubs: make(map[chan []byte]struct{}), states: make(map[string]stateItem), nonceExpiry: make(map[string]time.Time), replays: make(map[string]time.Time), } _ = mime.AddExtensionType(".js", "application/javascript; charset=utf-8") _ = mime.AddExtensionType(".css", "text/css; charset=utf-8") _ = mime.AddExtensionType(".html", "text/html; charset=utf-8") _ = mime.AddExtensionType(".map", "application/json; charset=utf-8") // Core s.mux.HandleFunc("/healthz", s.handleHealthz) // Auth (public-key) s.mux.Handle("/v1/auth/key/challenge", s.withCORS(http.HandlerFunc(s.handleKeyChallenge))) s.mux.Handle("/v1/auth/key/verify", s.withCORS(http.HandlerFunc(s.handleKeyVerify))) // Discord SSO s.mux.Handle("/v1/auth/discord/start", s.withCORS(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.handleDiscordStart(w, r, providers.Discord) }))) s.mux.Handle("/v1/auth/discord/callback", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.handleDiscordCallback(w, r, providers.Discord) })) // Objects s.mux.Handle("/v1/object", s.withCORS(http.HandlerFunc(s.handlePutObject))) s.mux.Handle("/v1/object/", s.withCORS(http.HandlerFunc(s.handleObjectByHash))) // Index + SSE s.mux.Handle("/v1/index", s.withCORS(http.HandlerFunc(s.handleIndex))) s.mux.Handle("/v1/index/stream", s.withCORS(http.HandlerFunc(s.handleIndexSSE))) // GDPR/policy s.mux.Handle("/v1/gdpr/policy", s.withCORS(http.HandlerFunc(s.handleGDPRPolicy))) // Admin: reindex s.mux.Handle("/v1/admin/reindex", s.withCORS(http.HandlerFunc(s.handleAdminReindex))) return s } func (s *Server) ListenHTTP(addr string) error { log.Printf("http listening on %s", addr) server := &http.Server{ Addr: addr, Handler: s.withCORS(s.mux), ReadHeaderTimeout: 5 * time.Second, } return server.ListenAndServe() } func (s *Server) ListenHTTPS(addr, certFile, keyFile string) error { log.Printf("https listening on %s", addr) server := &http.Server{ Addr: addr, Handler: s.withCORS(s.mux), ReadHeaderTimeout: 5 * time.Second, } return server.ListenAndServeTLS(certFile, keyFile) } func (s *Server) secureHeaders(w http.ResponseWriter) { w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") w.Header().Set("Cross-Origin-Resource-Policy", "same-site") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Strict-Transport-Security", "max-age=15552000; includeSubDomains; preload") } func (s *Server) withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.secureHeaders(w) // Strong CSP for static will be set in static server; API allows connect from client origin w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS, POST") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-GC-Private, X-GC-3P-Assent, X-GC-TZ, X-GC-Key, X-GC-TS, X-GC-Proof") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // ---------- Health & policy ---------- func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) { s.secureHeaders(w) w.Header().Set("Content-Type", "text/plain; charset=utf-8") io.WriteString(w, "ok") } func (s *Server) handleGDPRPolicy(w http.ResponseWriter, r *http.Request) { s.secureHeaders(w) w.Header().Set("Content-Type", "application/json; charset=utf-8") type policy struct { StoresPII bool `json:"stores_pii"` CollectIP bool `json:"collect_ip"` CollectUA bool `json:"collect_user_agent"` Timestamps string `json:"timestamps"` ZeroTrust bool `json:"zero_trust"` Accounts string `json:"accounts"` ProofOfPoss bool `json:"proof_of_possession"` } resp := policy{ StoresPII: false, CollectIP: false, CollectUA: false, Timestamps: map[bool]string{true: "coarse_utc", false: "utc"}[s.coarseTS], ZeroTrust: s.zeroTrust, Accounts: "public-key only", ProofOfPoss: s.requirePoP, } _ = json.NewEncoder(w).Encode(resp) } // ---------- Auth helpers ---------- type authCtx struct { sub string cnf string // "p256:" or "ed25519:" } func (s *Server) parseAuth(w http.ResponseWriter, r *http.Request) (*authCtx, bool) { // Dev bypass if s.allowUnauth { return &authCtx{sub: "dev"}, true } // Dev bearer if s.devBearer != "" && r.Header.Get("Authorization") == "Bearer "+s.devBearer { return &authCtx{sub: "dev"}, true } h := r.Header.Get("Authorization") if h == "" { http.Error(w, "unauthorized", http.StatusUnauthorized) return nil, false } // gc2 HMAC token if strings.HasPrefix(h, "Bearer gc2.") && len(s.signingKey) != 0 { claims, err := auth.VerifyGC2(s.signingKey, strings.TrimPrefix(h, "Bearer "), time.Now()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return nil, false } return &authCtx{sub: claims.Sub, cnf: claims.CNF}, true } http.Error(w, "unauthorized", http.StatusUnauthorized) return nil, false } func (s *Server) verifyPoP(w http.ResponseWriter, r *http.Request, ac *authCtx, body []byte) bool { if !s.requirePoP { return true } pubHdr := r.Header.Get("X-GC-Key") ts := r.Header.Get("X-GC-TS") proof := r.Header.Get("X-GC-Proof") if pubHdr == "" || ts == "" || proof == "" { http.Error(w, "missing proof", http.StatusUnauthorized) return false } // timestamp window sec, _ := strconv.ParseInt(ts, 10, 64) d := time.Since(time.Unix(sec, 0)) if d < -5*time.Minute || d > 5*time.Minute { http.Error(w, "stale proof", http.StatusUnauthorized) return false } // cnf must match if ac.cnf == "" || ac.cnf != pubHdr { http.Error(w, "key mismatch", http.StatusUnauthorized) return false } // build message sum := sha256.Sum256(body) msg := strings.ToUpper(r.Method) + "\n" + r.URL.String() + "\n" + ts + "\n" + hex.EncodeToString(sum[:]) // verify signature ok := false switch { case strings.HasPrefix(pubHdr, "ed25519:"): raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(pubHdr, "ed25519:")) if err == nil { sig, err := base64.RawURLEncoding.DecodeString(proof) if err == nil && len(raw) == ed25519.PublicKeySize { ok = ed25519.Verify(ed25519.PublicKey(raw), []byte(msg), sig) } } case strings.HasPrefix(pubHdr, "p256:"): raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(pubHdr, "p256:")) if err == nil && len(raw) == 65 && raw[0] == 0x04 { x := new(big.Int).SetBytes(raw[1:33]) y := new(big.Int).SetBytes(raw[33:65]) pk := ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y} der, err := base64.RawURLEncoding.DecodeString(proof) if err == nil { ok = ecdsa.VerifyASN1(&pk, []byte(msg), der) } } } if !ok { http.Error(w, "bad proof", http.StatusUnauthorized) return false } // replay cache h := sha256.Sum256([]byte(proof + "|" + ts)) key := base64.RawURLEncoding.EncodeToString(h[:]) s.replayMu.Lock() defer s.replayMu.Unlock() if exp, exists := s.replays[key]; exists && time.Now().Before(exp) { http.Error(w, "replay", http.StatusUnauthorized) return false } s.replays[key] = time.Now().Add(10 * time.Minute) return true } // ---------- Public-key auth: challenge/verify ---------- func (s *Server) handleKeyChallenge(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } nonce := s.randToken(16) exp := time.Now().Add(10 * time.Minute) s.nonceMu.Lock() s.nonceExpiry[nonce] = exp s.nonceMu.Unlock() _ = json.NewEncoder(w).Encode(map[string]any{"nonce": nonce, "exp": exp.Unix()}) } type keyVerifyReq struct { Nonce string `json:"nonce"` Alg string `json:"alg"` // "p256" or "ed25519" Pub string `json:"pub"` // base64(raw) for that alg (p256 uncompressed point 65B; ed25519 32B) Sig string `json:"sig"` // base64(signature over "key-verify\n"+nonce) } func (s *Server) handleKeyVerify(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req keyVerifyReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Nonce == "" || req.Alg == "" || req.Pub == "" || req.Sig == "" { http.Error(w, "bad request", http.StatusBadRequest) return } // check nonce s.nonceMu.Lock() exp, ok := s.nonceExpiry[req.Nonce] if ok { delete(s.nonceExpiry, req.Nonce) } s.nonceMu.Unlock() if !ok || time.Now().After(exp) { http.Error(w, "nonce invalid", http.StatusUnauthorized) return } msg := "key-verify\n" + req.Nonce pubRaw, err := base64.RawURLEncoding.DecodeString(req.Pub) if err != nil { http.Error(w, "bad pub", http.StatusBadRequest) return } sigRaw, err := base64.RawURLEncoding.DecodeString(req.Sig) if err != nil { http.Error(w, "bad sig", http.StatusBadRequest) return } var cnf string switch strings.ToLower(req.Alg) { case "ed25519": if len(pubRaw) != ed25519.PublicKeySize || len(sigRaw) != ed25519.SignatureSize { http.Error(w, "bad key", http.StatusBadRequest) return } if !ed25519.Verify(ed25519.PublicKey(pubRaw), []byte(msg), sigRaw) { http.Error(w, "verify failed", http.StatusUnauthorized) return } cnf = "ed25519:" + req.Pub case "p256": if len(pubRaw) != 65 || pubRaw[0] != 0x04 { http.Error(w, "bad key", http.StatusBadRequest) return } x := new(big.Int).SetBytes(pubRaw[1:33]) y := new(big.Int).SetBytes(pubRaw[33:65]) pk := ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y} // sigRaw assumed DER (WebCrypto) if !ecdsa.VerifyASN1(&pk, []byte(msg), sigRaw) { http.Error(w, "verify failed", http.StatusUnauthorized) return } cnf = "p256:" + req.Pub default: http.Error(w, "unsupported alg", http.StatusBadRequest) return } sub := auth.AccountIDFromPub(pubRaw) ttl := 8 * time.Hour now := time.Now() bearer, err := auth.MintGC2(s.signingKey, auth.Claims{ Sub: sub, Exp: now.Add(ttl).Unix(), Nbf: now.Add(-60 * time.Second).Unix(), Iss: "greencoast", Aud: "api", CNF: cnf, }) if err != nil { http.Error(w, "sign error", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{ "bearer": bearer, "sub": sub, "exp": now.Add(ttl).Unix(), }) } // ---------- Objects & Index ---------- func (s *Server) handlePutObject(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Limit body to 10 MiB by default const maxBlob = int64(10 << 20) r.Body = http.MaxBytesReader(w, r.Body, maxBlob) // Read body first to support PoP over body hash var buf bytes.Buffer n, err := io.Copy(&buf, r.Body) if err != nil { http.Error(w, "read error", 500) return } ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, buf.Bytes()) { return } isPrivate := r.Header.Get("X-GC-Private") == "1" creatorTZ := strings.TrimSpace(r.Header.Get("X-GC-TZ")) if creatorTZ != "" && !isReasonableTZ(creatorTZ) { creatorTZ = "" } sum := sha256.Sum256(buf.Bytes()) hash := hex.EncodeToString(sum[:]) if err := s.store.Put(hash, bytes.NewReader(buf.Bytes())); err != nil { http.Error(w, "store error", 500) return } when := time.Now().UTC() if s.coarseTS { when = when.Truncate(time.Minute) } entry := index.Entry{ Hash: hash, Bytes: n, StoredAt: when.Format(time.RFC3339Nano), Private: isPrivate, CreatorTZ: creatorTZ, } if err := s.idx.Put(entry); err != nil { http.Error(w, "index error", 500) return } s.sseBroadcast(map[string]any{"event": "put", "data": entry}) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(entry) } func (s *Server) handleObjectByHash(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/object/"), "/") if len(parts) == 0 || parts[0] == "" { http.NotFound(w, r) return } hash := parts[0] switch r.Method { case http.MethodGet: ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, nil) { return } rc, n, err := s.store.Get(hash) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } defer rc.Close() w.Header().Set("Content-Type", "application/octet-stream") if n > 0 { w.Header().Set("Content-Length", fmt.Sprintf("%d", n)) } _, _ = io.Copy(w, rc) case http.MethodDelete: ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, nil) { return } if err := s.store.Delete(hash); err != nil { http.Error(w, "delete error", 500) return } _ = s.idx.Delete(hash) s.sseBroadcast(map[string]any{"event": "delete", "data": map[string]string{"hash": hash}}) w.WriteHeader(http.StatusNoContent) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, nil) { return } items, err := s.idx.List() if err != nil { http.Error(w, "index error", 500) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(items) } func (s *Server) handleIndexSSE(w http.ResponseWriter, r *http.Request) { ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, nil) { return } flusher, ok2 := w.(http.Flusher) if !ok2 { http.Error(w, "stream unsupported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Connection", "keep-alive") ch := make(chan []byte, 8) s.sseMu.Lock() if s.sseClosed { s.sseMu.Unlock() http.Error(w, "closed", http.StatusGone) return } s.sseSubs[ch] = struct{}{} s.sseMu.Unlock() fmt.Fprintf(w, "data: %s\n\n", `{"event":"hello","data":"ok"}`) flusher.Flush() ctx := r.Context() t := time.NewTicker(25 * time.Second) defer t.Stop() defer func() { s.sseMu.Lock() delete(s.sseSubs, ch) s.sseMu.Unlock() close(ch) }() for { select { case <-ctx.Done(): return case b := <-ch: w.Write(b) w.Write([]byte("\n\n")) flusher.Flush() case <-t.C: w.Write([]byte("data: {}\n\n")) flusher.Flush() } } } func (s *Server) sseBroadcast(v interface{}) { b, _ := json.Marshal(v) s.sseMu.Lock() for ch := range s.sseSubs { select { case ch <- append([]byte("data: "), b...): default: } } s.sseMu.Unlock() } // ---------- Admin: reindex ---------- func (s *Server) handleAdminReindex(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } ac, ok := s.parseAuth(w, r) if !ok { return } if !s.verifyPoP(w, r, ac, nil) { return } walker, ok2 := s.store.(blobWalker) if !ok2 { http.Error(w, "store does not support walk", http.StatusNotImplemented) return } count := 0 err := walker.Walk(func(hash string, size int64, mod time.Time) error { count++ return s.idx.Put(index.Entry{ Hash: hash, Bytes: size, StoredAt: mod.UTC().Format(time.RFC3339Nano), Private: false, }) }) if err != nil { http.Error(w, "walk error: "+err.Error(), 500) return } items, _ := s.idx.List() w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]any{ "walked": count, "indexed": len(items), }) } // ---------- Discord SSO with PKCE + device key binding ---------- func (s *Server) handleDiscordStart(w http.ResponseWriter, r *http.Request, cfg DiscordProvider) { if !cfg.Enabled || cfg.ClientID == "" || cfg.ClientSecret == "" || cfg.RedirectURI == "" { http.Error(w, "discord sso disabled", http.StatusBadRequest) return } if r.Header.Get("X-GC-3P-Assent") != "1" { http.Error(w, "third-party provider not assented", http.StatusForbidden) return } deviceKey := strings.TrimSpace(r.Header.Get("X-GC-Key")) if deviceKey == "" { http.Error(w, "device key required", http.StatusBadRequest) return } // PKCE verifier := s.randToken(32) chalSum := sha256.Sum256([]byte(verifier)) challenge := base64.RawURLEncoding.EncodeToString(chalSum[:]) state := s.randToken(16) s.stateMu.Lock() s.states[state] = stateItem{ Exp: time.Now().Add(10 * time.Minute), Verifier: verifier, DeviceKey: deviceKey, } s.stateMu.Unlock() v := url.Values{} v.Set("response_type", "code") v.Set("client_id", cfg.ClientID) v.Set("redirect_uri", cfg.RedirectURI) v.Set("scope", "identify") v.Set("state", state) v.Set("code_challenge", challenge) v.Set("code_challenge_method", "S256") authURL := (&url.URL{ Scheme: "https", Host: "discord.com", Path: "/api/oauth2/authorize", RawQuery: v.Encode(), }).String() w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]string{"url": authURL}) } func (s *Server) handleDiscordCallback(w http.ResponseWriter, r *http.Request, cfg DiscordProvider) { if !cfg.Enabled { http.Error(w, "disabled", http.StatusBadRequest) return } q := r.URL.Query() code := q.Get("code") state := q.Get("state") if code == "" || state == "" { http.Error(w, "invalid state/code", http.StatusBadRequest) return } s.stateMu.Lock() item, ok := s.states[state] if ok && time.Now().Before(item.Exp) { delete(s.states, state) } s.stateMu.Unlock() if !ok { http.Error(w, "state expired", http.StatusBadRequest) return } // Exchange code for token (with verifier) form := url.Values{} form.Set("client_id", cfg.ClientID) form.Set("client_secret", cfg.ClientSecret) form.Set("grant_type", "authorization_code") form.Set("code", code) form.Set("redirect_uri", cfg.RedirectURI) form.Set("code_verifier", item.Verifier) req, _ := http.NewRequestWithContext(r.Context(), http.MethodPost, "https://discord.com/api/oauth2/token", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "token exchange failed", 502) return } defer res.Body.Close() if res.StatusCode/100 != 2 { b, _ := io.ReadAll(res.Body) http.Error(w, "discord token error: "+string(b), 502) return } var tok struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` } if err := json.NewDecoder(res.Body).Decode(&tok); err != nil { http.Error(w, "token decode failed", 502) return } // Fetch user id ureq, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://discord.com/api/users/@me", nil) ureq.Header.Set("Authorization", tok.TokenType+" "+tok.AccessToken) ures, err := http.DefaultClient.Do(ureq) if err != nil { http.Error(w, "user fetch failed", 502) return } defer ures.Body.Close() if ures.StatusCode/100 != 2 { b, _ := io.ReadAll(ures.Body) http.Error(w, "discord user error: "+string(b), 502) return } var user struct { ID string `json:"id"` } if err := json.NewDecoder(ures.Body).Decode(&user); err != nil { http.Error(w, "user decode failed", 502) return } // Bind token to device key from /start ttl := 8 * time.Hour now := time.Now() sub := "discord:" + user.ID bearer, err := auth.MintGC2(s.signingKey, auth.Claims{ Sub: sub, Exp: now.Add(ttl).Unix(), Nbf: now.Add(-60 * time.Second).Unix(), Iss: "greencoast", Aud: "api", CNF: item.DeviceKey, }) if err != nil { http.Error(w, "sign error", 500) return } u, _ := url.Parse(cfg.RedirectURI) u.Fragment = "bearer=" + url.QueryEscape(bearer) + "&next=/" http.Redirect(w, r, u.String(), http.StatusFound) } // ---------- Utilities, shutdown ---------- func isReasonableTZ(tz string) bool { if !strings.Contains(tz, "/") || len(tz) > 64 { return false } for _, r := range tz { if !(r == '/' || r == '_' || r == '-' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) { return false } } return true } func (s *Server) Shutdown(ctx context.Context) error { s.sseMu.Lock() s.sseClosed = true for ch := range s.sseSubs { close(ch) } s.sseSubs = make(map[chan []byte]struct{}) s.sseMu.Unlock() return nil } func (s *Server) randToken(n int) string { // HMAC over time + counter to avoid importing crypto/rand; good enough for state/nonce // (If you prefer, switch to crypto/rand.) b := []byte(fmt.Sprintf("%d|%d", time.Now().UnixNano(), len(s.states)+len(s.nonceExpiry))) m := hmac.New(sha256.New, []byte(fmt.Sprintf("%p", s))) m.Write(b) sum := m.Sum(nil) return base64.RawURLEncoding.EncodeToString(sum[:n]) }