Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# `resolver` — `createJwtSessionResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stage-4 concrete `SessionResolver` factory for the API plugin
foundation. Reads a JWT from the request (Authorization header or
configurable cookie), verifies it via `@suaveplan/jws` against either a
JWKS endpoint (cached and refreshed on `kid` miss) or a single static
JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy,
and projects the validated claims onto a `Principal` via an overridable
mapper. The factory is the only public entry point — internals
(`JwksCache`, claims mapper, error registry) are surface-exported for
testability but most consumers use `createJwtSessionResolver(opts)` alone.
## Features
- `Authorization: Bearer <jwt>` header extraction with case-insensitive
scheme match; configurable header name accepts a raw token form for
non-standard headers (e.g. `x-api-token: <jwt>`).
- Cookie fallback when `cookieName` is supplied; header still wins when
both are present.
- JWKS resolution via `JwksCache` with positive TTL, negative cache,
in-flight dedup, and `kid`-miss refresh.
- Static-key path for HS-family symmetric tokens or single-key
deployments.
- Mandatory `iss` and `aud` enforcement (single string or list); `exp`
and `nbf` enforced when present with configurable `clockToleranceSec`.
- Per-request soft failures collapse to `null` so the upstream
`SessionResolverChain` can fall through to the next adapter; operator
failures (JWKS unreachable, malformed mapper, malformed static key)
surface as `AuthJwtError` with the documented codes.
- Idempotent `dispose()` flips an internal flag so subsequent
`resolve()` calls return `null` rather than emitting late traffic on
a torn-down resolver.
## Usage: Basic
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
const req = new Request("https://api.example/users", {
headers: { authorization: "Bearer eyJ..." },
});
const principal = await resolver.resolve(req);
```
## Usage: Advanced
Static HS256 key, cookie fallback, custom claims mapper, and an
injectable clock for deterministic temporal-claim tests:
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
staticKey: { kty: "oct", k: "<base64url-secret>" },
issuer: ["https://idp1.example", "https://idp2.example"],
audience: "https://api.example",
algorithms: ["HS256"],
cookieName: "sp_session",
clockToleranceSec: 5,
clock: { nowMs: () => 1_700_000_000_000 },
claimsToPrincipal: (claims) => {
const sub = claims["sub"];
if (typeof sub !== "string") return null;
return {
id: sub as never,
scopes: [] as never[],
tenantId: typeof claims["org_id"] === "string"
? (claims["org_id"] as never)
: undefined,
claims,
};
},
});
```
## API Reference
- `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver`
— returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` +
idempotent `dispose`).
## Implementation Notes
The resolver deliberately separates "this is not my caller" (chain
falls through, returns `null`) from "the host is mis-configured"
(throws `AuthJwtError`). Verification failures inside `@suaveplan/jws`
return `null` so a stray Bearer header from an unrelated IdP cannot
401-storm the API — the chain's next adapter (anonymous, static key,
etc.) still gets its turn. The temporal-claim check uses a single
clock reading per request to avoid TOCTOU between the `exp` and `nbf`
arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather
than push it into `@suaveplan/jws` because those are JWT-claim
concerns, not JWS-protected-header concerns — the JWS package
deliberately handles only RFC 7515 (signature) and leaves RFC 7519
(payload claims) to its consumers.