# `@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 ` and `Cookie: suaveplan.session=` (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"`.