package httpd import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/aoagents/agent-orchestrator/backend/internal/config" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/controllers" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope" prsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/pr" projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project" ) // APIDeps bundles every service the API layer's controllers depend on. type APIDeps struct { Projects projectsvc.Manager Sessions controllers.SessionService PRs prsvc.ActionManager } // API owns one controller per resource and is the single Register call the // router invokes to mount the /api/v1 surface. type API struct { cfg config.Config projects *controllers.ProjectsController sessions *controllers.SessionsController prs *controllers.PRsController } // NewAPI constructs the API surface from its dependencies. cfg carries the // per-request timeout so the REST group can apply it without re-reading the // environment. func NewAPI(cfg config.Config, deps APIDeps) *API { return &API{ cfg: cfg, projects: &controllers.ProjectsController{ Mgr: deps.Projects, }, sessions: &controllers.SessionsController{ Svc: deps.Sessions, }, prs: &controllers.PRsController{Svc: deps.PRs}, } } // Register mounts the bounded /api/v1 REST surface. Long-lived surfaces such // as muxed terminal streams stay outside this timeout group. func (a *API) Register(root chi.Router) { timeout := a.cfg.RequestTimeout if timeout <= 0 { timeout = config.DefaultRequestTimeout } root.Route("/api/v1", func(r chi.Router) { // Serve the OpenAPI document from the same origin as the routes it describes. r.Get("/openapi.yaml", apispec.ServeYAML) r.Group(func(r chi.Router) { r.Use(middleware.Timeout(timeout)) a.projects.Register(r) a.sessions.Register(r) a.prs.Register(r) // Sibling REST controllers plug in here. }) // Surfaces that intentionally bypass the REST timeout register at this level. }) } // notFoundJSON returns the locked envelope for unmatched routes. Chi's default // 404 is a text/plain body; the API surface must answer JSON so consumers can // parse it uniformly. func notFoundJSON(w http.ResponseWriter, r *http.Request) { envelope.WriteAPIError(w, r, http.StatusNotFound, "not_found", "ROUTE_NOT_FOUND", r.Method+" "+r.URL.Path+" has no handler", nil) } // methodNotAllowedJSON returns the locked envelope when a method probes a // known path without a matching verb (e.g. PUT /projects/{id} after we drop // the legacy PUT alias). func methodNotAllowedJSON(w http.ResponseWriter, r *http.Request) { envelope.WriteAPIError(w, r, http.StatusMethodNotAllowed, "method_not_allowed", "METHOD_NOT_ALLOWED", r.Method+" not allowed on "+r.URL.Path, nil) }