first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# `resolver` — `SessionResolver` contract
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SessionResolver` is the abstract per-request session-resolver shape that every
concrete adapter in Stages 4/6/8 (`api-auth-jwt`, `api-auth-anonymous`,
`api-auth-bearer-static`, `api-auth-better-auth`, `api-auth-rolled`) implements.
It deliberately captures only the universal vocabulary — inspect a WHATWG
`Request`, return a `Principal | null`, dispose — so any concrete resolver can
plug behind the host's chain runner without further coordination.
The tri-valued return contract (principal / null / throw) collapses to two
host-level outcomes: the chain short-circuits on the first principal, the host
applies its `publicRoutes` decision when every resolver returned `null`, and
`AUTH.RESOLVER_THREW` is surfaced when any resolver throws (fail-closed by
default per design doc §6).
## Features
- Generic over `P extends Principal` — concrete adapters MAY narrow `Principal`
via interface merging without losing assignability to the chain's `Principal`
return type.
- Async by construction; resolvers MAY fetch JWKs, hit Redis, or call a token
introspection endpoint.
- `dispose` is part of the contract, not a side concern — the host blue/green
rotates resolver chains and relies on `dispose` to release JWK caches, close
HTTP keepalive pools, etc.
## Usage: Basic
A minimal resolver that always returns null (the "anonymous fallback" pattern
codified in `api-auth-anonymous`):
```ts
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
const anonymous: SessionResolver = {
async resolve() {
return null;
},
async dispose() {
/* no resources to release */
},
};
```
## Usage: Advanced
A resolver that reads a `Bearer` token, parses claims, and returns a typed
principal — with `dispose` releasing a token-cache eviction timer:
```ts
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
import type { Principal, PrincipalId, Scope } from "@suaveplan/api-types/principal";
interface JwtClaims {
sub: string;
scope?: string;
[key: string]: unknown;
}
class JwtResolver implements SessionResolver {
private readonly evictTimer: ReturnType<typeof setInterval>;
private disposed = false;
public constructor(private readonly verifyJwt: (raw: string) => Promise<JwtClaims | null>) {
this.evictTimer = setInterval(() => {
/* prune cache */
}, 30_000);
}
public async resolve(req: Request): Promise<Principal | null> {
const header = req.headers.get("authorization");
if (!header?.startsWith("Bearer ")) {
return null;
}
const claims = await this.verifyJwt(header.slice("Bearer ".length));
if (claims === null) {
return null;
}
const scopes = (claims.scope ?? "").split(" ").filter(Boolean) as readonly Scope[];
return {
id: claims.sub as PrincipalId,
scopes,
claims,
};
}
public async dispose(): Promise<void> {
if (this.disposed) {
return;
}
this.disposed = true;
clearInterval(this.evictTimer);
}
}
```
## API Reference
| Symbol | Kind | Description |
|-------------------|-------------|------------------------------------------------------|
| `SessionResolver` | interface | `resolve(req): Promise<P \| null>` + `dispose()`. |
| `P` (generic) | type param | Defaults to `Principal`. Narrowing is opt-in. |
## Implementation Notes
- `Request` is the WHATWG Fetch `Request` (DOM lib). Node ≥18, Bun, Deno, and
every Workers runtime all expose it as a global; `api-auth` does not import
any Node-specific request type so the contract is universal.
- Returning `null` for "credential present but invalid" is acceptable but
discouraged — emit `AUTH.PRINCIPAL_INVALID_SHAPE` instead, so the host can
distinguish "no credential" from "bad credential" in metrics.
- `dispose` SHOULD be idempotent; tests in the conformance harness call it
twice in a row and assert no throw.