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

4.2 KiB

@suaveplan/api-auth-rolled — rolled session-token resolver (Stage 8.12 keystone)

Package: @suaveplan/api-auth-rolled Status: PROPOSED Type: new-package Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/

Why

Per design doc §2.3, genesis ships two reference auth providers in v1: Better-Auth (third-party wrapper) AND api-auth-rolled (composes the genesis crypto/auth primitives end-to-end). Shipping the rolled session-resolver in Stage 8 is the keystone that proves rotating-key JWTs compose against the existing primitives without a third-party dependency, and gives operators a fallback session-auth path that owns zero third-party runtime dependencies.

This change scopes Stage 8.12 to the session-resolver half of the dual contract. The matching AuthProviderAdapter (account-lifecycle routes, password gating, MFA, OAuth, etc.) is deferred to a successor change api-auth-rolled-provider so this milestone ships at 100% coverage in a single iteration. Sharing the host's main DB connection, the primitive composition, and the JWT key-rotation contract are all established here — the successor change layers routes onto the same adapter shape.

What changes

  • ADDED: createRolledAuthAdapter({ store, keys, issuer, audience, ... }) returns AuthAdapter (session resolver + security-scheme contribution).
  • ADDED: TokenRotator — manages a rotating set of RS256 signing keys. Active key signs new JWTs; retired-but-non-revoked keys still verify in-flight tokens; revoked keys neither. decide() is the advisory rotation timing signal; setKeys() is the operator-driven swap.
  • ADDED: Session tokens are signed JWTs (RS256) via @suaveplan/jws + @suaveplan/crypto-rsa JWKs; key rotation supported by JWKS-style key id. The resolver accepts both Authorization: Bearer <token> and Cookie: suaveplan.session=<token> (configurable name).
  • ADDED: RolledAuthStore — the structural storage interface the adapter consumes. Concrete DB-backed implementations are out of scope for this change (operator wires them).
  • ADDED: projectUserRowToPrincipal — maps internal UserRow shape to the @suaveplan/api-types Principal contract.
  • ADDED: securitySchemes contribution for api-openapi — cookie + bearer schemes both contributed.
  • ADDED: Error codes (AUTH_ROLLED_*): exhaustive 20-code registry covering account-lifecycle, session, and infrastructure failure modes.

Out of scope (deferred to api-auth-rolled-provider)

  • /auth/register, /auth/login, /auth/logout, /auth/refresh route implementations.
  • Password hashing + strength + breach gating at registration time.
  • MFA (TOTP, WebAuthn), SRP, OAuth flows.
  • Email-verification / password-reset token issuance.
  • Migrations for users, user_sessions, etc.

The session resolver shipped here verifies tokens issued by any provider that signs to the same JWK set, so the dual contract is split clean — operators MAY wire api-auth-rolled for session reads against a separate provider (e.g. Clerk) that issues the JWTs.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
Implements auth.session §2.3
Shares the host's main DB connection §2.3
Composes crypto-rsa + jws primitives §2.3
Rotating-kid JWT signing §2.3

Verification approach

  • src/session/session.test.ts — issue, verify (active kid, retired kid, revoked kid), expired token rejected, cookie + bearer extraction.
  • src/token-rotator/token-rotator.test.ts — decide/setKeys/ snapshot/publicJwkFor paths.
  • src/principal-projection/principal-projection.test.ts — projection invariants.
  • src/options/options.test.ts — Zod schema + DATABASE_REQUIRED + key-set invariants.
  • src/security-schemes/security-schemes.test.ts — cookie + bearer contributions validate.
  • src/composition/composition.spec-conformance.test.ts — every AUTH_ROLLED_* code is thrown by at least one production path.

Migration impact

None — additive only. api-host registers this when host.auth.session = "rolled".