Files
2026-05-27 23:29:53 +01:00

4.9 KiB

Capability Spec Delta: api-auth-jwt (api-auth-jwt)

Package: @suaveplan/api-auth-jwt Status: PROPOSED

ADDED Requirements

Requirement: Session-resolver-only surface

The package SHALL implement the AuthAdapter session-resolver contract from @suaveplan/api-auth. It MUST NOT register any HTTP routes — login/logout/refresh are provider responsibilities and live in separate adapters.

Scenario: factory exposes resolveSession but no route contributions

  • WHEN createJwtSessionResolver({ jwksUrl: "..." }) is called
  • THEN the returned adapter has resolveSession AND no contributeRoutes method AND securitySchemeContribution() returns a Bearer JWT entry

Verified by: src/factory/create-jwt-session-resolver.test.ts::"exposes resolveSession and no route contributions"


Requirement: JWT verification against JWKS or static key

The package SHALL verify tokens using @suaveplan/jws against either a JWKS URL (with TTL + ETag caching) or a single static JsonWebKey. Algorithms MUST be restricted to the allowlist in options.algorithms; any token signed with a non-allowlisted algorithm MUST be rejected with AUTH_JWT_ALGORITHM_NOT_ALLOWED.

Scenario: token signed with disallowed alg is rejected

  • WHEN a token signed with HS256 is presented to a resolver whose algorithms: ["RS256"]
  • THEN resolveSession() returns null AND emits AUTH_JWT_ALGORITHM_NOT_ALLOWED on the failure counter

Scenario: JWKS kid miss triggers refresh

  • WHEN a token's kid is not in the cached JWKS AND the cache is older than 0ms
  • THEN the resolver refetches the JWKS exactly once (in-flight dedup) AND retries verification

Verified by: src/factory/create-jwt-session-resolver.test.ts::"rejects tokens signed with non-allowlisted algorithms" AND src/jwks/jwks-cache.test.ts::"kid miss triggers a single in-flight JWKS refresh"


Requirement: Direct and worker-proxy surfaces both pass conformance

The adapter SHALL pass runAuthSessionConformance from @suaveplan/api-auth/testing under BOTH createJwtSessionResolver (direct) AND createJwtSessionResolverWorkerProxy (proxy). Neither surface may skip an invariant.

Scenario: direct surface conforms

  • WHEN runAuthSessionConformance("jwt-direct", () => createJwtSessionResolver(...)) runs
  • THEN every invariant passes

Scenario: worker-proxy surface conforms

  • WHEN runAuthSessionConformance("jwt-worker-proxy", () => createJwtSessionResolverWorkerProxy(channel.port2, ...)) runs over a real MessageChannel
  • THEN every invariant passes

Verified by: src/factory/create-jwt-session-resolver.spec-conformance.test.ts::"jwt-direct conforms to AuthSession contract" AND src/proxy/create-jwt-session-resolver-worker-proxy.spec-conformance.test.ts::"jwt-worker-proxy conforms to AuthSession contract"


Requirement: Claim → Principal projection is overridable

The package SHALL provide defaultClaimToPrincipal mapping sub → id, scope/scopes → scopes[], tenant_id/tid → tenantId, remaining claims into claims. Callers MAY override via options.claimToPrincipal.

Scenario: custom mapper supplies tenantId from a non-standard claim

  • WHEN options.claimToPrincipal reads org_id instead of tenant_id
  • THEN the produced Principal.tenantId reflects org_id

Verified by: src/claim-mapping/default-claim-to-principal.test.ts::"custom claimToPrincipal overrides default tenantId extraction"


The package SHALL extract the token from the configured header (default Authorization: Bearer <jwt>) or cookie (when cookieName is set). If both are present, header wins.

  • WHEN cookieName: "session" is configured AND a request carries Cookie: session=<jwt>
  • THEN the resolver returns the projected Principal
  • WHEN a request carries BOTH a valid Bearer header AND a valid cookie
  • THEN the Principal reflects the header token's claims

Verified by: src/factory/create-jwt-session-resolver.test.ts::"extracts token from cookie when configured" AND src/factory/create-jwt-session-resolver.test.ts::"header source wins over cookie when both present"


Requirement: Options Zod-validated

JwtSessionResolverOptions MUST be Zod-validated. Constructing the resolver with no jwksUrl AND no staticKey MUST throw AUTH_JWT_INVALID_OPTIONS synchronously.

Scenario: missing key material throws

  • WHEN createJwtSessionResolver({} as never)
  • THEN a SuaveplanError with code AUTH_JWT_INVALID_OPTIONS is thrown synchronously

Verified by: src/options/jwt-session-resolver-options.test.ts::"throws when neither jwksUrl nor staticKey is supplied"