Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user