Documentation
¶
Overview ¶
Package web wires the application's HTTP surface. Routes split into three layers: every request goes through recover → log → authMiddleware (session + viewer + CSRF + must_change_password gate). Authenticated-only routes additionally pass through requireSession; the login page passes through requireAnonymous. There is no public signup route — the first user is created by the operator via scripts/operator/create-user.sql (kamal create-user) and subsequent users join via invite links rendered on /invites/:token.
/oauth/* implements a PKCE-only OAuth 2.1 server for /mcp + /api/v1/*. /api/v1/whoami exists as the bearer middleware's production smoke surface.
Index ¶
- func CORS(allowedOrigins []string) func(http.Handler) http.Handler
- func DecodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, bool)
- func Handler(opts Options) http.Handler
- func MaxBody(n int64) func(http.Handler) http.Handler
- func RequireBearer(svc *oauth.Service, logger *slog.Logger) func(http.Handler) http.Handler
- type Options
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CORS ¶
CORS handles the SDK snippet's cross-origin POST to /api/v1/ingest/events. An empty allowedOrigins slice is "permissive" — the response carries Access-Control-Allow-Origin: * so any first-party page can write events. A non-empty slice is the operator-restricted form: the request Origin must match exactly or the CORS headers are omitted (the browser then blocks the response from JS, which is the only enforcement layer that matters).
Allow-Methods / Allow-Headers are static — we don't accept cookies or custom verbs on /api/v1/ingest/events.
Preflight (OPTIONS + Access-Control-Request-Method) short-circuits with 204; the wrapped handler never runs.
func DecodeJSON ¶
DecodeJSON reads the request body as JSON into T with the uniform error mapping the v1 endpoints expect: a body that overran MaxBody → 413, a JSON syntax/type error → 400. On either failure the response is fully written and ok=false is returned so the handler should bail. On success the decoded value is returned with ok=true.
Pair with MaxBody upstream so r.Body is already wrapped in http.MaxBytesReader; this helper just catches the *http.MaxBytesError the reader surfaces and converts it to the right status.
func MaxBody ¶
MaxBody wraps the request body in http.MaxBytesReader so reads past n bytes fail with *http.MaxBytesError. DecodeJSON below maps that error to 413; raw-body handlers can do the same with errors.As.
Mount per-route — different surfaces have different ceilings (ingest 10 MiB; future /v1/query much smaller).
func RequireBearer ¶
RequireBearer enforces an OAuth bearer token on the wrapped handler. Missing, malformed, expired, or revoked tokens get a uniform 401 with a WWW-Authenticate header — we don't distinguish failure modes per RFC 6750 §3.
On success the handler downstream can pull the (user, project, scope, client) identity bag from ctx via oauth.FromContext.
Types ¶
type Options ¶
type Options struct {
AuthService *auth.Service
OAuthService *oauth.Service
OAuthIssuer string
Logger *slog.Logger
SecureCookies bool
// Version is the build-time version stamp (git describe), injected into
// cmd/server via -ldflags and surfaced in the /healthz body so operators
// can confirm what's deployed from `kamal logs` / a health probe. Empty in
// most tests; "dev" for un-stamped local builds.
Version string
// IngestService + the three following knobs wire POST /api/v1/ingest/events.
// Nil IngestService is supported so degenerate test scenarios that build a
// handler without a CH pool still work — the route is just absent.
IngestService *ingest.Service
AllowedOrigins []string
IngestMaxBodyBytes int64
DLQDepth503Threshold int
QueryExecutor *query.Executor
QuerySchema *query.SchemaProvider
QueryMaxBodyBytes int64
// MCPHandler is the Streamable HTTP transport for the /mcp endpoint
// (internal/mcp.NewHTTPHandler). It mounts behind the same RequireBearer
// + CORS middleware as /api/v1/*. Nil leaves /mcp unmounted — supported
// for degenerate test scenarios built without a CH pool.
MCPHandler http.Handler
// RateLimiter is the extension seam consulted on the ingest and query/MCP
// paths after the tenant is resolved (see docs/extending.md). Nil defaults to
// the no-op extension.AllowAll, so the open-source build's behavior is
// unchanged; a wrapper or self-hoster injects a real limiter here.
RateLimiter extension.RateLimiter
// Entitlement is the extension seam consulted on the analysis surfaces
// (REST query + schema, web playground; MCP is gated in internal/mcp) after
// the project resolves. Nil defaults to the no-op extension.Unlimited, so
// the open-source build never gates analysis. A hosted wrapper injects a
// real implementation to lock a project's analysis once it is over quota.
Entitlement extension.Entitlement
// UpgradeURL is where the web query playground redirects when Entitlement
// denies an over-quota project — the wrapper's branded billing/upgrade page.
// Empty falls back to a plain 402 (the API/MCP surfaces always answer 402).
UpgradeURL string
}
Options bundles the dependencies needed to build the HTTP handler. SecureCookies enables the Secure flag on session/csrf cookies — disabled in dev (plaintext http on localhost) and enabled in production via env.
OAuthService and OAuthIssuer are required to wire the OAuth routes; they may be nil/empty in degenerate test scenarios that build a handler without a database. The auth-only path keeps working in that case.