// Package httpapi is the server's outer edge: routing, authentication // middleware, the two WebSocket upgrades, and the embedded web UI. // // Everything is one origin and one port. The frontend is served from the same // binary, so there is no CORS story to get wrong and no second thing to deploy. package httpapi import ( "encoding/json" "errors" "io/fs" "log/slog" "net/http" "strings" "time" "github.com/coder/websocket" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/user/grok-glance/internal/auth" "github.com/user/grok-glance/internal/hub" "github.com/user/grok-glance/internal/state" ) // Options configures the server. type Options struct { Store *state.Store Auth *auth.Manager Hub *hub.Hub Log *slog.Logger // Web is the built frontend, rooted at index.html. Nil serves a plain // placeholder page instead, so `go run ./cmd/glance` works before `npm run // build` has ever been run. Web fs.FS } // Server is the HTTP handler tree. type Server struct { opts Options router chi.Router } // New wires the routes. func New(opts Options) *Server { s := &Server{opts: opts} r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.Recoverer) r.Use(securityHeaders) r.Route("/api", func(r chi.Router) { // Open: tells an unauthenticated browser which page to render. It leaks // only whether setup has happened, which the /setup 404 reveals anyway. r.Get("/status", s.handleStatus) // Bootstrap-gated: the token is the only thing standing between a fresh // server and whoever reaches the port first. r.Group(func(r chi.Router) { r.Use(s.requireBootstrap) r.Post("/setup/begin", s.handleSetupBegin) r.Post("/setup/complete", s.handleSetupComplete) }) r.Post("/login", s.handleLogin) r.Post("/logout", s.handleLogout) // The agent link authenticates with an API key, not a cookie: it is a // program on another machine, not a browser. r.Get("/acp/agent", s.handleAgentSocket) r.Group(func(r chi.Router) { r.Use(s.requireSession) r.Get("/agents", s.handleAgents) r.Get("/ws", s.handleBrowserSocket) }) }) r.NotFound(s.serveWeb) s.router = r return s } // ServeHTTP implements http.Handler. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.router.ServeHTTP(w, r) } // securityHeaders keeps the UI from being framed or sniffed. // // The CSP is strict because glance renders agent output — file contents, command // output, model text — and none of that is trusted markup. `default-src 'self'` // with no `unsafe-inline` means an injected