Files
skills/docs/api-stack/api/api-auth-rolled/src/session/session.md
2026-05-29 08:20:56 +01:00

101 lines
3.9 KiB
Markdown

# `session`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The rolling-JWT session resolver — the keystone of the rolled auth
adapter. Builds a `SessionResolver` (per `@suaveplan/api-auth`) that
parses-and-verifies a JWT extracted from either the `Authorization:
Bearer` header or a configurable cookie, runs the JWT through the
`TokenRotator` to find the verifying JWK by `kid`, projects the matching
user row onto a `Principal`, and returns `null` on every soft failure so
the host's resolver chain can move on to the next adapter.
The same module exposes `issueToken(userId)` so the future
account-lifecycle provider (deferred to `api-auth-rolled-provider`) can
share a single signing path with the resolver. Keeping issue and verify
co-located means the JWT shape and the rotator wiring stay in lock-step.
## Features
- RS256 signing via `@suaveplan/jws` `compactSign` with the rotator's
active JWK; verification via `compactVerify` against the rotator's
retired-or-active JWK matched by `kid`.
- Bearer + cookie extraction in one pass — bearer wins when both are
present so service clients can override a stale cookie.
- Strict JWT-claim validation: `iss`/`aud` exact match, `exp` future,
`iat` not before `users.createdAt`.
- Soft failures (missing token, bad signature, expired, unknown kid)
collapse to `null`; hard failures (storage adapter throws) propagate
as `AuthRolledError`.
- `buildSetCookie` returns a deterministic `Set-Cookie` value with
`HttpOnly`, `Path=/`, `Max-Age`, `SameSite`, `Secure` — used by the
future provider half but published here so the cookie shape lives next
to the cookie parser.
## Usage: Basic
```ts
import { createRolledSessionResolver, TokenRotator } from "@suaveplan/api-auth-rolled";
const rotator = new TokenRotator({ keys, rotationPeriodMs, clock });
const resolver = createRolledSessionResolver({
options,
rotator,
clock,
cookieName: "suaveplan.session",
sessionTtlSec: 3600,
});
const principal = await resolver.resolve(req);
```
## Usage: Advanced
```ts
import { createRolledSessionResolver } from "@suaveplan/api-auth-rolled";
const resolver = createRolledSessionResolver({ /* ... */ });
// Provider half (future) signs new tokens with the same key set as the
// resolver — sharing one path means the rotator's view of the active
// kid is the only source of truth.
const issued = await resolver.issueToken("u_1");
const setCookie = buildSetCookie({
name: "suaveplan.session",
value: issued.token,
maxAgeSec: issued.expiresAt - issued.issuedAt,
sameSite: "lax",
secure: true,
});
```
## API Reference
- `createRolledSessionResolver({ options, rotator, clock, cookieName, sessionTtlSec }) → SessionResolver & { issueToken, verifyToken }`
- `buildSetCookie({ name, value, maxAgeSec, sameSite, secure }) → string`
- `tokenSizeOf(token: string): number` — byte length, used by the
conformance harness to assert tokens stay under the 8 KiB cookie limit.
## Implementation Notes
The resolver intentionally returns `null` on every parse/verify failure
rather than throwing — `SessionResolver` chain semantics treat thrown
errors as operator-actionable infrastructure failures, so soft failures
must collapse to `null` to let the chain advance to the next adapter
(typically `api-auth-anonymous`). The two cases where the resolver does
throw are `AuthRolledError(PRINCIPAL_PROJECTION_FAILED)` raised by the
projection step (storage-driver bug) and whatever error the operator's
`store.findUserById` raises (DB connection lost). The session-resolver
chain handles both by surfacing 500 to the client and routing to the
host's audit log.
The header parse is done manually rather than via `compactDecode` so a
malformed header (e.g. missing `kid`) bypasses the rotator lookup
entirely. The rotator can verify with retired-but-non-revoked keys, so
rotating the active key never invalidates a session mid-lifetime —
operators can rotate without coordinating with active users.