# `resolver` — `createJwtSessionResolver` _(no submodules)_ ## Purpose Stage-4 concrete `SessionResolver` factory for the API plugin foundation. Reads a JWT from the request (Authorization header or configurable cookie), verifies it via `@suaveplan/jws` against either a JWKS endpoint (cached and refreshed on `kid` miss) or a single static JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy, and projects the validated claims onto a `Principal` via an overridable mapper. The factory is the only public entry point — internals (`JwksCache`, claims mapper, error registry) are surface-exported for testability but most consumers use `createJwtSessionResolver(opts)` alone. ## Features - `Authorization: Bearer ` header extraction with case-insensitive scheme match; configurable header name accepts a raw token form for non-standard headers (e.g. `x-api-token: `). - Cookie fallback when `cookieName` is supplied; header still wins when both are present. - JWKS resolution via `JwksCache` with positive TTL, negative cache, in-flight dedup, and `kid`-miss refresh. - Static-key path for HS-family symmetric tokens or single-key deployments. - Mandatory `iss` and `aud` enforcement (single string or list); `exp` and `nbf` enforced when present with configurable `clockToleranceSec`. - Per-request soft failures collapse to `null` so the upstream `SessionResolverChain` can fall through to the next adapter; operator failures (JWKS unreachable, malformed mapper, malformed static key) surface as `AuthJwtError` with the documented codes. - Idempotent `dispose()` flips an internal flag so subsequent `resolve()` calls return `null` rather than emitting late traffic on a torn-down resolver. ## Usage: Basic ```ts import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver"; const resolver = createJwtSessionResolver({ jwksUrl: "https://idp.example/.well-known/jwks.json", issuer: "https://idp.example", audience: "https://api.example", algorithms: ["RS256"], }); const req = new Request("https://api.example/users", { headers: { authorization: "Bearer eyJ..." }, }); const principal = await resolver.resolve(req); ``` ## Usage: Advanced Static HS256 key, cookie fallback, custom claims mapper, and an injectable clock for deterministic temporal-claim tests: ```ts import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver"; const resolver = createJwtSessionResolver({ staticKey: { kty: "oct", k: "" }, issuer: ["https://idp1.example", "https://idp2.example"], audience: "https://api.example", algorithms: ["HS256"], cookieName: "sp_session", clockToleranceSec: 5, clock: { nowMs: () => 1_700_000_000_000 }, claimsToPrincipal: (claims) => { const sub = claims["sub"]; if (typeof sub !== "string") return null; return { id: sub as never, scopes: [] as never[], tenantId: typeof claims["org_id"] === "string" ? (claims["org_id"] as never) : undefined, claims, }; }, }); ``` ## API Reference - `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver` — returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` + idempotent `dispose`). ## Implementation Notes The resolver deliberately separates "this is not my caller" (chain falls through, returns `null`) from "the host is mis-configured" (throws `AuthJwtError`). Verification failures inside `@suaveplan/jws` return `null` so a stray Bearer header from an unrelated IdP cannot 401-storm the API — the chain's next adapter (anonymous, static key, etc.) still gets its turn. The temporal-claim check uses a single clock reading per request to avoid TOCTOU between the `exp` and `nbf` arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather than push it into `@suaveplan/jws` because those are JWT-claim concerns, not JWS-protected-header concerns — the JWS package deliberately handles only RFC 7515 (signature) and leaves RFC 7519 (payload claims) to its consumers.