httpapi

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: MIT Imports: 50 Imported by: 0

Documentation

Overview

Package httpapi is the /v1 REST/JSON adapter over the core store service. It is a thin transport shim: every handler validates input, calls one core method, and renders the result or the single structured error envelope of ADR-0012. The web and MCP surfaces are peer adapters over the same core.

Governing: ADR-0012 (Backend Platform and API Shape), ADR-0003 (one core, thin adapters), SPEC-0002 (Artifact Lifecycle, REST Endpoints, Security).

The webhook open ingress: the single Public, anonymous-write endpoint in Cairn (SPEC-0005 HTTP endpoints table, `ANY /h/{id}`; ADR-0010 "Security of an open ingress endpoint"). External senders — services, agents, anything pointed at the endpoint's ingress URL — hit this route with no credential of any kind; it captures the inbound request into the endpoint's seq-ordered ring buffer (webhook.Service.Capture, story #83) and always answers with the SAME fixed, benign, inert response, never anything the payload steered (SPEC-0005 REQ "Fixed Benign Response").

This is the single most exposed surface in Cairn, so every guard here is load-bearing, not routine (SPEC-0005 "Security Requirements ... MANDATORY and CRITICAL"):

  • Inert capture — the payload is parsed only enough to store and index (content-type, size); it is NEVER executed, deserialized-to-execute, evaluated, or followed (REQ "No Payload Execution / Inert Capture"). This handler does not even attempt to interpret the body's content type; it hands the raw bytes straight to Capture.
  • Body-size cap BEFORE buffering — http.MaxBytesReader aborts the read the instant the configured cap is exceeded, so an oversize sender is 413'd without the full body ever landing in memory or object storage (REQ "Request Body Size Limits").
  • Per-source-IP AND per-endpoint rate limiting, each its OWN dedicated limiter (never sharing a budget with authenticated traffic) — a flood from one IP or at one endpoint is 429'd and captures nothing (REQ "Rate Limiting").
  • Header hygiene — delegated to webhook.Capture's sanitizeHeaders (webhook/sanitize.go), which drops Authorization/Cookie/hop-by-hop headers before a row is ever written, so a captured record is a sanitized projection, never a replayable credential dump (REQ "Header Hygiene & Ephemerality as Containment").
  • No SSRF — Cairn never fetches or follows any URL a payload contains; this handler only ever writes bytes it already has to storage (REQ "Redirect & SSRF Validation").
  • Write-only — Capture's return value is discarded entirely; a poster gets back a fixed acknowledgment, never the captured stream, another request, or any signal about the endpoint's contents (SPEC-0005 "Ingress grants no read").
  • Unguessable id, uniform 404 — an unknown or expired endpoint id maps to the SAME ErrEndpointNotFound Capture already returns uniformly (REQ "Unguessable ID & No Enumeration"), exactly mirroring every other link-capability read in this adapter.
  • Opaque internal failures — an unexpected Capture failure (e.g. an object-storage write error) is logged server-side with the request id and answered with the ordinary fixed response, never a stack trace or internal detail (REQ "Ingress error is opaque to the caller").

This route bypasses Pocket ID/OIDC and the session/OAuth auth stack entirely — deliberately: it is a machine ingress like /v1 and /mcp, public by design (SPEC-0005 "The ingress row is the single Public endpoint in Cairn"). It carries the strict /v1-style CSP (never the HTMX/Alpine webCSP) because, even though it renders no HTML, it must never loosen the hardened baseline on the surface attackers reach first.

Governing: ADR-0010, SPEC-0005 (Security Requirements section).

The webhook live SSE stream (`GET /v1/hooks/{id}/stream`, SPEC-0005 REQ "One Stream, Two Transports (Live Fan-out)", ADR-0010, ADR-0012 SSE transport). It is the browser-facing half of the identical seq-ordered capture log the mcp://cairn/hook/<id> MCP resource (mcp.go) reads over the other transport — both are thin adapters over webhook.Service's hub and RequestsAfter, exactly mirroring internal/httpapi/stream.go's split for the trajectory share type.

Governing: SPEC-0005 (Live Fan-out, Concurrency Safety), ADR-0010, ADR-0012 (SSE transport), ADR-0007 (link-capability reads).

hookMux contributes the webhook share type's /v1/hooks* management, metadata, and captured-request read surface through the ADR-0002 RouteMounter capability, exactly mirroring runMux (runs.go) for the other live share type. The webhook service is a runtime dependency (a live Postgres pool + object store), so the adapter captures it at construction and mounts its routes through this seam rather than the core router hard-coding webhook-specific paths.

This is the model + management story (issue #83): creation requires authentication and passes the CSRF seam; every read follows the endpoint's ADR-0007 link capability, so a valid id reads and an unknown/expired id is a uniform 404 with no owner check on the read path (SPEC-0005 "Read endpoints MUST enforce the ADR-0007 link-capability policy"). The public, anonymous-write ingress that actually fills the buffer (`ANY /h/{id}`, SPEC-0005) is deliberately NOT mounted on this router group — it carries its own security posture entirely (no auth, its own rate limits) and lives in hook_ingress.go / api.go's separate top-level route group (issue #84).

Governing: ADR-0002 (RouteMounter capability), ADR-0010 (Live Webhook Endpoints), SPEC-0005 (Webhook Inspector — HTTP endpoints table), ADR-0007 (link-capability reads).

The MCP surface (SPEC-0007, ADR-0003, ADR-0004): a Go MCP server exposing Cairn's core operations — read, create & push (single-body artifacts, bundles, and trajectory runs), comment, react — as MCP tools, and the trajectory live-span stream as a readable MCP resource, mounted in-process at POST/GET /mcp (streamable HTTP transport) alongside the REST and web adapters in the same binary. It is a thin adapter: every handler resolves the caller's OAuth identity and scope, then calls exactly the same core method the REST adapter calls (store, annotation service, trajectory service) — ADR-0003 "no surface can fork the rules". artifact_create covers the single-body types (file, markdown, code); run_create/run_append_spans mirror POST /v1/runs and POST /v1/runs/{id}/spans over the trajectory service; bundle_create mirrors the multipart multi-file path the web/CLI use over store.CreateBundle — every creatable share type is reachable over MCP (issue #65).

Authorization is OAuth-only (SPEC-0007 endpoint table: "/mcp ... Required — OAuth 2.1 bearer access token, audience-bound to Cairn"): the static APIToken bearer surface and the insecure dev shortcut that authenticate /v1 are deliberately NOT wired here, so a static token can never reach the MCP transport. The subject of every call is the human the grant was issued to (agents inherit, never exceed, the human's reach); the acting model is read from the MCP client's `initialize` Implementation and stamped as provenance OnBehalfOf with channel `via MCP` (ADR-0004 subject-vs-actor).

Governing: ADR-0003 (triple-surface parity, in-process adapters over one core), ADR-0004 (MCP as a first-class surface with OAuth), SPEC-0007 REQ "MCP Tool Surface — Artifact & Bundle Read", REQ "Create & Push", REQ "Comment & React", REQ "MCP Resource Surface — Stream Reads", REQ "Subject/Actor Identity Mapping & Least Privilege", REQ "Error Handling Standards", REQ "Rate Limiting", REQ "Request Body Size Limits".

A2UI (Agent-to-UI) resources: render `application/a2ui+json` projections of a trajectory run's waterfall + stream, and a bundle's member cards, so an A2UI-capable MCP host (Crush once joestump-agent/crush#217 lands) draws the view inline instead of asking the model to re-render raw JSON.

Three resources ship, all read-only at the A2UI layer (mutations stay on the existing MCP tools; the surfaces are text-only — no buttons or actions are emitted — until the `a2ui_action` round-trip lands in joestump-agent/crush#221):

cairn://run/{id}/a2ui       — trace header + stats (category bar, hot
                              spots) + a span flame graph: per-span
                              timeline bars in a fixed-width gutter
cairn://bundle/{id}/a2ui    — bundle envelope (totals, type mix) + a
                              member list with badges, relative size
                              bars and engagement counts
cairn://artifact/{id}/a2ui  — single-body artifact (markdown, code, file)

All follow the A2UI-over-MCP transport contract: https://a2ui.org/guides/a2ui_over_mcp/. The wire shape is the same `{"version":"v0.9","updateComponents":{...}}` envelope the Crush inline <a2ui-json> scanner consumes, so the payload is directly spliceable into a chat reply.

Governing: joestump-agent/crush#217 (A2UI-over-MCP epic), issue #90 (server-side story), ADR-0003 (thin adapter — these handlers resolve the same core reads the REST/JSON handlers do and only re-project the result).

Tool input schema shaping for the MCP surface.

The SDK infers a tool's input schema from its handler's In type. For Go's nil-able kinds — a slice, a map, a pointer — that inference is faithful to Go and hostile to clients: it emits a *union* type, `"type": ["null", "array"]`, because a nil slice marshals to JSON null. JSON Schema permits that; a great many MCP clients do not. Faced with a type they cannot represent as a single string, they drop the whole subschema to `{}` — which leaves the parameter looking untyped, so the client sends the value however it likes (commonly a JSON-encoded string), and the server's own validator — which still holds the real union schema — rejects it. The agent then sees a server demanding an array its published schema gave no way to send.

That is not hypothetical: it is exactly how `bundle_create` became uncallable over MCP, and it is the same defect the string-encoded-array unwrap in mcp.go was added to paper over for `run_create`'s spans.

So the schema published for a tool collapses those unions to the single concrete type. Nothing is loosened by this: a required field was never legitimately null, and an optional one is still omissible — `required` is what governs presence, not a null branch in the type. What changes is that the parameter arrives at the client as `{"type": "array", "items": {...}}`, which every client can represent, so it sends an array and validation passes on the first try.

Governing: SPEC-0007 REQ "Create & Push", REQ "Agent-Shaped Tool Schemas".

Cairn is a self-contained OIDC relying party: it logs the human in directly against Pocket ID (authorization-code + PKCE + nonce), rather than sitting behind an oauth2-proxy forward-auth layer. The flow mints the SAME server-side session the dev-password login does (session.go), so every downstream consumer — the Bin, the comment composer, and critically the /oauth/authorize consent screen (ADR-0004) — needs no OIDC-awareness of its own: an OIDC-authenticated session IS a web session, full stop.

This mirrors ~/src/switchboard's internal/auth OIDC relying party (same libraries: github.com/coreos/go-oidc/v3 + golang.org/x/oauth2, same state+nonce+PKCE short-lived cookie shape), adapted to Cairn's existing session store (session.Store, keyed on a bare actor id string — Cairn has no separate "human" table to upsert into) and its existing cookie/CSRF helpers.

Governing: ADR-0013 (native OIDC relying party), issue #55.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type APIToken

type APIToken struct {
	// Secret is the bearer value the client presents. It is stored only as a
	// hash inside TokenAuthenticator, never compared in plaintext.
	Secret string
	// ActorID is the identity a request bearing this token authenticates AS. The
	// secret proves the identity; the client never asserts the actor id itself.
	ActorID string
	// IsAgent marks a token minted for an agent/MCP client acting on the human's
	// behalf, which is granted exactly the three ADR-0004 agent scopes and never
	// sharing:manage. A human personal token additionally carries sharing:manage.
	IsAgent bool
}

APIToken is a single static bearer credential: an opaque high-entropy secret that maps to an actor identity and its capability. It is the pre-OAuth MVP token seam (ADR-0004 Option B, "static API keys"); full OAuth 2.1 with dynamic client registration and refresh rotation replaces it in #22 by swapping this Authenticator, leaving every downstream authz check unchanged.

func ParseAPITokens

func ParseAPITokens(raw string) ([]APIToken, error)

ParseAPITokens parses the CAIRN_API_TOKENS configuration value into a set of static bearer credentials. The format is a comma-separated list of `secret:actor[:role]` entries, where role is `human` (default) or `agent`. Whitespace around entries and fields is trimmed. An empty value yields no tokens (the bearer surface then rejects every token, failing closed). A malformed entry, a blank secret/actor, or a duplicate secret is a configuration error surfaced at startup rather than a silently dropped credential.

Governing: ADR-0004 (token seam), ADR-0012 (config from the environment).

type Authenticator

type Authenticator interface {
	Authenticate(r *http.Request) (*Principal, error)
}

Authenticator resolves the authenticated principal for a request, or returns errs.ErrUnauthorized when credentials are absent or invalid. Real OAuth 2.1 (bearer) lands in ADR-0004 (#22); this seam is what that adapter, the static token adapter below, and the web-session adapter (#11) implement.

type Config

type Config struct {
	// BaseURL is the public origin used to build short URLs, e.g.
	// https://cairn.stump.wtf. No trailing slash.
	BaseURL string
	// MaxUploadBytes caps a single request body / upload part (413 above it).
	MaxUploadBytes int64
	// DefaultTTL is the artifact expiry assigned at create (ADR-0007).
	DefaultTTL time.Duration
	// MaxRequestedTTL bounds an explicit client-requested expiry (the CLI's
	// `--ttl` flag, SPEC-0008) sent as X-Cairn-Ttl-Seconds on POST
	// /v1/artifacts: the server remains authoritative over expiry (ADR-0007
	// "owner-adjustable ... subject to any workspace cap") — a request
	// outside (0, MaxRequestedTTL] is rejected as validation_failed rather
	// than silently clamped, so a caller never believes it got a longer TTL
	// than it did. This header is honored only on the REST create path (the
	// CLI/web surface); the MCP agent surface's artifact_create/
	// bundle_create schemas carry no TTL field at all and always get
	// DefaultTTL (internal/httpapi/mcp.go), matching the existing
	// human-vs-agent capability split (ADR-0004: e.g. delete is human-only).
	// Defaults to 30 days.
	MaxRequestedTTL time.Duration
	// RatePerSecond / RateBurst configure per-IP rate limiting; <= 0 disables.
	RatePerSecond float64
	RateBurst     int
	// MaxRunRequestBytes caps a trajectory run/append request body before it is
	// buffered, so an oversize batch is 413 rather than read into memory
	// (SPEC-0004 endpoint security). Defaults to 64 MiB.
	MaxRunRequestBytes int64
	// StreamHeartbeat is the interval between SSE heartbeat comments on the live
	// span stream, which keep proxies from idling the connection out and let the
	// server notice a vanished client on the next write. Defaults to 15s.
	StreamHeartbeat time.Duration
	// DevLoginPassword is the shared secret the MVP dev login (SPEC-0001,
	// ADR-0004) accepts for any actor id. Demoted to a local-dev-only fallback
	// by ADR-0013: it is honored only when OIDC is unconfigured (see
	// loginEnabled). An empty value disables interactive web login entirely.
	DevLoginPassword string
	// SessionTTL is the lifetime of a web session and its cookies. Defaults to 7
	// days.
	SessionTTL time.Duration
	// OIDC relying-party config (ADR-0013): Cairn authenticates humans directly
	// against Pocket ID rather than an external forward-auth proxy. OIDCIssuer
	// gates the whole feature — empty means OIDC is not configured and
	// EnableOIDC is a no-op, leaving the dev-password fallback as the only login
	// path. The redirect URI is always BaseURL + /auth/callback (derived, never
	// separately configured).
	OIDCIssuer       string
	OIDCClientID     string // defaults to "cairn" when empty
	OIDCClientSecret string
	// APITokens are the static bearer credentials the API/MCP surface accepts
	// (ADR-0004 MVP token seam). Each secret maps to an actor and role; an absent
	// set means the bearer surface rejects every token (fail closed). A raw
	// bearer string is never trusted as an actor id.
	APITokens []APIToken
	// DevInsecureBearerAuth, when true, additionally trusts a raw bearer token AS
	// the actor id (DevActorAuthenticator) after the verifying TokenAuthenticator
	// declines. It is a development/test-only shortcut that MUST stay false in
	// production; the production default verifies every bearer token.
	DevInsecureBearerAuth bool
	// OAuth 2.1 authorization-server tuning (SPEC-0007, ADR-0004).
	// AccessTokenTTL is the short audience-bound access-token lifetime (default
	// ~1h); RefreshTokenTTL the rotating refresh-token lifetime (default 30d).
	AccessTokenTTL  time.Duration
	RefreshTokenTTL time.Duration
	// OAuthRatePerSecond / OAuthRateBurst configure the dedicated per-IP rate
	// limiter on the OAuth bootstrap endpoints (register/token/revoke/authorize),
	// throttled tighter than the general surface to blunt client-spraying and
	// code/refresh brute force (SPEC-0007 REQ "Rate Limiting"). Defaults: 10/s,
	// burst 30; always on when the authorization server is wired.
	OAuthRatePerSecond float64
	OAuthRateBurst     int
	// HookIngressRatePerSecond / HookIngressRateBurst configure the open
	// webhook ingress's dedicated PER-SOURCE-IP limiter, and
	// HookEndpointRatePerSecond / HookEndpointRateBurst its PER-ENDPOINT
	// limiter (SPEC-0005 REQ "Rate Limiting": "rate-limited per-endpoint and
	// per-source-IP"). Both are separate from the general per-IP limiter
	// (RatePerSecond/RateBurst) because the anonymous-write ingress is the
	// single most exposed surface in Cairn and must carry its own budget,
	// never share one with authenticated traffic. Defaults: 5/s burst 20
	// per-IP, 10/s burst 50 per-endpoint; always on (a deployment that truly
	// wants no ingress throttling must set both to a very high value —
	// zero/negative still falls back to the default rather than disabling
	// it, matching the OAuth limiter's always-on posture on this
	// internet-facing surface).
	HookIngressRatePerSecond  float64
	HookIngressRateBurst      int
	HookEndpointRatePerSecond float64
	HookEndpointRateBurst     int
}

Config tunes the REST adapter.

type CredentialVerifier

type CredentialVerifier interface {
	Verify(actorID, secret string) bool
}

CredentialVerifier resolves a login form's (actor, secret) into the actor id a session is minted for, or false when the credential is rejected. It is the swap-in seam: the dev password verifier below is replaced by OAuth / an upstream IdP (#22) without touching session issuance. Governing: ADR-0004.

type DevActorAuthenticator

type DevActorAuthenticator struct{}

DevActorAuthenticator is the INSECURE development-only Authenticator: it trusts the raw bearer token AS the actor id, with no verification whatsoever. Anyone can therefore authenticate as any actor by typing their name, so it MUST NEVER be enabled in production — it exists solely so local development and the test suite can act as arbitrary actors without minting tokens. It is wired only when CAIRN_DEV_INSECURE_BEARER_AUTH is explicitly set (see New); the production default is the verifying TokenAuthenticator, so no production path trusts a raw bearer==actor.

Governing: ADR-0004 (the seam OAuth replaces; this dev stub is never a prod credential).

func (DevActorAuthenticator) Authenticate

func (DevActorAuthenticator) Authenticate(r *http.Request) (*Principal, error)

Authenticate implements Authenticator. The bearer token is taken verbatim as the actor id; the channel is fixed server-side to the REST surface's `via API` so provenance is still not client-spoofable even under this dev shortcut. The grant is the agent scope set (no sharing:manage) — a dev caller is never more privileged than an agent.

type DevPasswordVerifier

type DevPasswordVerifier struct {
	Password string
}

DevPasswordVerifier is the MVP login stub: any actor id authenticates with the single shared dev password. An empty password disables login entirely (the deployment opted out), so a misconfigured instance fails closed rather than accepting a blank secret.

func (DevPasswordVerifier) Verify

func (v DevPasswordVerifier) Verify(actorID, secret string) bool

Verify reports whether the presented secret matches the configured dev password under a constant-time compare. Login is refused outright when no password is configured or the actor id is blank.

type OAuthAuthenticator

type OAuthAuthenticator struct {
	// contains filtered or unexported fields
}

OAuthAuthenticator resolves `Authorization: Bearer <access token>` against the authorization server's issued token families. The resulting principal is the on-behalf-of mapping of ADR-0004: the SUBJECT (ActorID) is the human who approved consent — agents inherit, never exceed, the human's reach — while IsAgent marks the caller as an agent so human-only capabilities (delete, sharing) stay refused regardless of scopes. Scopes are exactly the grant's approved subset. The channel is server-derived for the presenting surface.

Governing: ADR-0004 (subject = human, actor = model), SPEC-0007 REQ "Subject/Actor Identity Mapping & Least Privilege".

func (*OAuthAuthenticator) Authenticate

func (a *OAuthAuthenticator) Authenticate(r *http.Request) (*Principal, error)

Authenticate implements Authenticator. Any failure — unknown, expired, revoked, or audience-mismatched token — is a uniform errs.ErrUnauthorized (401, SPEC-0007 scenario "Revoked token used").

type PATAuthenticator

type PATAuthenticator struct {
	// contains filtered or unexported fields
}

PATAuthenticator resolves `Authorization: Bearer <token>` against minted personal access tokens, wiring PATs into the same bearer chain as the static CAIRN_API_TOKENS entries and OAuth access tokens (ADR-0004: "wire PATs into the existing bearer auth chain ... so a PAT authenticates on /v1 exactly like a CAIRN_API_TOKENS entry"). The subject (ActorID) is always the owning human; IsAgent is the token's own flag, which keeps human-only capabilities (delete; httpapi.requireHuman) off an agent-marked PAT regardless of which of the three scopes it holds — sharing:manage is never grantable to any PAT at all, since pat.ParseScope only recognizes the three ADR-0004 scopes.

func (*PATAuthenticator) Authenticate

func (a *PATAuthenticator) Authenticate(r *http.Request) (*Principal, error)

Authenticate implements Authenticator. Any failure — unknown or revoked secret — is a uniform errs.ErrUnauthorized, matching the other bearer authenticators' fail-closed shape.

type Principal

type Principal struct {
	ActorID string
	Channel artifact.Channel
	IsAgent bool
	Scopes  map[string]bool
	// Ambient reports whether the caller was authenticated by an ambient
	// credential the browser attaches automatically — a session cookie — rather
	// than an explicit bearer token. Only ambient credentials are forgeable by
	// a cross-site request, so CSRF protection is gated on this flag: token
	// callers (API/MCP/CLI) are exempt, cookie-session callers are guarded
	// (SPEC-0006 REQ "CSRF Protection"). The token authenticators leave it false;
	// the web-session Authenticator (#11) sets it true.
	Ambient bool
}

Principal is the authenticated caller. Channel is derived server-side from the authenticated surface — never from a client claim (SPEC-0002 "Channel is server-derived"). Scopes gate capabilities such as sharing:manage, which agents do not receive (SPEC-0002 "Agent cannot broaden sharing").

func (*Principal) HasScope

func (p *Principal) HasScope(scope string) bool

HasScope reports whether the principal holds scope.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the /v1 REST adapter over the core store and the ADR-0011 web app shell. Both are thin projections of the same core (ADR-0003): the JSON API and the HTML shell share the store, registry, and annotation service, so they can never disagree about an artifact's facts.

func New

func New(st *store.Store, reg *sharetype.Registry, auth Authenticator, cfg Config, logger *slog.Logger) *Server

New constructs a Server. If auth is nil, a session-aware Authenticator is used when a store is present (bearer tokens for API/MCP/CLI, session cookies for the web, per SPEC-0001/ADR-0004), falling back to the bare BearerAuthenticator for storeless unit wirings; if logger is nil slog.Default() is used.

func (*Server) EnableOIDC

func (s *Server) EnableOIDC(ctx context.Context) error

EnableOIDC discovers the configured issuer and wires the OIDC relying-party login flow. It is a no-op (returns nil, leaves s.oidc nil) when CAIRN_OIDC_ISSUER is unset — the local dev_login_password fallback then stays the only login path (ADR-0013). Call once at startup; a discovery failure is returned so the caller can fail fast rather than silently run with human login broken.

Governing: ADR-0013, issue #55.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the routed http.Handler for both surfaces this adapter serves: the /v1 REST/JSON API and the ADR-0011 web app shell. The two are separate route groups so each carries its OWN Content-Security-Policy — the API keeps the strict `default-src 'none'` (securityHeaders) while the HTML shell gets webCSP (self + the script/style/font origins HTMX+Alpine need), with neither policy loosening the other (SPEC-0001 REQ "Security Headers"). The RequestID/RealIP/Recoverer and per-IP rate limiter are shared, so id resolution is throttled on both surfaces (SPEC-0001 REQ "Rate Limiting").

type SessionAuthenticator

type SessionAuthenticator struct {
	// contains filtered or unexported fields
}

SessionAuthenticator is the web-aware Authenticator adapter. It resolves a request to a principal by first honoring an explicit bearer token (API / MCP / CLI, non-ambient), then falling back to the session cookie (web, ambient). A bearer caller therefore keeps the API's non-ambient, CSRF-exempt semantics even on the web binary, while a browser session is ambient and CSRF-guarded. The channel is server-derived per surface (SPEC-0009): via API for the token path, via web for the cookie path — never a client claim.

Governing: ADR-0004 (one Authenticator seam), SPEC-0009 (server-derived actor/channel), SPEC-0006 (CSRF gated on Ambient).

func (*SessionAuthenticator) Authenticate

func (a *SessionAuthenticator) Authenticate(r *http.Request) (*Principal, error)

Authenticate implements Authenticator. Order matters: an explicit bearer token wins so a scripted caller is never accidentally treated as an ambient browser.

type TokenAuthenticator

type TokenAuthenticator struct {
	// contains filtered or unexported fields
}

TokenAuthenticator verifies an `Authorization: Bearer <token>` credential against a static registry of known secrets, resolving each to the actor and scopes it was minted for. This is the security-critical replacement for the old dev stub: a raw bearer string is NEVER trusted as an actor id — it must hash to a registered secret, or the request is unauthorized. Secrets are held only as SHA-256 digests, and lookup hashes the presented token to a fixed-width key so verification cost does not vary with which token matched.

The channel is server-derived to `via API` for this surface (SPEC-0002 "Channel is server-derived"); an agent token is granted only the ADR-0004 agent scopes, so no token can broaden sharing or delete another actor's work.

Governing: ADR-0004 (MCP/OAuth token seam — this is the MVP static-token bridge to #22), SPEC-0002 (server-derived channel), SPEC-0006 (auth seam).

func NewTokenAuthenticator

func NewTokenAuthenticator(tokens []APIToken) *TokenAuthenticator

NewTokenAuthenticator builds a TokenAuthenticator over the given static credentials. A nil/empty set yields an authenticator that rejects every bearer token — the fail-closed default for a deployment that configured none.

func (*TokenAuthenticator) Authenticate

func (a *TokenAuthenticator) Authenticate(r *http.Request) (*Principal, error)

Authenticate implements Authenticator. An absent bearer, or one whose secret is not registered, is errs.ErrUnauthorized. A registered secret resolves to its actor with the server-derived `via API` channel and the scopes its role grants — never to whatever the caller typed after `Bearer `.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL