# `resolver` — `SessionResolver` contract _(no submodules)_ ## Purpose `SessionResolver` is the abstract per-request session-resolver shape that every concrete adapter in Stages 4/6/8 (`api-auth-jwt`, `api-auth-anonymous`, `api-auth-bearer-static`, `api-auth-better-auth`, `api-auth-rolled`) implements. It deliberately captures only the universal vocabulary — inspect a WHATWG `Request`, return a `Principal | null`, dispose — so any concrete resolver can plug behind the host's chain runner without further coordination. The tri-valued return contract (principal / null / throw) collapses to two host-level outcomes: the chain short-circuits on the first principal, the host applies its `publicRoutes` decision when every resolver returned `null`, and `AUTH.RESOLVER_THREW` is surfaced when any resolver throws (fail-closed by default per design doc §6). ## Features - Generic over `P extends Principal` — concrete adapters MAY narrow `Principal` via interface merging without losing assignability to the chain's `Principal` return type. - Async by construction; resolvers MAY fetch JWKs, hit Redis, or call a token introspection endpoint. - `dispose` is part of the contract, not a side concern — the host blue/green rotates resolver chains and relies on `dispose` to release JWK caches, close HTTP keepalive pools, etc. ## Usage: Basic A minimal resolver that always returns null (the "anonymous fallback" pattern codified in `api-auth-anonymous`): ```ts import type { SessionResolver } from "@suaveplan/api-auth/resolver"; const anonymous: SessionResolver = { async resolve() { return null; }, async dispose() { /* no resources to release */ }, }; ``` ## Usage: Advanced A resolver that reads a `Bearer` token, parses claims, and returns a typed principal — with `dispose` releasing a token-cache eviction timer: ```ts import type { SessionResolver } from "@suaveplan/api-auth/resolver"; import type { Principal, PrincipalId, Scope } from "@suaveplan/api-types/principal"; interface JwtClaims { sub: string; scope?: string; [key: string]: unknown; } class JwtResolver implements SessionResolver { private readonly evictTimer: ReturnType; private disposed = false; public constructor(private readonly verifyJwt: (raw: string) => Promise) { this.evictTimer = setInterval(() => { /* prune cache */ }, 30_000); } public async resolve(req: Request): Promise { const header = req.headers.get("authorization"); if (!header?.startsWith("Bearer ")) { return null; } const claims = await this.verifyJwt(header.slice("Bearer ".length)); if (claims === null) { return null; } const scopes = (claims.scope ?? "").split(" ").filter(Boolean) as readonly Scope[]; return { id: claims.sub as PrincipalId, scopes, claims, }; } public async dispose(): Promise { if (this.disposed) { return; } this.disposed = true; clearInterval(this.evictTimer); } } ``` ## API Reference | Symbol | Kind | Description | |-------------------|-------------|------------------------------------------------------| | `SessionResolver` | interface | `resolve(req): Promise

` + `dispose()`. | | `P` (generic) | type param | Defaults to `Principal`. Narrowing is opt-in. | ## Implementation Notes - `Request` is the WHATWG Fetch `Request` (DOM lib). Node ≥18, Bun, Deno, and every Workers runtime all expose it as a global; `api-auth` does not import any Node-specific request type so the contract is universal. - Returning `null` for "credential present but invalid" is acceptable but discouraged — emit `AUTH.PRINCIPAL_INVALID_SHAPE` instead, so the host can distinguish "no credential" from "bad credential" in metrics. - `dispose` SHOULD be idempotent; tests in the conformance harness call it twice in a row and assert no throw.