package main import ( "errors" "fmt" "net/http" chassisauth "git.d-ma.be/mathias/mcp-chassis/auth" "github.com/mathiasbq/hyperguild/ingestion/internal/api" ) // publicRoutes is the complete set of patterns allowed on the outer mux, // outside the REST API's auth (hyperguild#149). Each authenticates itself or is // meant to be open: /mcp (bearer/JWT), /capture (bearer/JWT), the webhook // (HMAC), /oauth/token (client secret), OAuth metadata and /metrics (open). // A REST pattern added here would beat the authenticated catch-all by // specificity and ship open, which is why buildMux refuses anything else. var publicRoutes = map[string]bool{ "/mcp": true, "POST /capture": true, "POST /webhooks/brain-sync": true, "GET /.well-known/oauth-protected-resource": true, "GET /.well-known/oauth-authorization-server": true, "POST /oauth/token": true, "GET /metrics": true, } // restAuth is the credential the REST API checks: the same as /mcp's. type restAuth struct { token string validator *chassisauth.JWTValidator realm string resourceMetadataURL string } // buildMux is the server's whole router. The REST API is mounted at "/" // behind bearer auth that buildMux constructs itself, so a caller cannot pass // a no-op; public handlers are registered as given, and only for patterns on // the publicRoutes allowlist. It returns an http.Handler, not the // *ServeMux, so nothing can register a route on the result afterwards and // bypass the allowlist. func buildMux(auth restAuth, h *api.Handler, public map[string]http.Handler) (http.Handler, error) { if auth.token == "" { return nil, errors.New("buildMux: REST auth needs a token") } mux := http.NewServeMux() for pattern, handler := range public { if !publicRoutes[pattern] { return nil, fmt.Errorf("buildMux: %q is not an allowed public route", pattern) } mux.Handle(pattern, handler) } mountREST(mux, h, func(next http.Handler) http.Handler { return chassisauth.BearerMiddleware(auth.token, auth.validator, auth.realm, auth.resourceMetadataURL, next) }) return mux, nil } // mountREST registers the REST API on its own mux and mounts it at "/" behind // auth: anything not registered as public lands here and is authenticated, // including routes added later. func mountREST(mux *http.ServeMux, h *api.Handler, auth func(http.Handler) http.Handler) { rest := http.NewServeMux() rest.HandleFunc("POST /query", h.Query) rest.HandleFunc("POST /write", h.Write) rest.HandleFunc("POST /index", h.Index) rest.HandleFunc("POST /ingest", h.Ingest) rest.HandleFunc("POST /ingest-path", h.IngestPath) rest.HandleFunc("POST /ingest-raw", h.IngestRaw) rest.HandleFunc("POST /backfill-refs", h.BackfillRefs) rest.HandleFunc("GET /pending", h.Pending) rest.HandleFunc("POST /promote", h.Promote) rest.HandleFunc("POST /backfill-embeddings", h.BackfillEmbeddings) rest.HandleFunc("GET /pass-rate", h.PassRate) mux.Handle("/", auth(rest)) }