| 555 | } |
| 556 | |
| 557 | func (a *API) RegisterRoutes(r *mux.Router) { |
| 558 | // Catch-all 404/405 handlers so every error response leaves the |
| 559 | // server as `text/plain; charset=utf-8` with a single-line body. |
| 560 | // gorilla/mux's defaults are bare status codes with no body and no |
| 561 | // Content-Type, which breaks client error handling and surfaced |
| 562 | // during the e2e contract sweep — see tests/e2e-prod 07-error-contract. |
| 563 | r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 564 | http.Error(w, "not found", http.StatusNotFound) |
| 565 | }) |
| 566 | r.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 567 | http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
| 568 | }) |
| 569 | |
| 570 | // Internal machine-to-machine endpoint: the external limits |
| 571 | // provisioner (hosted billing sidecar) calls this to bust the |
| 572 | // in-process limits cache for a given user immediately after it |
| 573 | // writes account_limits. Authenticated by shared HMAC over the |
| 574 | // request body; deliberately not advertised in OpenAPI. |
| 575 | r.HandleFunc("/api/internal/limits/invalidate", a.handleInvalidateLimits).Methods("POST") |
| 576 | |
| 577 | // HITL magic-link pages. Human-facing token-gated HTML (not the JSON |
| 578 | // API), opened from the approval email. GET renders a confirmation |
| 579 | // page with a POST form; POST executes the action. Splitting this way |
| 580 | // keeps email-client URL scanners (Gmail, Outlook Safe Links, |
| 581 | // corporate mail gateways) from triggering side effects just by |
| 582 | // previewing the link. Served under /v1 via the chi root's fallback to |
| 583 | // this mux (these are raw HTML routes, not Huma operations). |
| 584 | r.HandleFunc("/v1/approve", a.handleApproveMagicLinkGet).Methods("GET") |
| 585 | r.HandleFunc("/v1/approve", a.handleApproveMagicLinkPost).Methods("POST") |
| 586 | r.HandleFunc("/v1/reject", a.handleRejectMagicLinkGet).Methods("GET") |
| 587 | r.HandleFunc("/v1/reject", a.handleRejectMagicLinkPost).Methods("POST") |
| 588 | |
| 589 | // --- Non-versioned operational endpoints --- |
| 590 | r.HandleFunc("/api/health", a.handleHealth).Methods("GET", "HEAD") |
| 591 | r.HandleFunc("/api/feedback", a.handleFeedback).Methods("POST") |
| 592 | |
| 593 | // OAuth 2.1 / RFC 6749 endpoints, root + unversioned (Slice 5b: renamed |
| 594 | // from /oauth2/* to /oauth2/* to conform to the auth.md spec — no |
| 595 | // back-compat alias). Handlers 404 when SetOAuthProvider wasn't called, so |
| 596 | // registering unconditionally is safe. |
| 597 | r.HandleFunc("/oauth2/authorize", a.handleOAuthAuthorize).Methods("GET") |
| 598 | r.HandleFunc("/oauth2/consent", a.handleOAuthConsent).Methods("POST") |
| 599 | r.HandleFunc("/oauth2/token", a.handleOAuthToken).Methods("POST") |
| 600 | r.HandleFunc("/oauth2/revoke", a.handleOAuthRevoke).Methods("POST") |
| 601 | r.HandleFunc("/oauth2/register", a.handleOAuthRegister).Methods("POST") |
| 602 | r.HandleFunc("/oauth2/clients/{client_id}", a.handleOAuthGetClient).Methods("GET") |
| 603 | r.HandleFunc("/.well-known/oauth-authorization-server", a.handleOAuthDiscovery).Methods("GET") |
| 604 | // Public JWKS for verifying e2a-minted agent JWTs (Slice 5b). Always |
| 605 | // registered; serves {"keys":[]} when no signing key is configured. |
| 606 | r.HandleFunc("/.well-known/jwks.json", a.handleJWKS).Methods("GET") |
| 607 | // auth.md agent-identity bootstrap (Slice 5b-2): present an agent-scoped |
| 608 | // credential, receive an identity_assertion to exchange at /oauth2/token |
| 609 | // (grant_type=jwt-bearer). 501 when no signing key is configured. |
| 610 | r.HandleFunc("/agent/identity", a.handleAgentIdentity).Methods("POST") |
| 611 | |
| 612 | // User auth (Google OAuth for agent developers) |
| 613 | if a.userAuth != nil { |
| 614 | r.HandleFunc("/api/auth/login", a.userAuth.HandleLogin).Methods("GET") |