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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
# `chain` — `SessionResolverChain`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Ordered, first-match-wins runtime over an array of `SessionResolver`s. The
chain is the integration point at design-doc §6 step 3: the host's per-request
pipeline calls `chain.resolve(req)` exactly once and routes the result into the
`RequestContext.principal` slot (`publicRoutes` decides 401 vs `null`).
The chain owns three orthogonal contracts that are easy to get wrong if every
adapter re-implements them: (1) iteration order — first non-null wins, no
later resolver is invoked; (2) fail-closed throw semantics — a resolver
exception surfaces `AUTH.RESOLVER_THREW` and short-circuits the chain, with an
opt-in `continueOnThrow` escape hatch documented for SaaS-provider flakiness;
(3) empty-chain rejection — `new SessionResolverChain([])` throws
`AUTH.CHAIN_EMPTY` at construction so deployment misconfiguration cannot mask
itself as "no resolvers, every request 401s."
## Features
- Class form implements `SessionResolver` itself — the chain can be nested
inside another chain, used as the host's only resolver, or composed with
middleware identically.
- Idempotent `dispose()` propagates to every wrapped resolver.
- Cause-chain propagation via `AuthError`'s `cause` option; the index of the
throwing resolver is recorded in `context.resolverIndex`.
## Usage: Basic
```ts
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
declare const jwtResolver: SessionResolver;
declare const bearerStaticResolver: SessionResolver;
const chain = new SessionResolverChain([jwtResolver, bearerStaticResolver]);
const principal = await chain.resolve(new Request("https://example.test"));
```
## Usage: Advanced
Enable `continueOnThrow` for a deployment that fronts an intentionally flaky
SaaS auth upstream — the chain logs the cause via the `AuthError` it would
have thrown, but keeps moving to the static-bearer fallback resolver:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
declare const saasJwtResolver: SessionResolver;
declare const staticBearerResolver: SessionResolver;
const chain = new SessionResolverChain(
[saasJwtResolver, staticBearerResolver],
{ continueOnThrow: true },
);
const req = new Request("https://example.test/api/orders", {
headers: { authorization: "Bearer st_ops_token" },
});
const principal = await chain.resolve(req); // may come from staticBearerResolver
```
## API Reference
| Symbol | Kind | Description |
|---------------------------------|--------|------------------------------------------------------------|
| `SessionResolverChain` | class | `new(resolvers, options?)`; implements `SessionResolver`. |
| `SessionResolverChainOptions` | type | `{ continueOnThrow?: boolean }`. |
## Implementation Notes
- The chain iterates synchronously over `this.resolvers` and awaits each
resolver in turn — concurrent resolution is intentionally NOT offered.
Concurrency would change the "first match wins" semantics into "first to
return wins," producing a non-deterministic principal selection when two
resolvers both recognise the request.
- The throw-handling branch records the resolver index but NOT the resolver
identity — concrete adapters MAY redact stack traces, and the chain has no
inspection contract on its members beyond `resolve` + `dispose`.
- `dispose()` is idempotent via a `disposed` flag; the conformance harness
calls it twice and asserts no throw.