/** * The daemon's identity/actor seam (task #88, locked decision #6). * * LOCAL IS SACRED: the default deployment stays exactly what it was — a * 127.0.0.1-bound daemon with loopback CORS and NO auth, where every request * implicitly acts as the full-trust local operator. Nothing in this module * runs a verifier, reads a token file, or changes a response in that mode; it * only pins ONE actor shape onto the request so downstream code (audit trail, * bus events, gate decisions) never branches on "was there auth". * * TEAM/HOSTED REQUIRES AUTH: `WICKED_RUNTIME=team` or `WICKED_CREW_AUTH=required` * arms a bearer-token check on `/api/v1/*` and the `/ws` upgrade paths. * Verification is pluggable behind {@link TokenVerifier}; v1 ships the static * workload-token verifier (hashed-at-rest file, {@link loadTokenFile}) and the * OIDC verifier ({@link OidcConfig} + {@link createOidcVerifier}, crew#249): * JWTs checked against the issuer's JWKS (discovery or an explicit `jwksUri`), * `iss`/`aud`/expiry enforced. A malformed OIDC config fails the boot loudly * rather than running with a verifier that was never configured. * * Deny semantics: missing/unknown token in required mode → 401 (with * `WWW-Authenticate: Bearer`); an authenticated actor below a route's rung on * the trust ladder → 403. The ladder is {@link requiredTrust} — minimal and * documented (docs/auth.md), not a per-route registry. */ import type { FastifyInstance, FastifyRequest } from 'fastify'; import type { Actor, TrustLevel } from '../core/types.js'; declare module 'fastify' { interface FastifyRequest { /** * The authenticated actor — or {@link LOCAL_ACTOR} when auth is off. Set by * the identity hook for every request under `/api/v1` and `/ws`; routes * outside that scope (SPA assets) never read it. The two unit-token routes * ({@link isUnitTokenRoute}) are the one exception inside the scope: they * carry no actor, the body's capability token is their credential. */ actor: Actor; } } export type AuthMode = 'required' | 'off'; /** * The implicit full-trust local operator. `admin` is the ladder's top rung — * the local single-operator deployment has exactly one human and no boundary * to enforce between that human and the daemon, so "full trust" and "admin" * are the same fact spelled once. */ export declare const LOCAL_ACTOR: Actor; /** True when `actor` sits at or above `min` on the ladder. */ export declare function trustAtLeast(actor: Actor, min: TrustLevel): boolean; /** * Resolve the daemon's auth mode. DENY-DOMINANT: `WICKED_RUNTIME=team` forces * `required` and nothing can talk it back down — a team runtime with auth * switched off is exactly the silently-ungoverned state this seam exists to * prevent. Outside team runtime, `WICKED_CREW_AUTH` (`required`|`off`) is * explicit; unset means the local default, off. */ export declare function resolveAuthMode(env?: NodeJS.ProcessEnv): AuthMode; /** * The verifier seam. A verifier resolves a presented bearer token to an * {@link Actor}, or `null` when the token is not one of its. Verifiers are * tried in order; the first non-null answer wins: the static workload-token * verifier, then the OIDC verifier when `auth.json` configures one * (docs/auth.md § "The OIDC seam"). */ export interface TokenVerifier { /** Names the verifier in logs and errors (`static-tokens`, `oidc`). */ readonly name: string; verify(token: string): Promise; } /** Where the workload-token file lives unless overridden. */ export declare function defaultTokensPath(env?: NodeJS.ProcessEnv): string; /** One entry of `tokens.json` — the HASH of a token, never the token itself. */ export interface TokenFileEntry { /** Lowercase hex SHA-256 of the bearer token (64 chars). */ sha256: string; actor: Actor; /** Operator note ("ci runner", "maria's laptop"); never used for decisions. */ label?: string; } /** `sha256` hex of a presented token — the only form the daemon stores or compares. */ export declare function tokenHash(token: string): string; /** * Parse a tokens.json payload into a hash→actor map. FAILS CLOSED on any * malformed entry: a typo'd trust level that silently skipped its entry would * lock an operator out (or, worse, a hand-edit that half-parsed would demote * someone) with nothing saying so. The error names the entry index and field. */ export declare function parseTokenFile(json: string, source: string): Map; /** * Load `tokens.json` from disk into a static verifier. A MISSING file is a * loud warning, not a boot failure: the daemon still starts and every request * 401s, which is the honest fail-closed posture (an unreachable daemon tells * the operator less than one that answers "no tokens are registered"). * A PRESENT-but-malformed file fails the boot — see {@link parseTokenFile}. */ export declare function loadTokenFile(path: string, warn: (msg: string) => void): Map; /** The static workload-token verifier: sha256 the presented token, look it up. */ export declare function createStaticVerifier(entries: Map): TokenVerifier; /** * The OIDC verifier's config shape. Lives in `~/.config/wicked-crew/auth.json` * as `{ "oidc": … }`. */ export interface OidcConfig { /** The issuer URL — discovery happens at `/.well-known/openid-configuration`. */ issuer: string; /** The audience (`aud`) access tokens must carry to be accepted by this daemon. */ audience: string; /** Explicit JWKS URI; omit to use the discovery document's `jwks_uri`. */ jwksUri?: string; /** * Claim mapping. `id` defaults to `sub`; `trust` names a claim whose value is * a {@link TrustLevel} (defaults to the `defaultTrust` below when absent). * OIDC-verified principals are always `kind: 'human'` — workloads use tokens. */ claims?: { id?: string; trust?: string; }; /** Trust assigned when the trust claim is absent (default `observer` — least privilege). */ defaultTrust?: TrustLevel; } /** Where the auth config (OIDC seam) lives unless overridden. */ export declare function defaultAuthConfigPath(env?: NodeJS.ProcessEnv): string; /** * Create an OIDC verifier that validates JWTs against the issuer's JWKS. * Supports RS256/384/512, ES256/384/512, PS256/384/512. JWKS keys are cached * per issuer with a 5-minute TTL and auto-refreshed on unknown kid. */ export declare function createOidcVerifier(config: OidcConfig): TokenVerifier; /** `createServer`'s auth options — everything defaults to env/file resolution. */ export interface AuthOptions { /** Override mode resolution (tests / embedders). Default: {@link resolveAuthMode}. */ mode?: AuthMode; /** Token file path. Default: `WICKED_CREW_TOKENS` or `~/.config/wicked-crew/tokens.json`. */ tokensPath?: string; /** auth.json path (the OIDC seam's config). Default: `~/.config/wicked-crew/auth.json`. */ configPath?: string; /** * Replace the default verifier chain entirely (tests / embedders with their * own identity source). When set, the token file and auth.json are not read. */ verifiers?: TokenVerifier[]; /** * Origins allowed CORS when auth is required, beyond loopback. Default: * `WICKED_CREW_ALLOWED_ORIGINS` (comma-separated) or — when unset — ANY * origin: bearer auth carries no ambient credential, so cross-origin JS * without a token holds nothing (docs/auth.md § "CORS pairing"). */ allowedOrigins?: string[]; } /** The resolved auth state `createServer` builds once and hooks consult per request. */ export interface ResolvedAuth { mode: AuthMode; verifiers: TokenVerifier[]; /** `null` = any origin (required mode's default); else an allowlist. Studio's dev origin always passes (origin-guard.ts). */ allowedOrigins: string[] | null; } /** Resolve mode + verifier chain. Reads files ONLY in required mode. */ export declare function resolveAuth(options: AuthOptions | undefined, warn: (msg: string) => void): ResolvedAuth; /** * Minimum trust for a request. Minimal by design (the task's rule: no * deny-by-default registry over unknown routes): * * - `/ws/terminals` and any sub-path → `operator` (write-capable WS channel; * inbound frames are raw PTY stdin regardless of the GET upgrade method) * - reads (GET/HEAD) and the main `/ws` stream → `observer` * - governance writes, settings writes, project archive/restore → `admin` * - every other mutation (launch, gate, cancel, chats, terminals, repos, * workflows, project CRUD/membership) → `operator` * * `body` is consulted for exactly one rule: `PATCH /projects/:id` is an * `operator` rename/describe unless the patch carries `status` (archive or * restore), which changes what the whole team can attach work to — `admin`. */ export declare function requiredTrust(method: string, path: string, body: unknown): TrustLevel; /** True for the paths the auth boundary covers: the API and the WS upgrades. */ export declare function isProtectedPath(path: string): boolean; /** True for exactly the unit-token routes above (method and path both exact). */ export declare function isUnitTokenRoute(method: string, path: string): boolean; /** * Extract the presented bearer token. `Authorization: Bearer ` everywhere; * the `/ws` upgrade paths ALSO accept `?access_token=` (RFC 6750 §2.3), * because the browser `WebSocket` constructor cannot set headers. Header wins * when both are present. */ export declare function extractToken(req: FastifyRequest, path: string): string | null; /** * Install the identity (401) and trust (403) hooks. * * Identity runs at `onRequest` — before body parsing, and (per * `@fastify/websocket`) before the upgrade completes, so a bad token is an * HTTP 401 on the wire for REST and WS alike. Trust runs at `preHandler`, * where the parsed body exists (the one body-dependent rule is the project * archive). Both scope to {@link isProtectedPath}; the SPA shell and its * assets stay public so a hosted skin can load and ASK for a token. */ export declare function registerAuthHooks(app: FastifyInstance, auth: ResolvedAuth): void; //# sourceMappingURL=auth.d.ts.map