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
resolveSessionAND nocontributeRoutesmethod ANDsecuritySchemeContribution()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
HS256is presented to a resolver whosealgorithms: ["RS256"] - THEN
resolveSession()returnsnullAND emitsAUTH_JWT_ALGORITHM_NOT_ALLOWEDon the failure counter
Scenario: JWKS kid miss triggers refresh
- WHEN a token's
kidis 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 realMessageChannel - 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.claimToPrincipalreadsorg_idinstead oftenant_id - THEN the produced
Principal.tenantIdreflectsorg_id
Verified by: src/claim-mapping/default-claim-to-principal.test.ts::"custom claimToPrincipal overrides default tenantId extraction"
Requirement: Token extraction honours header and cookie sources
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.
Scenario: cookie source produces a Principal
- WHEN
cookieName: "session"is configured AND a request carriesCookie: session=<jwt> - THEN the resolver returns the projected
Principal
Scenario: header takes precedence over cookie
- 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
SuaveplanErrorwith codeAUTH_JWT_INVALID_OPTIONSis thrown synchronously
Verified by: src/options/jwt-session-resolver-options.test.ts::"throws when neither jwksUrl nor staticKey is supplied"