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,73 @@
# @suaveplan/api-tenant-resolver-jwt — tenant-resolver adapter for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` from the per-request context (attached by an earlier `api-auth-*` chain) and projects it into a `TenantContext`. When the configured claim is present the resolver short-circuits the chain; when absent it returns `null` so a header/subdomain resolver can take over. Configurable claim key (default `tenantId`) and strict-mode (`requirePrincipal: true`) that throws `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID` instead of advancing.
Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the
already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*`
chain step) and projects it into a `TenantContext` for the host's per-request
pipeline.
```ts
import { createJwtTenantResolver } from "@suaveplan/api-tenant-resolver-jwt";
const resolver = createJwtTenantResolver();
const ctx = await resolver.resolve(request);
// → { id: "acme", slug: "acme" } | null
```
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry for `@suaveplan/api-tenant-resolver-jwt`. Every public failure path in the resol…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry for `@suaveplan/api-tenant-resolver-jwt`. Every public failure path in the resol…
- [options](./src/options/options.md) — Defines and validates the `JwtTenantResolverOptions` accepted by `createJwtTenantResolver`. The options shape is delibe…
- [options](./src/options/options.md) — Defines and validates the `JwtTenantResolverOptions` accepted by `createJwtTenantResolver`. The options shape is delibe…
- [resolver](./src/resolver/resolver.md) — Implements The tenant resolver factory `createJwtTenantResolver`. The resolver reads the tenant identifier from…
- [resolver](./src/resolver/resolver.md) — Implements The tenant resolver factory `createJwtTenantResolver`. The resolver reads the tenant identifier from…
<!-- end:auto-docs -->
## Why a dedicated package
The tenant-resolver chain (design doc §6, step 4) runs after the session
resolver. When the session is a JWT carrying `tenant_id` / `tid`, the tenant
resolver's job is trivial: read `principal.tenantId`. Collapsing that into a
dedicated adapter — rather than encoding it inside `api-auth-jwt` — keeps the
two contracts orthogonal and lets operators chain `tenant-resolver-jwt` with
`api-tenant-resolver-header` and `api-tenant-resolver-subdomain` (Stage 8)
for the paths a JWT alone doesn't cover.
## Chain interaction (forward reference)
The host runs resolvers left-to-right; the first non-`null` result wins:
```
[ api-tenant-resolver-jwt, api-tenant-resolver-header, api-tenant-resolver-subdomain ]
| | |
reads principal reads X-Tenant-Id reads sub.domain
(typical bearer) (admin / S2S calls) (public site routes)
```
When the JWT carries no tenant claim (or the request is unauthenticated on a
public route), `tenant-resolver-jwt` returns `null` and the next resolver in
the chain runs. With `requirePrincipal: true` the chain stops here and a
401-equivalent is surfaced via `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID`.
## Options
| Option | Default | Effect |
| ------------------ | ------------- | ------------------------------------------------------------------ |
| `id` | `"jwt"` | Resolver id used in chain logs and metric tags. |
| `claimKey` | `"tenantId"` | Reads `Principal.tenantId` directly; any other key reads `claims`. |
| `requirePrincipal` | `false` | Throw instead of returning `null` when claim is absent. |
| `getPrincipal` | reads `req.principal` | Override how the principal is fetched from the request. |
## Error codes
- `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID` — strict-mode rejected.
- `TENANT_RESOLVER_JWT.INVALID_OPTIONS` — construction-time Zod failure.
## Exports
- `.` — top-level barrel re-exporting everything below.
- `./resolver` — `createJwtTenantResolver`, `CreateJwtTenantResolverOptions`, `PrincipalExtractor`.
- `./options` — `JwtTenantResolverOptions`, `parseJwtTenantResolverOptions`.
- `./error-codes` — `JwtTenantResolverError`, `TENANT_RESOLVER_JWT_CODES`, `JwtTenantResolverErrorCode`.

View File

@@ -0,0 +1,85 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error-code registry for `@suaveplan/api-tenant-resolver-jwt`.
Every public failure path in the resolver raises a `JwtTenantResolverError`
carrying one of two namespaced `TENANT_RESOLVER_JWT.*` codes, so downstream
consumers (chain runners, observability sinks, error-mapping middleware) can
branch deterministically on the string value rather than on a fragile
`instanceof` chain or on `error.message` substring matches.
The registry is intentionally small — Stage 4 resolvers do almost no work
beyond reading a claim — but it is exhaustive: any new failure path MUST
land here first and then be wired through `JwtTenantResolverError` so the
public surface stays in lockstep with the spec.
## Features
- Frozen `TENANT_RESOLVER_JWT_CODES` object produced by `defineCodes` so the
literal `"TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID"` is comparable
at type-check time.
- `JwtTenantResolverError` narrows `code` to `JwtTenantResolverErrorCode`,
so a single `catch (e: unknown)` branch can refine straight to a known
union without further casts.
- Carries the `JwtTenantResolverErrorCode` redundantly on
`jwtTenantResolverCode` for ergonomic destructuring in test harnesses
where the generic `code` field is too widely typed.
## Usage: Basic
```ts
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "@suaveplan/api-tenant-resolver-jwt/error-codes";
try {
// …call createJwtTenantResolver(...).resolve(req)
} catch (error: unknown) {
if (error instanceof JwtTenantResolverError) {
if (error.code === TENANT_RESOLVER_JWT_CODES.PRINCIPAL_MISSING_TENANT_ID) {
// strict-mode rejected — return a 401
}
}
}
```
## Usage: Advanced
```ts
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "@suaveplan/api-tenant-resolver-jwt/error-codes";
function classify(error: unknown): "invalid-options" | "missing-principal" | "other" {
if (!(error instanceof JwtTenantResolverError)) return "other";
switch (error.jwtTenantResolverCode) {
case TENANT_RESOLVER_JWT_CODES.INVALID_OPTIONS:
return "invalid-options";
case TENANT_RESOLVER_JWT_CODES.PRINCIPAL_MISSING_TENANT_ID:
return "missing-principal";
}
}
```
## API Reference
- `TENANT_RESOLVER_JWT_CODES` — `Readonly<Record<"PRINCIPAL_MISSING_TENANT_ID" | "INVALID_OPTIONS", string>>`.
- `JwtTenantResolverErrorCode` — string union of the two registry values.
- `JwtTenantResolverError extends SuaveplanError<JwtTenantResolverErrorCode>` —
constructor `(code, message, options?)`; exposes `jwtTenantResolverCode`.
## Implementation Notes
`defineCodes` produces a frozen registry where every value is the literal
`"TENANT_RESOLVER_JWT.<KEY>"`. The class redundantly stores the code on
`jwtTenantResolverCode` to side-step the wider `SuaveplanError<string>`
typing that some toolchains see when the type parameter is erased through
re-export. Codes are append-only — never rename or remove an entry, or
downstream branches break silently across a version bump.

View File

@@ -0,0 +1,81 @@
# `options`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines and validates the `JwtTenantResolverOptions` accepted by
`createJwtTenantResolver`. The options shape is deliberately narrow — Stage 4
adapters do almost no work — but every field is Zod-validated at construction
time so a typo in `claimKey` or a boolean coerced from a string never reaches
the per-request hot path. Construction-time failures throw
`JwtTenantResolverError(INVALID_OPTIONS)` so the host surfaces a stable error
code rather than a raw `ZodError`.
The parser also fills in defaults (`id = "jwt"`, `claimKey = "tenantId"`,
`requirePrincipal = false`), so the resolver implementation can treat its
options as fully-resolved `Required<…>` and avoid scattering `??` defaults
through the request path.
## Features
- `JwtTenantResolverOptions` — three optional fields covering the entire
configuration surface (`id`, `claimKey`, `requirePrincipal`).
- `parseJwtTenantResolverOptions(input)` — strict Zod parser; rejects
unknown keys, empty strings, and non-boolean values.
- Pure: no I/O, no side effects, no module-level state — safe to call from
module-init code.
## Usage: Basic
```ts
import { parseJwtTenantResolverOptions } from "@suaveplan/api-tenant-resolver-jwt/options";
const resolved = parseJwtTenantResolverOptions(undefined);
// → { id: "jwt", claimKey: "tenantId", requirePrincipal: false }
```
## Usage: Advanced
```ts
import {
parseJwtTenantResolverOptions,
type JwtTenantResolverOptions,
} from "@suaveplan/api-tenant-resolver-jwt/options";
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "@suaveplan/api-tenant-resolver-jwt/error-codes";
function safeParse(input: JwtTenantResolverOptions): "ok" | "bad" {
try {
parseJwtTenantResolverOptions(input);
return "ok";
} catch (e) {
if (
e instanceof JwtTenantResolverError &&
e.code === TENANT_RESOLVER_JWT_CODES.INVALID_OPTIONS
) {
return "bad";
}
throw e;
}
}
```
## API Reference
- `interface JwtTenantResolverOptions` — `{ id?: string; claimKey?: string; requirePrincipal?: boolean }`.
- `function parseJwtTenantResolverOptions(input)` — `Required<Pick<JwtTenantResolverOptions, "id" | "claimKey" | "requirePrincipal">>`. Throws `JwtTenantResolverError(INVALID_OPTIONS)`.
## Implementation Notes
The Zod schema is `.strict()` — unknown keys reject at parse time. This is
intentional: a downstream caller passing a misspelt field (e.g.
`claim_key` instead of `claimKey`) would otherwise see the field silently
ignored and the resolver fall back to defaults, masking the configuration
bug behind a hard-to-trace "no tenant resolved" failure mode. The parser
runs once at construction so the per-request hot path operates on
fully-resolved `Required<…>` values without re-validation.

View File

@@ -0,0 +1,95 @@
# `resolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements The tenant resolver factory `createJwtTenantResolver`. The
resolver reads the tenant identifier from an already-resolved `Principal`
attached by an earlier `api-auth-*` chain step and projects it into a
`TenantContext`. When the configured claim is present the resolver returns
that context (the chain short-circuits); when absent it returns `null` so a
subsequent header / subdomain resolver can take over.
The resolver is intentionally trivial — it does no JWT verification of its
own (that is `api-auth-jwt`'s job) and no network I/O. Its value is making
the data-flow explicit: the host wires `auth → tenant-resolver-jwt → next`
and the conformance suite gets to test the empty/missing-tenant edge cases
uniformly without coupling the auth and tenant contracts.
## Features
- `createJwtTenantResolver(opts?)` factory returning a
`TenantResolver` conforming to `@suaveplan/api-types/tenant-resolver`.
- Dependency-injected `getPrincipal` extractor: defaults to reading
`(request as { principal }).principal`, override for host wiring backed
by a `WeakMap` or `AsyncLocalStorage`.
- Configurable `claimKey`: defaults to `"tenantId"` (reads
`Principal.tenantId`); any other key reads `Principal.claims[key]` and
brand-casts the string to `TenantId`.
- Strict-mode `requirePrincipal`: when `true`, a missing principal or a
missing claim throws `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID`
rather than returning `null`.
- Pure read-path: no module state, no async beyond the `Promise<…>` shape
required by the contract.
## Usage: Basic
```ts
import { createJwtTenantResolver } from "@suaveplan/api-tenant-resolver-jwt/resolver";
const resolver = createJwtTenantResolver();
const ctx = await resolver.resolve(request);
if (ctx) {
// → { id: "acme" as TenantId, slug: "acme" }
}
```
## Usage: Advanced
```ts
import type { Principal } from "@suaveplan/api-types/principal";
import { createJwtTenantResolver } from "@suaveplan/api-tenant-resolver-jwt/resolver";
const principals = new WeakMap<Request, Principal | null>();
const resolver = createJwtTenantResolver({
id: "jwt-primary",
claimKey: "tid",
requirePrincipal: true,
getPrincipal: (req) => principals.get(req) ?? null,
});
// host wiring (api-auth-jwt would do this in production):
principals.set(request, { id: "u" as never, scopes: [], claims: { tid: "acme" } });
const ctx = await resolver.resolve(request);
// → { id: "acme" as TenantId, slug: "acme" } — short-circuits the chain
```
## API Reference
- `type PrincipalExtractor = (request: Request) => Principal | null | undefined`.
- `interface CreateJwtTenantResolverOptions extends JwtTenantResolverOptions { getPrincipal?: PrincipalExtractor }`.
- `function createJwtTenantResolver(options?): TenantResolver` — returns
`{ id, resolve(request) }`. `resolve` returns
`Promise<TenantContext | null>`; throws
`JwtTenantResolverError(PRINCIPAL_MISSING_TENANT_ID)` only when
`requirePrincipal: true` and the principal / claim is absent.
## Implementation Notes
The resolver brand-casts the claim value to `TenantId` without further
validation: the principal has already passed an `api-auth-*` adapter that
owns the verification of the JWT signature and the trust boundary. Re-validating
here would duplicate work the host already performed. Empty-string claim values
are treated as absent so a misconfigured signer can never accidentally route
all traffic into a tenant whose slug is the empty string.
The default `PrincipalExtractor` reads `request.principal`, which works for
the simple host wiring (mutate the request once after auth resolves) but
falls down for environments where the `Request` is frozen (Workers, certain
edge runtimes). In those cases pass an explicit `getPrincipal` backed by a
`WeakMap<Request, Principal>` populated by the auth middleware.