first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# @suaveplan/api-auth-static-principal — fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.
Fixed-`Principal` session resolver for the `@suaveplan/api` plugin
system. Returns the configured `Principal` verbatim on every request —
no token decoding, no network I/O, no internal state. Used in plugin
integration tests and the `api-plugin-example` fixture, where the goal is to
exercise the entire downstream pipeline (RBAC gate, tenant resolver chain
reading `principal.tenantId`, tenant-scoped DB) with a deterministic
identity carried in config.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
- [factory](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
- [create-static-principal-resolver](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
<!-- end:auto-docs -->
## Why does this exist?
Per the API foundation design doc §13.2, the testing kit needs an auth
adapter that supplies a fixed `Principal` without any token decoding. The
§13.2 example uses it directly:
```ts
adapters: {
"auth.session": {
kind: "static-principal",
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
},
}
```
Tests exercise the whole pipeline — RBAC, tenant resolution, tenant-scoped
DB — with a deterministic principal in config. No mocks, no JWKS setup, no
token signing.
## Surface
| Subpath | Export |
| -------------- | --------------------------------------------------------------------------------------- |
| `./resolver` | `createStaticPrincipalResolver(opts): SessionResolver` |
| `./adapter` | `createStaticPrincipalAdapter(opts): AuthAdapter` |
| `./error-codes`| `AUTH_STATIC_PRINCIPAL_CODES`, `AuthStaticPrincipalError`, `AuthStaticPrincipalErrorCode` |
The root barrel re-exports the resolver, adapter, error codes, options
schema (`StaticPrincipalResolverOptionsSchema` /
`parseStaticPrincipalResolverOptions`), and the empty
`staticPrincipalSecurityScheme()` contribution.
## Usage
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
host.registerAuth(adapter);
// every request now produces `principal === { id: "u1", ... }`.
```
Skip auth on selected paths to test `publicRoutes` behaviour without
rewiring the adapter chain:
```ts
const adapter = createStaticPrincipalAdapter({
principal: { id: "u1", scopes: [], claims: {} },
nullForPaths: ["/healthz", "/webhooks/*"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
```
## Conformance
The resolver passes `runAuthSessionConformance` from
`@suaveplan/api-auth/testing` in seeded-principal mode. See
`src/factory/create-static-principal-resolver.spec-conformance.test.ts` and
`tests/conformance/api-auth.test.ts`.
## Errors
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — outer options bag failed Zod
validation (wrong shape, missing required fields, unknown top-level keys).
`AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` field
is malformed (missing `id`, non-string `id`, non-array `scopes`, unknown
keys inside `principal`, …).
Both fire synchronously at construction time. The resolver itself never
throws at request time.
## Stage-5 wiring (forward-looking)
When `api-host/testing` ships its Stage-5 `createTestHost` surface, this
adapter is registered in the adapter table under
`kind: "static-principal"`, so tests will declare it inline:
```ts
const host = createTestHost({
adapters: {
"auth.session": {
kind: "static-principal",
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
},
},
});
```
Until then, callers wire the adapter directly via
`createStaticPrincipalAdapter()` as shown above.

View File

@@ -0,0 +1,81 @@
# `adapter` — composite `AuthAdapter` factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Bundles the static-principal resolver, the (absent) security-scheme
contribution, and a pinned `csrf: false` into a single `AuthAdapter` value.
This is the call-site shape for the canonical "test host with a known
Principal" wiring — `host.registerAuth(createStaticPrincipalAdapter({...}))`
gives the test the entire auth pipeline without further plumbing.
The factory delegates construction-time validation to
`createStaticPrincipalResolver`, so misconfigured options surface as the
same typed `AuthStaticPrincipalError` they do at the resolver level. The
adapter holds no state of its own.
## Features
- Single-call construction of the resolver + scheme + csrf triple.
- Pins `csrf: false` because the static-principal layer has no session
cookie to forge against — every request carries the configured Principal
by construction.
- `securityScheme` omitted (matching `api-auth-anonymous` topology) so the
generated OpenAPI document doesn't advertise a real auth flow for
routes wired to this adapter.
## Usage: Basic
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
host.registerAuth(adapter);
```
## Usage: Advanced
Compose with `applyTo` / `nullForPaths` to skip auth on test-only routes:
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: { id: "u1", scopes: [], claims: {} },
nullForPaths: ["/healthz", "/webhooks/*"],
});
// host.registerAuth(adapter) — /healthz and /webhooks/* resolve to null;
// every other route gets the configured Principal.
```
## API Reference
| Symbol | Kind | Description |
| ------------------------------------- | -------- | ---------------------------------------------------------- |
| `createStaticPrincipalAdapter(opts)` | function | Returns `AuthAdapter` composed from the four building blocks. |
## Implementation Notes
`csrf: false` is the load-bearing pin: a static-principal host can't honour
a real CSRF check (there's no session cookie carrying a synchronizer
token), so the host should skip CSRF entirely for routes wired to this
adapter. If a downstream test needs CSRF behaviour exercised in isolation,
compose with a separate adapter that pins `csrf: true` and let the host's
adapter-chain merge rule pick the safer value (the host's policy is to use
the strictest `csrf` flag in the chain).
`validateAuthAdapter` accepts this adapter's shape (security scheme absent;
csrf set to a boolean). The package's adapter test asserts the validation
contract is satisfied without re-implementing the validator.

View File

@@ -0,0 +1,87 @@
# `error-codes` — stable code registry for the static-principal adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the
`AuthStaticPrincipalError` concrete subclass of
`@suaveplan/error/SuaveplanError`. This module is the single source of truth
for every failure code the package can surface to a caller — wiring this
registry into the host's error envelope is how a malformed test-time
Principal configuration reaches the developer rather than silently producing a
broken auth chain.
Codes are produced via `defineCodes("AUTH_STATIC_PRINCIPAL", [...])` so the
full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`. The
class augments the base error with `authStaticPrincipalCode`, providing a
narrowed code property when consumers have already discriminated by
`instanceof`.
## Features
- Stable, frozen `AUTH_STATIC_PRINCIPAL_CODES` map built via `defineCodes` so
the prefix can't drift.
- Discriminated-union friendly: `AuthStaticPrincipalErrorCode` is a literal
union over the registry entries.
- Carries arbitrary `context` (Zod issues, the offending field path) via the
base `SuaveplanError` options bag.
## Usage: Basic
```ts
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "@suaveplan/api-auth-static-principal/error-codes";
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
"createStaticPrincipalResolver: options failed schema validation"
);
```
## Usage: Advanced
Attach Zod issues so the caller can pinpoint the bad field without re-running
validation:
```ts
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "@suaveplan/api-auth-static-principal/error-codes";
import z from "zod";
const parsed = OptionsSchema.safeParse(input);
if (!parsed.success) {
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
"principal failed schema validation",
{ context: { issues: parsed.error.issues } }
);
}
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------------------------------ |
| `AUTH_STATIC_PRINCIPAL_CODES` | constant | Frozen map of code constants. Keys are the short names; values are full. |
| `AuthStaticPrincipalErrorCode` | type | Literal union over the registry values. |
| `AuthStaticPrincipalError` | class | `SuaveplanError` subclass typed against the code registry. |
## Implementation Notes
`defineCodes` joins the prefix with each suffix using a single `.` so the
resulting strings match the host's structured-log code field. The class
stores the code twice — once in the inherited `code` slot and once in the
narrower `authStaticPrincipalCode` slot — so callers that already narrowed by
`instanceof` get a precisely-typed code field without a follow-up `as`.
The package's static-principal resolver never throws at request time; both
codes are construction-time only. This is deliberate — every per-request
failure for fixed-principal tests should manifest in the test assertion, not
in the auth pipeline.

View File

@@ -0,0 +1,99 @@
# `factory/create-static-principal-resolver` — fixed-Principal resolver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the
configured `Principal` verbatim on every request — no token decoding, no
JWKS, no I/O. This is the workhorse adapter for plugin integration tests,
the `api-plugin-example` fixture, and any environment where the goal is to
exercise the downstream pipeline (RBAC gate, tenant resolver chain reading
`principal.tenantId`, tenant-scoped DB) with a deterministic identity carried
in config.
The factory consumes options previously validated by the `options` module so
construction is total: every misuse maps to either
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` or `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
and surfaces synchronously at construction time. The resolver itself never
throws at request time.
## Features
- Defensive clone on every `resolve()` — callers cannot mutate one returned
Principal and observe the change on subsequent calls.
- `applyTo(req)` predicate for arbitrary "return null for this request"
rules.
- `nullForPaths` suffix-glob list (`/webhooks/*`) and exact-match patterns
for the most common public-route test setup.
- Idempotent no-op `dispose()` matching the `SessionResolver` contract.
- Returned objects are frozen (top-level + `scopes` + `claims`) to make
attempted mutations fail loudly in strict mode.
## Usage: Basic
```ts
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
const resolver = createStaticPrincipalResolver({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
const principal = await resolver.resolve(
new Request("https://example.test/api/billing/invoices")
);
// principal.id === "u1"
```
## Usage: Advanced
Combine `applyTo` and `nullForPaths` to model a mixed-route test surface:
```ts
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
const resolver = createStaticPrincipalResolver({
principal: {
id: "u1",
scopes: ["billing:write", "billing:read"],
tenantId: "acme",
claims: { sub: "u1" },
},
nullForPaths: ["/webhooks/*", "/healthz"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
// /healthz and /webhooks/stripe resolve to null.
// Requests with the `x-skip-auth` header resolve to null.
// Everything else returns the configured Principal.
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------------------------ |
| `createStaticPrincipalResolver(o)` | function | Returns a `SessionResolver` carrying `o.principal` on every match. |
## Implementation Notes
The clone strategy is: deep-freeze a single template at construction, then
clone-from-template per `resolve()`. The per-call clone is unavoidable —
otherwise two callers each freezing the returned object would freeze each
other's state. The per-call clone copies the top level plus `scopes` (array
spread) and `claims` (object spread); deeper structures inside `claims` are
not cloned. This matches the `Principal` contract: `claims` values are
`unknown`, so the package can't responsibly deep-clone them. Callers passing
mutable nested claims values are responsible for not mutating them in-place.
`pathMatches` accepts only exact-match and a single suffix glob (`/foo/*`)
on purpose — richer matching belongs in `applyTo`. The trailing slash is
preserved when stripping the `*` so `/foo/*` matches `/foo/x` but not
`/foox`. The pattern dialect is documented on the spec and exercised by the
test file.

View File

@@ -0,0 +1,82 @@
# `options` — Zod-validated options for `createStaticPrincipalResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the runtime contract between the test harness call site and the
resolver factory. The schema validates both the outer options bag
(`{ principal, nullForPaths?, applyTo? }`) and the nested `Principal` shape
(non-empty `id`, readonly `scopes`, optional `tenantId`, default-`{}`
`claims`). On failure it raises a typed `AuthStaticPrincipalError` carrying
the originating Zod issues — splitting between `INVALID_OPTIONS` (anything
outside `principal`) and `INVALID_PRINCIPAL` (anything inside it).
This module is the only place the package touches Zod at runtime. Keeping it
isolated lets the resolver factory stay a thin three-line function and lets
the conformance test target the resolver behaviour without invoking schema
side-effects.
## Features
- Strict outer schema — unknown keys at the top level reject with
`INVALID_OPTIONS`.
- Discriminated error codes: `principal` issues map to `INVALID_PRINCIPAL`;
everything else maps to `INVALID_OPTIONS`.
- Pre-flight type check rejects non-object inputs with `INVALID_OPTIONS`
before Zod sees them — a cleaner message than the Zod default for `null` /
arrays.
- `claims` defaults to `{}` so callers don't have to write
`claims: {}` for the common case.
## Usage: Basic
```ts
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
const opts = parseStaticPrincipalResolverOptions({
principal: {
id: "u_1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { email: "user@acme.test" },
},
});
```
## Usage: Advanced
Validate options that include both `applyTo` and `nullForPaths`:
```ts
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
const opts = parseStaticPrincipalResolverOptions({
principal: { id: "u_1", scopes: [], claims: {} },
nullForPaths: ["/webhooks/*", "/healthz"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
```
## API Reference
| Symbol | Kind | Description |
| -------------------------------------------- | -------- | ----------------------------------------------------------------- |
| `StaticPrincipalResolverOptionsSchema` | Zod | Strict object schema for the outer bag. |
| `parseStaticPrincipalResolverOptions(raw)` | function | Returns the validated options or throws `AuthStaticPrincipalError`. |
## Implementation Notes
The pre-flight `typeof`/`Array.isArray` check lets the function return a
single canonical error for "you handed me something that isn't an object" —
Zod's own behaviour on `null` is to emit
`{ code: "invalid_type", expected: "object" }`, which is correct but verbose.
Splitting into a pre-flight + Zod pass is one extra branch but keeps the
caller-facing failure message stable.
`claims` is recorded as `z.record(z.unknown())` so any free-form JWT/claims
payload validates. Coercing to readonly happens at the TypeScript boundary
(`StaticPrincipalResolverOptions`), not in Zod — Zod's `.readonly()` is
type-level only and would re-wrap arrays at runtime unnecessarily.

View File

@@ -0,0 +1,75 @@
# `security-scheme` — OpenAPI contribution for the static-principal adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the per-adapter `SecuritySchemeContribution` slot of
`AuthAdapter`. The static-principal adapter is a test-only construct: there
is no real authentication flow to document on routes that use it. This
module returns `undefined` deliberately — which `validateAuthAdapter` from
`@suaveplan/api-auth` permits — so the generated OpenAPI document emits no
security entry for routes under this adapter, mirroring how the anonymous
tail adapter is treated.
Wrapping the absence in a function gives the package a symmetric surface
with adapters that compute their contribution dynamically, and gives the
test suite a callable to target for the "intentionally absent" assertion.
## Features
- Returns `undefined` so the host omits a `security` entry on routes wired
to this adapter.
- Permitted by `validateAuthAdapter` (the anonymous-style topology).
- Callable surface for symmetry with adapters that compute their scheme.
## Usage: Basic
```ts
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
import type { AuthAdapter } from "@suaveplan/api-auth";
const adapter: AuthAdapter = {
resolver,
securityScheme: staticPrincipalSecurityScheme(),
csrf: false,
};
```
## Usage: Advanced
When composing static-principal with another adapter that supplies a real
scheme, drop this contribution and use the other adapter's scheme in the
composite — the host requires only one to be present per route. The
function still acts as documentation that the static-principal layer
contributes nothing of its own:
```ts
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
if (staticPrincipalSecurityScheme() !== undefined) {
// unreachable in this version — the function is documented to return
// undefined. Tests assert this invariant.
}
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------ |
| `staticPrincipalSecurityScheme()` | function | Returns `undefined` — no OpenAPI contribution. |
## Implementation Notes
`validateAuthAdapter` treats `securityScheme: undefined` as a permitted
topology (the "no auth configured" path used by `api-auth-anonymous`). The
generated OpenAPI document omits a `security` array on routes wired to an
adapter that omits the scheme; combined with the host's `publicRoutes`
decision, this matches the runtime behaviour of the static-principal
adapter in tests.
Changing this to return a real scheme in a future version would be a
breaking change for downstream consumers' OpenAPI snapshots; treat the
"returns undefined" behaviour as a load-bearing contract.