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,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.
|
||||
Reference in New Issue
Block a user