4.1 KiB
resolver — SessionResolver contract
(no submodules)
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 narrowPrincipalvia interface merging without losing assignability to the chain'sPrincipalreturn type. - Async by construction; resolvers MAY fetch JWKs, hit Redis, or call a token introspection endpoint.
disposeis part of the contract, not a side concern — the host blue/green rotates resolver chains and relies ondisposeto 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):
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:
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
Requestis the WHATWG FetchRequest(DOM lib). Node ≥18, Bun, Deno, and every Workers runtime all expose it as a global;api-authdoes not import any Node-specific request type so the contract is universal.- Returning
nullfor "credential present but invalid" is acceptable but discouraged — emitAUTH.PRINCIPAL_INVALID_SHAPEinstead, so the host can distinguish "no credential" from "bad credential" in metrics. disposeSHOULD be idempotent; tests in the conformance harness call it twice in a row and assert no throw.