Files
suaveplan/openspec/archive/2026-05-26-api-auth-jwt/proposal.md
2026-05-27 23:29:53 +01:00

3.8 KiB

@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.