5.8 KiB
Capability Spec Delta: api-auth-rolled (api-auth-rolled)
Package: @suaveplan/api-auth-rolled
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
Scope note (Stage 8.12 keystone): this change ships the rolled session-resolver half of the eventual dual contract. Issues RS256 JWTs with rotating
kidslots, projects DB rows onto thePrincipalcontract, and contributes cookie + bearer OpenAPI security schemes. The matchingAuthProviderAdapterhalf (account-lifecycle routes, password gating, MFA, OAuth) is deferred to a successor changeapi-auth-rolled-providerso this milestone can ship at 100% coverage in a single iteration.
ADDED Requirements
Requirement: Implements auth.session
The package SHALL expose createRolledAuthAdapter returning an
AuthAdapter (session resolver + security scheme contribution).
Scenario: adapter exposes the auth.session contract
- WHEN
createRolledAuthAdapter({ store, keys, issuer, audience })is called - THEN the returned value implements
{ resolver, securityScheme }per@suaveplan/api-auth/session-adapter
Verified by: src/session/session.test.ts::"adapter exposes resolver and securityScheme"
Requirement: Session tokens are RS256-signed JWTs with rotating kid
The package SHALL issue session tokens as RS256 JWTs signed via
@suaveplan/crypto-rsa + @suaveplan/jws. Keys are identified by kid
so rotation is possible without invalidating in-flight sessions. The
TokenRotator manages an active key (signs) and retired-but-non-revoked
keys (verify only). Expired tokens cause resolveSession to return null
(soft failure per SessionResolver chain semantics) — the host's chain
then emits 401.
Scenario: expired token rejected
- WHEN a JWT whose
expis in the past is presented - THEN
resolveSessionreturns null
Scenario: token signed by retired key still verifies
- WHEN a JWT signed under
kid=k0is presented after the active key has rotated tokid=k1, andk0is retired but not revoked - THEN
resolveSessionreturns the projected principal
Scenario: token signed by revoked key rejected
- WHEN a JWT whose
kidis revoked is presented - THEN
resolveSessionreturns null
Verified by: src/session/session.test.ts::"expired token returns null"
AND src/session/session.test.ts::"retired but non-revoked kid still verifies"
AND src/session/session.test.ts::"revoked kid rejected"
Requirement: Principal projection maps DB rows to api-types Principal
The package SHALL project each authenticated user row into the
Principal shape from api-types (id, scopes, optional tenantId,
claims).
Scenario: projection includes scopes and tenantId
- WHEN a user with scopes
["billing:read"]andtenantId = "acme"is resolved - THEN
Principalreflects those values exactly AND no extra fields leak
Verified by: src/principal-projection/principal-projection.test.ts::"projects scopes and tenantId"
Requirement: Shares host's main DB connection (no separate connection)
The package SHALL accept a store adapter passed in by the host and MUST
NOT open its own DB connection. The store interface is structural and
satisfied by any storage layer the host wires.
Scenario: adapter requires store at construction
- WHEN
createRolledAuthAdapter({})is called withoutstore - THEN it throws
AuthRolledError(DATABASE_REQUIRED)
Verified by: src/options/options.test.ts::"throws DATABASE_REQUIRED when store is missing"
Requirement: Comprehensive error code coverage
The package SHALL throw at least one production path per error code in
AUTH_ROLLED_*. The composition test enumerates the registry and asserts
every code has a production trigger.
Scenario: every error code is triggered by at least one production path
- WHEN the conformance test enumerates
AUTH_ROLLED_* - THEN each code maps to at least one named production scenario
Verified by: src/composition/composition.spec-conformance.test.ts::"every AUTH_ROLLED code is triggered"
Requirement: Contributes OpenAPI security schemes
The package SHALL contribute securitySchemes to api-openapi describing
the cookie + bearer schemes.
Scenario: schemes are picked up by emitter
- WHEN the adapter is inspected
- THEN
securityScheme.schemes.bearerandsecurityScheme.schemes.cookieare present and validate againstOpenApiSecuritySchemeSchema
Verified by: src/security-schemes/security-schemes.test.ts::"contributes cookie and bearer schemes"
Requirement: Rotating-key TokenRotator
The package SHALL expose a TokenRotator that surfaces snapshot(),
decide() (advisory rotation timing), setKeys() (operator-driven swap)
and publicJwkFor(kid) (verifier lookup). The rotator MUST enforce
exactly-one-active-non-revoked on every keyset swap.
Scenario: setKeys with no active key rejected
- WHEN
setKeys([key("k1", active=false)])is called - THEN it throws
AuthRolledError(KEY_ROTATION_FAILED)
Scenario: setKeys with two active keys rejected
- WHEN a replacement keyset contains two active+non-revoked keys
- THEN
setKeysthrowsAuthRolledError(KEY_ROTATION_FAILED)
Scenario: decide reports shouldRotate=true after period elapses
- WHEN
rotationPeriodMs = 1000and 1500 ms elapse on the injected clock - THEN
decide().shouldRotate === trueANDdecide().retiringKidequals the active kid
Verified by: src/token-rotator/token-rotator.test.ts::"setKeys rejects a replacement with zero active keys"
AND src/token-rotator/token-rotator.test.ts::"setKeys rejects a replacement with two active keys"
AND src/token-rotator/token-rotator.test.ts::"decide returns shouldRotate=true once the period elapses"