# `@suaveplan/api-auth-jwt` — JWT session resolver **Package**: `@suaveplan/api-auth-jwt` **Status**: PROPOSED **Type**: new-package **Stage**: 4 (Base adapters) — see umbrella `openspec/changes/api-plugin-foundation/` ## Why Per design doc §2.3, auth is split into two distinct contracts: session resolution (`Request → Principal | null`) and account lifecycle (provider). `api-auth-jwt` implements only the session-resolver half — it verifies a JWT from an `Authorization: Bearer` header or cookie against a JWKS URL or static key using `@suaveplan/jws`, and projects the resulting claims onto a `Principal`. It is NOT a provider: no login/register/refresh routes. This is what you pair with hosted IdPs (Clerk, Auth0, Cognito) or with any self-hosted JWT-issuing provider. Per Stage 4: ships both a direct callable surface and a worker-proxy surface (§5.2a). `runAuthSessionConformance` passes against both. ## What changes Net-new package depending on `@suaveplan/api-auth` (contract), `@suaveplan/api-types`, `@suaveplan/api-worker-rpc`, `@suaveplan/jws`, `@suaveplan/error`, and `zod`. - ADDED: `createJwtSessionResolver(opts)` — direct-surface factory returning an `AuthAdapter` (session-resolver only). - ADDED: `createJwtSessionResolverWorkerProxy(port, opts)` — proxy-surface factory. - ADDED: `JwtSessionResolverOptions` — Zod schema (`jwksUrl?: string`, `staticKey?: JsonWebKey`, `algorithms: string[]`, `audience?: string | string[]`, `issuer?: string | string[]`, `clockToleranceSec?: number`, `cookieName?: string`, `headerName?: string`, `claimToPrincipal?: (claims) => Principal`). - ADDED: `defaultClaimToPrincipal` — projects `sub`/`scope`/`scopes`/`tenant_id`/`tid` onto a `Principal`. - ADDED: `securitySchemeContribution()` — returns OpenAPI `securitySchemes` block describing the Bearer JWT scheme (consumed by `api-openapi` Stage 8). - ADDED: `AUTH_JWT_*` error codes (`AUTH_JWT_MISSING_TOKEN`, `AUTH_JWT_INVALID_SIGNATURE`, `AUTH_JWT_EXPIRED`, `AUTH_JWT_WRONG_AUDIENCE`, `AUTH_JWT_WRONG_ISSUER`, `AUTH_JWT_ALGORITHM_NOT_ALLOWED`, `AUTH_JWT_JWKS_FETCH_FAILED`, `AUTH_JWT_INVALID_OPTIONS`). - ADDED: JWKS caching with TTL + ETag revalidation; refresh on `kid` miss. ## Out of scope - Account lifecycle (register/login/logout/refresh) — that's `auth.provider`, not this package. See `api-auth-better-auth` (Stage 6) and `api-auth-rolled` (Stage 8). - Cookie-based CSRF gating — the host's request pipeline (§6) triggers `Bun.CSRF` based on auth-adapter declaration; this package only declares whether cookie auth is in use. - Multi-resolver chaining — handled by `api-auth` core (Stage 1). ## Source mapping Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`. | Requirement | Design doc section | |---|---| | Session resolver, not provider | §2.3 | | JWKS + static-key support | §2.3 (`api-auth-jwt` bullet) | | Worker-proxy variant | §5.2a | | Conformance both surfaces | §13.4 | | OpenAPI securityScheme contribution | §7 | ## Verification approach - `src/factory/create-jwt-session-resolver.test.ts` — direct-surface tests against real `@suaveplan/jws`-signed tokens. - `src/factory/create-jwt-session-resolver.spec-conformance.test.ts` — `runAuthSessionConformance("jwt-direct", ...)`. - `src/proxy/create-jwt-session-resolver-worker-proxy.test.ts` — proxy unit tests over `MessageChannel`. - `src/proxy/create-jwt-session-resolver-worker-proxy.spec-conformance.test.ts` — `runAuthSessionConformance("jwt-worker-proxy", ...)`. - `src/jwks/jwks-cache.test.ts` — TTL, ETag, `kid` miss refresh. - `src/claim-mapping/default-claim-to-principal.test.ts` — claim projection, scope normalization, tenant extraction. - `tests/integration/integration.test.ts` — sign with `@suaveplan/jws`, verify through this resolver end-to-end. ## Migration impact None — additive only.