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,868 @@
# `packages`
Suaveplan package tree. Every `@suaveplan/*` library lives under a category (`packages/<category>/`) and a subcategory (`packages/<category>/<subcategory>/`).
<!-- begin:auto-docs -->
## Categories
18 categories, 472 packages.
### `api-stack`
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.
#### `api`
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
| Package | Description |
|---|---|
| [`@suaveplan/api`](./api-stack/api/api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
| [`@suaveplan/api-2pc`](./api-stack/api/api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
| [`@suaveplan/api-audit-file`](./api-stack/api/api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
| [`@suaveplan/api-audit-stdout`](./api-stack/api/api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
| [`@suaveplan/api-auth`](./api-stack/api/api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
| [`@suaveplan/api-auth-anonymous`](./api-stack/api/api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
| [`@suaveplan/api-auth-bearer-static`](./api-stack/api/api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
| [`@suaveplan/api-auth-better-auth`](./api-stack/api/api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
| [`@suaveplan/api-auth-jwt`](./api-stack/api/api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
| [`@suaveplan/api-auth-provider`](./api-stack/api/api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
| [`@suaveplan/api-auth-rolled`](./api-stack/api/api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
| [`@suaveplan/api-auth-static-principal`](./api-stack/api/api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
| [`@suaveplan/api-db-adapter`](./api-stack/api/api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
| [`@suaveplan/api-db-sqlite-memory`](./api-stack/api/api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
| [`@suaveplan/api-db-worker-proxy`](./api-stack/api/api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
| [`@suaveplan/api-events-in-memory`](./api-stack/api/api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
| [`@suaveplan/api-events-worker-proxy`](./api-stack/api/api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
| [`@suaveplan/api-graphql`](./api-stack/api/api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
| [`@suaveplan/api-graphql-apollo`](./api-stack/api/api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
| [`@suaveplan/api-graphql-ui-altair`](./api-stack/api/api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
| [`@suaveplan/api-graphql-ui-graphiql`](./api-stack/api/api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
| [`@suaveplan/api-graphql-yoga`](./api-stack/api/api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
| [`@suaveplan/api-host`](./api-stack/api/api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
| [`@suaveplan/api-jobs-worker-proxy`](./api-stack/api/api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
| [`@suaveplan/api-lifecycle`](./api-stack/api/api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
| [`@suaveplan/api-loader`](./api-stack/api/api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-logger-worker-proxy`](./api-stack/api/api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
| [`@suaveplan/api-manifest`](./api-stack/api/api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
| [`@suaveplan/api-migrations`](./api-stack/api/api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
| [`@suaveplan/api-openapi`](./api-stack/api/api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
| [`@suaveplan/api-openapi-ui-redoc`](./api-stack/api/api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
| [`@suaveplan/api-openapi-ui-scalar`](./api-stack/api/api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
| [`@suaveplan/api-openapi-ui-swagger`](./api-stack/api/api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
| [`@suaveplan/api-plugin`](./api-stack/api/api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
| [`@suaveplan/api-plugin-audit`](./api-stack/api/api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
| [`@suaveplan/api-plugin-auth`](./api-stack/api/api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
| [`@suaveplan/api-plugin-campaigns`](./api-stack/api/api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
| [`@suaveplan/api-plugin-cms`](./api-stack/api/api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
| [`@suaveplan/api-plugin-database`](./api-stack/api/api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
| [`@suaveplan/api-plugin-event-bus`](./api-stack/api/api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
| [`@suaveplan/api-plugin-events`](./api-stack/api/api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
| [`@suaveplan/api-plugin-example`](./api-stack/api/api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
| [`@suaveplan/api-plugin-export`](./api-stack/api/api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
| [`@suaveplan/api-plugin-file-upload`](./api-stack/api/api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
| [`@suaveplan/api-plugin-graphql`](./api-stack/api/api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
| [`@suaveplan/api-plugin-import`](./api-stack/api/api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
| [`@suaveplan/api-plugin-notifications`](./api-stack/api/api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
| [`@suaveplan/api-plugin-payment`](./api-stack/api/api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
| [`@suaveplan/api-plugin-rate-limit`](./api-stack/api/api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
| [`@suaveplan/api-plugin-search`](./api-stack/api/api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
| [`@suaveplan/api-plugin-site`](./api-stack/api/api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
| [`@suaveplan/api-plugin-sse`](./api-stack/api/api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
| [`@suaveplan/api-plugin-tenant`](./api-stack/api/api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
| [`@suaveplan/api-plugin-user`](./api-stack/api/api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
| [`@suaveplan/api-plugin-vault`](./api-stack/api/api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
| [`@suaveplan/api-plugin-webhook`](./api-stack/api/api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
| [`@suaveplan/api-rate-limit`](./api-stack/api/api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
| [`@suaveplan/api-rbac`](./api-stack/api/api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
| [`@suaveplan/api-router`](./api-stack/api/api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-router-hono`](./api-stack/api/api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
| [`@suaveplan/api-router-in-memory`](./api-stack/api/api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
| [`@suaveplan/api-router-worker-bridge`](./api-stack/api/api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
| [`@suaveplan/api-saga`](./api-stack/api/api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
| [`@suaveplan/api-sandbox-permissions`](./api-stack/api/api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
| [`@suaveplan/api-sandbox-vm`](./api-stack/api/api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-stack/api/api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
| [`@suaveplan/api-sandbox-wasm`](./api-stack/api/api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-stack/api/api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
| [`@suaveplan/api-schemas`](./api-stack/api/api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
| [`@suaveplan/api-secrets`](./api-stack/api/api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
| [`@suaveplan/api-secrets-bun`](./api-stack/api/api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
| [`@suaveplan/api-secrets-env`](./api-stack/api/api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
| [`@suaveplan/api-secrets-vault`](./api-stack/api/api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
| [`@suaveplan/api-secrets-worker-proxy`](./api-stack/api/api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
| [`@suaveplan/api-sse`](./api-stack/api/api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
| [`@suaveplan/api-sse-bun`](./api-stack/api/api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
| [`@suaveplan/api-tenant`](./api-stack/api/api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
| [`@suaveplan/api-tenant-provider`](./api-stack/api/api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
| [`@suaveplan/api-tenant-provider-memory`](./api-stack/api/api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
| [`@suaveplan/api-tenant-resolver-header`](./api-stack/api/api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
| [`@suaveplan/api-tenant-resolver-jwt`](./api-stack/api/api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-stack/api/api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
| [`@suaveplan/api-types`](./api-stack/api/api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
| [`@suaveplan/api-worker-host`](./api-stack/api/api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
| [`@suaveplan/api-worker-rpc`](./api-stack/api/api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
| [`@suaveplan/api-ws`](./api-stack/api/api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
| [`@suaveplan/api-ws-broker-in-memory`](./api-stack/api/api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
| [`@suaveplan/api-ws-broker-redis`](./api-stack/api/api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
| [`@suaveplan/api-ws-bun`](./api-stack/api/api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
| [`@suaveplan/wasm-compiler-javy`](./api-stack/api/wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
### `commerce`
Booking engines, contact management, payment plugins, and commerce primitives.
#### `booking`
Universal booking engine with availability, conflict detection, and pricing.
| Package | Description |
|---|---|
| [`@suaveplan/booking`](./commerce/booking/booking/README.md) | Universal, JSON-configurable booking engine with pluggable strategies for availability resolution, conflict detection, pricing calculation, and rule evaluation… |
#### `contacts`
Contact and group management with deduplication and import/export.
| Package | Description |
|---|---|
| [`@suaveplan/api-plugin-contacts`](./commerce/contacts/api-plugin-contacts/README.md) | Contacts and Groups HTTP plugin for `@suaveplan/api`. Mounts the `/contacts` and `/groups` route trees on a Hono `OpenAPIHono`, backed by SurrealDB implementat… |
| [`@suaveplan/audience-rules`](./commerce/contacts/audience-rules/README.md) | Universal audience segmentation: rule-based filtering on contact data with 14 comparison operators, AND/OR chaining, dot-notation field resolution including ar… |
| [`@suaveplan/contacts`](./commerce/contacts/contacts/README.md) | Universal contact and group management with hierarchical nesting, audience resolution, contact merger, and CSV/JSON import-export. Multi-tenant, channel-agnost… |
| [`@suaveplan/macos-contacts`](./commerce/contacts/macos-contacts/README.md) | Universal bridge to macOS Contacts.app via AppleScript. Reads contacts and groups through the platform `osascript` binary, parses the output into typed records… |
### `communication`
Email delivery, messaging channels, and webhook clients.
#### `emails`
Email delivery, DMARC parsing, templates, campaigns, and suppression lists.
| Package | Description |
|---|---|
| [`@suaveplan/campaigns`](./communication/emails/campaigns/README.md) | Universal campaign orchestration. |
| [`@suaveplan/delivery-tracking`](./communication/emails/delivery-tracking/README.md) | Channel-agnostic delivery event tracking for campaigns and messages: lifecycle dedupe, in-memory + SurrealDB stores, time-series aggregation, tracking pixel an… |
| [`@suaveplan/dmarc`](./communication/emails/dmarc/README.md) | DMARC report parser for `@suaveplan` — parses aggregate (RUA) and forensic (RUF) DMARC XML reports into validated, strongly-typed structures. Accepts raw XML s… |
| [`@suaveplan/domain-normalizer`](./communication/emails/domain-normalizer/README.md) | Normalisation, matching, and alias-canonicalisation for internet domain names. Given heterogeneous URL or domain input — including common mobile/CDN subdomains… |
| [`@suaveplan/email-processing`](./communication/emails/email-processing/README.md) | Universal MIME parsing and email processing for the Suaveplan . Wraps `postal-mime` and `libmime` with structured errors, OpenTelemetry instrumentation, a hook… |
| [`@suaveplan/email-templates`](./communication/emails/email-templates/README.md) | Universal, React-free email template engine. Produces email-safe HTML strings with inline styles via VNode factories — runs identically in Bun, Node, and the b… |
| [`@suaveplan/emailer`](./communication/emails/emailer/README.md) | Universal email service with pluggable transports. Ships `EmailService`, three transports (Dummy / SMTP via injectable `SmtpClientLike` / Gmail REST via inject… |
| [`@suaveplan/suppression`](./communication/emails/suppression/README.md) | Email suppression list management for Suaveplan. Stores and checks email addresses that should not receive outgoing mail — unsubscribes, hard/soft bounces, com… |
#### `messaging`
Messaging adapters — Discord, Slack, Telegram, SMS, iMessage, and notifications.
| Package | Description |
|---|---|
| [`@suaveplan/discord-client`](./communication/messaging/discord-client/README.md) | Generic Discord API client for webhooks, bot messaging, and embeds. Runtime-agnostic via injectable `fetch` and `clock`. Maps every documented Discord failure… |
| [`@suaveplan/imessage-adapter`](./communication/messaging/imessage-adapter/README.md) | macOS iMessage SQLite chat.db watcher and `MessageSourceAdapter` implementation. Polls Apple's `~/Library/Messages/chat.db`, decodes Apple-epoch / nanosecond t… |
| [`@suaveplan/notification-channel`](./communication/messaging/notification-channel/README.md) | Multi-channel notification router with templates, sliding-window rate limiting, retry + dead-letter, scheduler, preferences, hooks, OpenTelemetry instrumentati… |
| [`@suaveplan/slack-client`](./communication/messaging/slack-client/README.md) | Typed, deterministic Slack client — Web API + incoming-webhook surface — with Zod-validated boundaries, OpenTelemetry-instrumented hot paths, injectable `Clock… |
| [`@suaveplan/sms`](./communication/messaging/sms/README.md) | Universal SMS core: provider-agnostic send pipeline with strict E.164 validation, Zod boundaries, template rendering, opt-out registry, token-bucket rate limit… |
| [`@suaveplan/sms-providers`](./communication/messaging/sms-providers/README.md) | Universal SMS provider implementations for @suaveplan/sms. Real HTTP clients for Twilio (Basic auth + form-encoded + X-Twilio-Signature HMAC-SHA1), Vonage (HMAC-SHA256 webhook), MessageBird (AccessKey + JWT HS256 webhook), Plivo (Basic auth + X-Plivo-Signature-V3 HMAC-SHA256), and AWS SNS (inline SigV4 + RSA-SHA1 SNS notification verifier). Ships ProviderRegistry, httpRequest over injectable FetchLike, composeAbortSignal, SMS_PROVIDERS_CODES + SmsProvidersError, per-provider mapStatus, parseInbound, parseOptOutKeyword, and five OTel instruments. No provider SDK deps; every HTTP call flows through an injectable FetchLike seam. |
| [`@suaveplan/telegram-client`](./communication/messaging/telegram-client/README.md) | Telegram Bot API client: send/edit/delete messages, six media variants, inline + custom keyboards, typed errors, injectable `fetch` + `clock`, and OpenTelemetr… |
#### `webhook-client`
Webhook delivery client with retry logic and HMAC verification.
| Package | Description |
|---|---|
| [`@suaveplan/webhook-client`](./communication/webhook-client/webhook-client/README.md) | Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking. Runtime-agnostic (injectable f… |
### `devtools`
Developer tooling, documentation generators, Docker helpers, and build utilities.
#### `docker`
Docker helpers and GPU runtime configuration.
| Package | Description |
|---|---|
| [`@suaveplan/docker-nvidia`](./devtools/docker/docker-nvidia/README.md) | A collection of docker / docker-compose scripts configured for use with WSL / Nvidia |
#### `docs`
Documentation utilities, SVG compression, PDF handling, and sitemap generation.
| Package | Description |
|---|---|
| [`@suaveplan/compress-svg`](./devtools/docs/compress-svg/README.md) | SVG optimisation and minification for TypeScript. Pure TypeScript — no DOMParser, no external SVG library. Runs in Bun, Node.js, browsers, and edge runtimes. |
| [`@suaveplan/docs`](./devtools/docs/docs/README.md) | Server-tier markdown documentation toolkit for Suaveplan. Discovers every `.md` file under a repo root, parses each via `gray-matter` + `remark` into a structu… |
| [`@suaveplan/pdf`](./devtools/docs/pdf/README.md) | Server-tier PDF 1.4 generation toolkit. **Chunk A foundation** — pure utilities + cross-cutting wiring (typed error registry, Zod boundary schemas, OpenTelemet… |
| [`@suaveplan/sitemap`](./devtools/docs/sitemap/README.md) | Sitemap generation and parsing for the sitemaps.org XML protocol. Produces valid `sitemap.xml`, sitemap index documents, and parses existing sitemaps. Supports… |
#### `tooling`
CLI frameworks, installers, and build tooling.
| Package | Description |
|---|---|
| [`@suaveplan/cli`](./devtools/tooling/cli/README.md) | Enterprise-grade CLI framework for Node.js. Composable fluent API with subcommand routing, option parsing, OTel tracing, and a full set of terminal UI primitiv… |
| [`@suaveplan/electrobun`](./devtools/tooling/electrobun/README.md) | Electrobun desktop-app generator and orchestrator for the SuavePlan ecosystem. Scaffolds a fresh Electrobun project from a layered config + prompt flow, drives… |
| [`@suaveplan/install-agent-skills`](./devtools/tooling/install-agent-skills/README.md) | Installer plugin that bundles every Suaveplan skill (`SKILL.md` + assets) and installs them into a target project via `@suaveplan/installer`. |
| [`@suaveplan/installer`](./devtools/tooling/installer/README.md) | Plugin host for project boilerplating and asset installation. Pipelines plugins through `detect → prompts → plan → apply → postInstall` with transactional roll… |
| [`@suaveplan/javy-cli-darwin-arm64`](./devtools/tooling/javy-cli-darwin-arm64/README.md) | Platform wrapper for **Javy v8.1.1** on `darwin-arm64`. |
| [`@suaveplan/javy-cli-darwin-x64`](./devtools/tooling/javy-cli-darwin-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `darwin-x64`. |
| [`@suaveplan/javy-cli-linux-arm64`](./devtools/tooling/javy-cli-linux-arm64/README.md) | Platform wrapper for **Javy v8.1.1** on `linux-arm64`. |
| [`@suaveplan/javy-cli-linux-x64`](./devtools/tooling/javy-cli-linux-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `linux-x64`. |
| [`@suaveplan/javy-cli-win32-x64`](./devtools/tooling/javy-cli-win32-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `win32-x64`. |
| [`@suaveplan/linter`](./devtools/tooling/linter/README.md) | Unified markdown + biome linting pipeline for Suaveplan: a remark-based markdown fixer covering MD012/022/025/029/031/032/035/036/040/044, an injectable `Comma… |
| [`@suaveplan/mcp`](./devtools/tooling/mcp/README.md) | Universal Model Context Protocol (MCP) server + client with a pluggable transport stack, JSON-RPC dispatcher, AbortSignal cancellation, progress notifications,… |
| [`@suaveplan/overview`](./devtools/tooling/overview/README.md) | Machine-readable Suaveplan project manifest: typed PackageRecord / SuavePlanManifest / WorkspaceGraph, Zod schemas, tier inference, filesystem discovery (server sub-path), graph utilities (topo, cycle detection, transitive closure, leaves, roots), plugin/category/tier roll-ups, six OVERVIEW.* error codes, three OTel counters + one histogram. |
| [`@suaveplan/profiler`](./devtools/tooling/profiler/README.md) | Universal runtime performance profiler. |
| [`@suaveplan/readline`](./devtools/tooling/readline/README.md) | AsyncGenerator line reader and interactive prompt interface for Node.js streams, file paths, and Web Streams. |
| [`@suaveplan/sandbox`](./devtools/tooling/sandbox/README.md) | Server-tier sandboxed code execution for `@suaveplan` plugin hosts and agent runtimes. Ships four `SandboxEngine` implementations (a `node:vm` default, real `n… |
| [`@suaveplan/test-vite`](./devtools/tooling/test-vite/README.md) | Vite + Vitest configuration factories and validators for Suaveplan: `createViteLibraryConfig`, `createViteAppConfig`, `createVitestConfig`, `createVitestBrowse… |
### `foundation`
Foundational primitives — types, errors, configuration, data structures, and cross-cutting utilities.
#### `barcode`
1-D barcode generation and parsing.
| Package | Description |
|---|---|
| [`@suaveplan/barcode`](./foundation/barcode/barcode/README.md) | Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary… |
| [`@suaveplan/qrcode`](./foundation/barcode/qrcode/README.md) | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction lev… |
#### `bitpacker`
Bit-level binary packing, schemas, compression, and streaming codecs.
| Package | Description |
|---|---|
| [`@suaveplan/bitpacker`](./foundation/bitpacker/bitpacker/README.md) | Core bit-level binary packing library. Provides `BitWriter`, `BitReader`, `BitPacker`, and a pluggable strategy system for schema-driven serialization. |
| [`@suaveplan/bitpacker-compressed`](./foundation/bitpacker/bitpacker-compressed/README.md) | Compression pipeline integration for bitpacker. |
| [`@suaveplan/bitpacker-crypto`](./foundation/bitpacker/bitpacker-crypto/README.md) | Bit-packer-aware crypto primitives. The shared interfaces (`Authority`, `DomainTag`, `AuthorityVerifier`, `AeadOps`) live in `@suaveplan/types`; the canonical… |
| [`@suaveplan/bitpacker-delta`](./foundation/bitpacker/bitpacker-delta/README.md) | Delta encoding utilities for efficient repeated state transmission. Detects changes between successive values and encodes differences rather than full values. |
| [`@suaveplan/bitpacker-game`](./foundation/bitpacker/bitpacker-game/README.md) | Game state utilities: quantization, prediction, interpolation, and client-server reconciliation. |
| [`@suaveplan/bitpacker-network`](./foundation/bitpacker/bitpacker-network/README.md) | Network simulation utilities for testing bitpacker under realistic network conditions. |
| [`@suaveplan/bitpacker-schema`](./foundation/bitpacker/bitpacker-schema/README.md) | Schema registry and validation for bitpacker. |
| [`@suaveplan/bitpacker-strategies`](./foundation/bitpacker/bitpacker-strategies/README.md) | Built-in packing strategies for `@suaveplan/bitpacker`. Provides strategies for numbers, strings, booleans, objects, arrays, enums, optional fields, oneOf unio… |
| [`@suaveplan/bitpacker-streaming`](./foundation/bitpacker/bitpacker-streaming/README.md) | Streaming APIs for bitpacker including chunked packing/unpacking and true TransformStream support with framing. |
| [`@suaveplan/bitpacker-validation`](./foundation/bitpacker/bitpacker-validation/README.md) | Safety limits and validation reports for bitpacker schemas. |
#### `core`
Core foundation — types, errors, configuration, schema registry, and utilities.
| Package | Description |
|---|---|
| [`@suaveplan/config`](./foundation/core/config/README.md) | Layered configuration management for Suaveplan. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the r… |
| [`@suaveplan/env`](./foundation/core/env/README.md) | Type-safe, schema-first environment variable loading for Node.js and edge runtimes. Define a schema of typed field builders, point it at your sources, and rece… |
| [`@suaveplan/error`](./foundation/core/error/README.md) | Foundation error substrate for Suaveplan. Every non-testing `@suaveplan/*` package extends this library's `SuaveplanError` base class and uses its stable code… |
| [`@suaveplan/schema-registry`](./foundation/core/schema-registry/README.md) | Versioned schema registry with forward/backward/full compatibility checking for evolving data contracts. Register schemas by subject name, enforce evolution ru… |
| [`@suaveplan/types`](./foundation/core/types/README.md) | Types-only foundation package. Zero runtime code, zero dependencies, zero bundle bytes. Provides the canonical shared interfaces that allow every `@suaveplan/*… |
| [`@suaveplan/typescript-config`](./foundation/core/typescript-config/README.md) | Shared TypeScript configuration presets for Suaveplan. This package ships composable JSON presets consumed via `"extends"` across every workspace package — no… |
| [`@suaveplan/utils`](./foundation/core/utils/README.md) | Zero-external-dependency utility library for Suaveplan. Covers debug helpers, deep object manipulation, platform detection, IEEE 754 float packing, pseudo-rand… |
#### `data`
Data manipulation, masking, deduplication, diffing, and byte utilities.
| Package | Description |
|---|---|
| [`@suaveplan/bytes`](./foundation/data/bytes/README.md) | Low-level byte utilities for Suaveplan: lossless hex/base64/UTF-8 encoding, `Uint8Array` manipulation, and UUID conversion. No native addons; depends only on p… |
| [`@suaveplan/clipboard`](./foundation/data/clipboard/README.md) | Enterprise Clipboard API abstraction for browser-tier consumers. Wraps the async Clipboard API with a textarea + `execCommand` fallback, a paste-event read fal… |
| [`@suaveplan/data-masking`](./foundation/data/data-masking/README.md) | PII/sensitive data masking for objects and strings with field-level rules, nested traversal, eight masking strategies, and OpenTelemetry instrumentation. |
| [`@suaveplan/dedup`](./foundation/data/dedup/README.md) | Entity deduplication engine for detecting and merging duplicate records in any data set. Given a list of objects with varying representations of the same real-… |
| [`@suaveplan/diff`](./foundation/data/diff/README.md) | Zero-dependency RFC 6902 JSON Patch engine for computing, applying, and inverting structural differences between JSON documents. The `diff(a, b)` function prod… |
| [`@suaveplan/json`](./foundation/data/json/README.md) | Safe, enterprise-grade JSON utilities with prototype-pollution protection, circular-reference detection, and OpenTelemetry instrumentation. |
| [`@suaveplan/media`](./foundation/data/media/README.md) | Universal media detection, metadata extraction, and constraint validation. Combines a pluggable magic-byte `SignatureRegistry` with the MIME database from `@su… |
| [`@suaveplan/mime`](./foundation/data/mime/README.md) | Structured MIME-type detection, parsing, and registry for the Suaveplan platform. Provides RFC 7231 Content-Type parsing, magic-byte file signature detection,… |
| [`@suaveplan/packer`](./foundation/data/packer/README.md) | Binary packing/unpacking utilities. Two modes: |
| [`@suaveplan/packer-browser`](./foundation/data/packer-browser/README.md) | Browser-tier runtime for `@suaveplan/packer` archives. Ships a Service Worker interceptor, a `PackedResourceManager` (LRU + Blob-URL bookkeeping + ref-count +… |
| [`@suaveplan/pagination`](./foundation/data/pagination/README.md) | Universal pagination primitives for cursor-based, offset-based, and keyset-based (seek) pagination strategies. The package covers the full pagination surface:… |
| [`@suaveplan/pwa`](./foundation/data/pwa/README.md) | Enterprise browser-tier PWA primitives for Suaveplan applications. |
| [`@suaveplan/react-packer`](./foundation/data/react-packer/README.md) | React 19 companion for `@suaveplan/packer-browser`. Ships `usePackedResource`, `usePackedImage`, `usePackedAudio`, `usePackedVideo`, and `usePackedFont` hooks… |
| [`@suaveplan/serialize`](./foundation/data/serialize/README.md) | Round-trip-preserving structured serialization for JavaScript's rich type system. The standard `JSON.stringify`/`JSON.parse` pair silently drops `Date` objects… |
| [`@suaveplan/templates`](./foundation/data/templates/README.md) | A plugin-based string template engine for TypeScript. Resolves `{variable}` and `{variable:transform}` tokens in strings using a priority-sorted, regex-scan pi… |
| [`@suaveplan/validation`](./foundation/data/validation/README.md) | Runtime schema validation with a Zod-compatible API. This package is a self-contained parse engine — it does not import Zod or any other validation library — s… |
| [`@suaveplan/web-workers`](./foundation/data/web-workers/README.md) | Enterprise browser-tier runtime for managing Web Workers, SharedWorkers, and fixed-size worker pools with typed RPC, AbortSignal-aware cancellation, transfer-l… |
#### `etl`
Extract-Transform-Load pipeline engine with typed stages.
| Package | Description |
|---|---|
| [`@suaveplan/etl`](./foundation/etl/etl/README.md) | Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation. |
#### `feature-flags`
Feature-flag evaluation engine with percentage and attribute targeting.
| Package | Description |
|---|---|
| [`@suaveplan/feature-flags`](./foundation/feature-flags/feature-flags/README.md) | Provider-agnostic feature flag evaluation engine for controlling feature rollout without code deploys. The package ships a `FlagManager` high-level façade, a `… |
#### `flow`
Finite state machines, CRDTs, idempotency, and CBOR serialization.
| Package | Description |
|---|---|
| [`@suaveplan/cbor`](./foundation/flow/cbor/README.md) | RFC 8949 (CBOR) encoder + decoder, RFC 8742 (CBOR Sequences), pull-style streaming decoder (`AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`), RFC 90… |
| [`@suaveplan/crdt`](./foundation/flow/crdt/README.md) | Conflict-free Replicated Data Types (CRDTs) for distributed systems. Provides eight composable CRDT primitives plus a vector-clock and a sync manager that coor… |
| [`@suaveplan/fsm`](./foundation/flow/fsm/README.md) | Typed, generic finite state machine (FSM) for TypeScript. Ships a synchronous-by-default interpreter with guard predicates, ordered transition actions, context… |
| [`@suaveplan/idempotency`](./foundation/flow/idempotency/README.md) | Idempotency key guard and deduplication for async operations. The package provides a complete `pending → completed \| failed` state machine, three store backend… |
| [`@suaveplan/job-scheduler`](./foundation/flow/job-scheduler/README.md) | Universal in-process job scheduler with cron + queued runs, retries, timeouts, concurrency control, and OpenTelemetry observability. Built on `@suaveplan/cron`… |
| [`@suaveplan/queue`](./foundation/flow/queue/README.md) | Enterprise-grade queue management: memory/priority/delayed queues, concurrent workers with retry and dead-letter forwarding, job state machines, and OTel instr… |
| [`@suaveplan/queue-distribution`](./foundation/flow/queue-distribution/README.md) | Worker-pool distribution layer: load-balancing strategies, worker pool management, manager with retry and health checks, in-process `LocalWorker`. Transport-ag… |
| [`@suaveplan/queue-distribution-http`](./foundation/flow/queue-distribution-http/README.md) | HTTP transport plugin for `@suaveplan/queue-distribution`. Ships `HttpWorkerCommunicator` (a fetch-backed `WorkerCommunicator` with a Zod-validated wire envelo… |
| [`@suaveplan/queue-distribution-worker-thread`](./foundation/flow/queue-distribution-worker-thread/README.md) | Web-Worker / Node `worker_threads` transport plugin for `@suaveplan/queue-distribution`. Ships `WorkerThreadCommunicator` (a `ThreadWorkerLike`-based message b… |
| [`@suaveplan/rate-limiter`](./foundation/flow/rate-limiter/README.md) | Enterprise rate limiter with three algorithms, rule-based routing, tenant multipliers, pluggable storage, and OTel instrumentation. |
| [`@suaveplan/retry`](./foundation/flow/retry/README.md) | Retry strategies and circuit-breaker for async operations. The package provides constant, linear, and exponential backoff with four jitter modes (full, equal,… |
| [`@suaveplan/rules`](./foundation/flow/rules/README.md) | Universal rule engine + validation system. Comprehensive `RuleEngine` with async evaluation, depth limit, AbortSignal, plugin registration; fluent `RuleBuilder… |
| [`@suaveplan/rules-hono`](./foundation/flow/rules-hono/README.md) | Hono middleware wrapping `@suaveplan/rules`. Six middleware factories — `ruleMiddleware`, `validationMiddleware`, `accessControl`, `responseValidation`, `rateL… |
| [`@suaveplan/streams`](./foundation/flow/streams/README.md) | WHATWG-Streams-based primitives: backpressure-first transforms, zero-copy `Uint8Array` operations, `AbortSignal` integration end-to-end, structured errors, OTe… |
| [`@suaveplan/streams-chunk-queue`](./foundation/flow/streams-chunk-queue/README.md) | Universal out-of-order chunk reconstitution. Accepts chunks for a file in any order and exposes a sequential `ReadableStream<Uint8Array>` consumer surface, com… |
| [`@suaveplan/streams-metadata`](./foundation/flow/streams-metadata/README.md) | Universal stream transforms that detect MIME type and extract EXIF/IPTC/XMP image metadata from buffered `Uint8Array` payloads. Three composable transforms (`M… |
| [`@suaveplan/undo`](./foundation/flow/undo/README.md) | Enterprise-grade undo/redo engine with branching history, async commands, history compression, selective undo, and command merging. Built for collaborative edi… |
| [`@suaveplan/workflow`](./foundation/flow/workflow/README.md) | Workflow and pipeline orchestration: sequential and parallel step execution, conditional branching with guard functions, retry strategies, timeouts, saga-patte… |
#### `random`
Pseudo-random generators, noise functions, and deterministic sequences.
| Package | Description |
|---|---|
| [`@suaveplan/noise`](./foundation/random/noise/README.md) | Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes… |
| [`@suaveplan/pseudo-random`](./foundation/random/pseudo-random/README.md) | Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the `Rng` interface from `@suaveplan/types`. Ships four algorithms — Mulberry32, SF… |
| [`@suaveplan/random`](./foundation/random/random/README.md) | High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every… |
#### `timing`
Cron, stopwatches, throttling, debouncing, and scheduling.
| Package | Description |
|---|---|
| [`@suaveplan/cron`](./foundation/timing/cron/README.md) | Universal cron expression parser, next-fire calculator with timezone + DST awareness, and an opt-in lightweight in-process scheduler. Zero external cron depend… |
| [`@suaveplan/datetime`](./foundation/timing/datetime/README.md) | Timezone-aware date/time utilities covering the most common application-layer date operations that the native `Date` API leaves unaddressed. The package provid… |
| [`@suaveplan/timing`](./foundation/timing/timing/README.md) | Timing and scheduling utilities for application-layer time management. The package covers measurement (stopwatch with lap recording), rate control (throttle an… |
### `gaming`
Game engines, entity-component systems, and simulation libraries.
#### `games`
Game controllers, entity-component systems, and game-server logic.
| Package | Description |
|---|---|
| [`@suaveplan/controller`](./gaming/games/controller/README.md) | Universal input controller foundation. Ships the unified `ControllerInput` shape (37 normalised fields in `[0, 255]`), `BaseController` abstract base with inje… |
| [`@suaveplan/ecs`](./gaming/games/ecs/README.md) | Archetype-based Entity Component System with `Uint32`-backed bitset queries, phase-ordered system scheduling, entity-ID recycling, Zod-validated component sche… |
| [`@suaveplan/game-server`](./gaming/games/game-server/README.md) | Universal game-server framework. Migrated from `@suaveplan/game-server` on the reference branch `claude/review-game-engine-arch-Kaykr` (main repo `packages/uni… |
### `geospatial`
Geospatial data, mapping, planetary coordinates, and logistics utilities.
#### `geo`
Geospatial coordinates, planetary data, and solar-system modelling.
| Package | Description |
|---|---|
| [`@suaveplan/geo`](./geospatial/geo/geo/README.md) | Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants. Every operation that needs a body's shape takes an explicit `Ellipso… |
| [`@suaveplan/geo-earth`](./geospatial/geo/geo-earth/README.md) | Earth configuration and legacy geo utilities. |
| [`@suaveplan/geo-planet`](./geospatial/geo/geo-planet/README.md) | Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements. |
| [`@suaveplan/solar-system`](./geospatial/geo/solar-system/README.md) | Solar system provider with astronomy engine. |
| [`@suaveplan/stars`](./geospatial/geo/stars/README.md) | Star catalog provider. |
| [`@suaveplan/tides`](./geospatial/geo/tides/README.md) | Tidal prediction providers. |
| [`@suaveplan/universe`](./geospatial/geo/universe/README.md) | Observer model, coordinate frames, wire protocol, and provider registry. |
#### `logistics`
Port data and logistics primitives.
| Package | Description |
|---|---|
| [`@suaveplan/ports`](./geospatial/logistics/ports/README.md) | Global port database with shipping-lane routing, ETA estimation, and UN/LOCODE port lookups for sea ports, airports, and rail terminals. |
### `identity`
Authentication, authorization, identity resolution, and credential management.
#### `auth`
Authentication providers, JWT, WebAuthn, OAuth, and session resolution.
| Package | Description |
|---|---|
| [`@suaveplan/auth`](./identity/auth/auth/README.md) | Provider-agnostic authentication library for Suaveplan. Ships JWT token lifecycle, API key management, session management, and framework-agnostic middleware —… |
| [`@suaveplan/better-auth-surrealdb`](./identity/auth/better-auth-surrealdb/README.md) | Better-Auth Adapter for SurrealDB v3 — bundles the canonical 17-table SurrealQL schema, compiles Better-Auth `Where[]` filters to parameterised SurrealQL, auto… |
| [`@suaveplan/jwt-tenant`](./identity/auth/jwt-tenant/README.md) | JWT-based multi-tenant token issuance and verification for Suaveplan. Ships an Ed25519 / ECDSA-P256 signing pipeline, in-memory JWKS store with rotation and ex… |
| [`@suaveplan/macos-keychain`](./identity/auth/macos-keychain/README.md) | macOS Keychain bridge for generic-password credentials. Wraps the system `/usr/bin/security` CLI behind a strongly-typed CRUD surface with typed errors, AbortS… |
| [`@suaveplan/oauth-client`](./identity/auth/oauth-client/README.md) | Universal OAuth 2.0 / OIDC client for Node.js, browsers, Bun, Deno, and edge runtimes. |
| [`@suaveplan/permissions`](./identity/auth/permissions/README.md) | Full RBAC/ABAC permissions engine for Suaveplan. Ships role management, policy evaluation, CASL-inspired ability builder, TTL permission cache, condition evalu… |
| [`@suaveplan/webauthn`](./identity/auth/webauthn/README.md) | W3C WebAuthn Level 3 client + parser + verifier for Suaveplan. Browser-tier registration / authentication entry points sit on the package root; framework-agnos… |
### `localization`
Internationalization, localization, locale detection, and translation infrastructure.
#### `i18n`
Internationalization core, backends, detectors, and locale resolution.
| Package | Description |
|---|---|
| [`@suaveplan/i18n`](./localization/i18n/i18n/README.md) | Slim integration core for the i18n family. After the Wave 6 modular split, this package owns the `MessageCatalog`, the `createI18n` / `I18nInstance` plugin slo… |
| [`@suaveplan/i18n-backend-chained`](./localization/i18n/i18n-backend-chained/README.md) | Universal chained backend orchestrator for the `@suaveplan/i18n` family. Composes N backends into a single `Backend` with read-fallthrough on `BACKEND_LOCALE_N… |
| [`@suaveplan/i18n-backend-fs-multifile`](./localization/i18n/i18n-backend-fs-multifile/README.md) | Server-tier multi-file Node fs/promises translation backend for the @suaveplan/i18n family. Reads <root>/<locale>/<namespace>.<ext> (nested), <root>/<locale>.<namespace>.<ext> (flat), or a caller-supplied glob pattern. JSON built-in; YAML and JSON5 auto-detected via dynamic import. Optional fs.watch loop emits loaded events through the catalog event bus. AbortSignal cancellation, OpenTelemetry instrumentation under i18n.backend.fs_multifile.*. Server-only — uses node:fs/promises; browser consumers should use @suaveplan/i18n-backend-http or @suaveplan/i18n-backend-storage. |
| [`@suaveplan/i18n-backend-http`](./localization/i18n/i18n-backend-http/README.md) | Universal translation backend for the `@suaveplan/i18n` family, backed by an injectable `fetch`. Read-only by design: maps HTTP 404 to `BACKEND_LOCALE_NOT_AVAI… |
| [`@suaveplan/i18n-backend-memory`](./localization/i18n/i18n-backend-memory/README.md) | Universal in-memory translation backend for the `@suaveplan/i18n` family. Backed by `Map<locale, Map<namespace, TranslationRecord>>` plus a modification ledger… |
| [`@suaveplan/i18n-backend-storage`](./localization/i18n/i18n-backend-storage/README.md) | Universal translation backend for the `@suaveplan/i18n` family, backed by any `AbstractStorage` implementation from `@suaveplan/storage`. Implements the full `… |
| [`@suaveplan/i18n-detector`](./localization/i18n/i18n-detector/README.md) | Universal locale-detection chain for the `@suaveplan/i18n` family. Ships the chain orchestrator, the BCP-47 default negotiator, the `getTextDirection` / `isRtl… |
| [`@suaveplan/i18n-formatter-collator`](./localization/i18n/i18n-formatter-collator/README.md) | Wrapper around `Intl.Collator` for the `@suaveplan/i18n` family — locale-aware string comparison and sorting (sensitivity, numeric-aware natural sort, case-fir… |
| [`@suaveplan/i18n-formatter-currency`](./localization/i18n/i18n-formatter-currency/README.md) | Locale-aware currency formatting backed by `Intl.NumberFormat` with `style: "currency"`. Wave 3 of the 14-package i18n modular split. Provides both a class (`C… |
| [`@suaveplan/i18n-formatter-date`](./localization/i18n/i18n-formatter-date/README.md) | Wrapper around `Intl.DateTimeFormat` for the `@suaveplan/i18n` family — 8-preset table (short/medium/long/full × date+time/time-only/date-only), range renderin… |
| [`@suaveplan/i18n-formatter-display-name`](./localization/i18n/i18n-formatter-display-name/README.md) | Wrapper around `Intl.DisplayNames` for the `@suaveplan/i18n` family — locale-aware language, region, script, currency, and calendar display names. Universal le… |
| [`@suaveplan/i18n-formatter-list`](./localization/i18n/i18n-formatter-list/README.md) | Locale-aware list joining backed by `Intl.ListFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`ListFormatter`) and a standalone he… |
| [`@suaveplan/i18n-formatter-number`](./localization/i18n/i18n-formatter-number/README.md) | Locale-aware number formatting backed by `Intl.NumberFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`NumberFormatter`) and a stan… |
| [`@suaveplan/i18n-formatter-relative-time`](./localization/i18n/i18n-formatter-relative-time/README.md) | Locale-aware relative-time formatting backed by `Intl.RelativeTimeFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`RelativeTimeFor… |
| [`@suaveplan/i18n-formatter-segmenter`](./localization/i18n/i18n-formatter-segmenter/README.md) | Wrapper around `Intl.Segmenter` for the `@suaveplan/i18n` family — locale-aware text segmentation by grapheme, word, or sentence. Compound emoji count as a sin… |
| [`@suaveplan/i18n-icu-parser`](./localization/i18n/i18n-icu-parser/README.md) | ICU MessageFormat parser, `{{var}}` interpolation engine, and 9-language CLDR pluralization for the `@suaveplan/i18n` family. Universal leaf package — depends… |
| [`@suaveplan/i18n-shared`](./localization/i18n/i18n-shared/README.md) | Shared runtime atoms for the entire `@suaveplan/i18n*` package family — locale constants, BCP-47 normalisation map, RTL set, locale descriptors, interpolation… |
| [`@suaveplan/locale-data`](./localization/i18n/locale-data/README.md) | Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions, OTel instrumentation, and Zod schemas. |
| [`@suaveplan/react-i18n`](./localization/i18n/react-i18n/README.md) | React bindings for `@suaveplan/i18n`. Ships the `<TranslationProvider>`, twelve hooks, four headless components, the class-component HOC, the nine built-in loc… |
### `media`
Audio, video, image, and font processing libraries.
#### `audio`
Audio processing, playback, analysis, and effect plugins.
| Package | Description |
|---|---|
| [`@suaveplan/audio-analysis-beat`](./media/audio/audio-analysis-beat/README.md) | Universal beat, tempo, and onset detection plugin for the `@suaveplan/audio-*` family. Detects BPM, beat locations, and onset times from `AudioDocument` stream… |
| [`@suaveplan/audio-analysis-loudness`](./media/audio/audio-analysis-loudness/README.md) | Universal ITU-R BS.1770-4 loudness analyser. Drop-in `AnalysisOp` plugin for the `@suaveplan/audio-*` family. |
| [`@suaveplan/audio-analysis-pitch`](./media/audio/audio-analysis-pitch/README.md) | Universal pitch, key, and chord detection Analysis Op plugins for the `@suaveplan/audio-*` family. Three detectors: |
| [`@suaveplan/audio-analysis-spectrum`](./media/audio/audio-analysis-spectrum/README.md) | Universal spectral analysis for the Suaveplan audio stack: real FFT, Hann/Hamming/Blackman windows, mel filterbank, DCT-II, and three high-level analyzers — `S… |
| [`@suaveplan/audio-cli`](./media/audio/audio-cli/README.md) | Server-tier `audio` command-line tool. Thin shell over `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, and `@suaveplan/audio-engine-server`: every audio tr… |
| [`@suaveplan/audio-codec-ffmpeg-native`](./media/audio/audio-codec-ffmpeg-native/README.md) | Server-tier `AudioCodec` plugin for `@suaveplan/audio-codecs` that shells out to the system `ffmpeg` binary via `fluent-ffmpeg` and streams audio through the s… |
| [`@suaveplan/audio-codec-ffmpeg-wasm`](./media/audio/audio-codec-ffmpeg-wasm/README.md) | Browser-tier `AudioCodec` plugin for `@suaveplan/audio-codecs` backed by `@ffmpeg/ffmpeg` running inside a dedicated Web Worker. Decodes and encodes `wav`, `mp… |
| [`@suaveplan/audio-codecs`](./media/audio/audio-codecs/README.md) | Universal audio codec registry: decode/encode plugin loader with magic-byte format detection, progressive streaming decode, native WAV/AIFF/CAF codecs, and laz… |
| [`@suaveplan/audio-controllers`](./media/audio/audio-controllers/README.md) | Universal MIDI hardware controller plugins. Built on `@suaveplan/audio-core`, `@suaveplan/audio-transport`, `@suaveplan/events`, `@suaveplan/fsm`, and `@suavep… |
| [`@suaveplan/audio-core`](./media/audio/audio-core/README.md) | Universal audio data model and processing primitives. Provides a paged `AudioDocument`, a non-destructive `PlanEngine`, a block-level `ProcessPipeline`, and a… |
| [`@suaveplan/audio-decode-wav`](./media/audio/audio-decode-wav/README.md) | Universal WAV decoder wrapper over the upstream `@audio/decode-wav` library. Exposes a stable, OTel-instrumented, AbortSignal-honouring contract over a frozen,… |
| [`@suaveplan/audio-engine-browser`](./media/audio/audio-engine-browser/README.md) | Browser-tier `TimelineAdapter` implementation for the `@suaveplan/audio-*` family. Hosts Web Audio rendering, `AudioWorklet` loading, and the `AudioFxPlugin.cr… |
| [`@suaveplan/audio-engine-server`](./media/audio/audio-engine-server/README.md) | Server-tier audio I/O layer for the `@suaveplan/audio-*` family. Provides the thinnest possible bridge between universal `AudioDocument` primitives and a Node.… |
| [`@suaveplan/audio-fingerprint`](./media/audio/audio-fingerprint/README.md) | High-accuracy audio fingerprinting service. The package is dual-runtime: a Python FastAPI backend (`src/fingerprint.py` + `src/api/app.py`) implements chromagr… |
| [`@suaveplan/audio-metadata`](./media/audio/audio-metadata/README.md) | Universal audio metadata extractor that parses ID3v1/v2, MP4/M4A atoms, FLAC Vorbis comments, Ogg Vorbis, WAV RIFF, and AIFF/AIFC containers, returning a norma… |
| [`@suaveplan/audio-sprite-packer`](./media/audio/audio-sprite-packer/README.md) | Universal audio sprite packer with injectable adapter, Zod-validated boundaries, and OpenTelemetry instrumentation. Zero browser or Node.js dependencies. |
| [`@suaveplan/audio-sprite-player`](./media/audio/audio-sprite-player/README.md) | Universal audio sprite playback engine. Consumes manifests produced by `@suaveplan/audio-sprite-packer`, validates them, schedules voices, runs voice-stealing… |
| [`@suaveplan/audio-timeline`](./media/audio/audio-timeline/README.md) | Headless DAW timeline engine with multi-track arrangements, clips, playback, loop regions, selection, zoom, per-track mixer state, undo/redo, and a pluggable `… |
| [`@suaveplan/audio-transport`](./media/audio/audio-transport/README.md) | Universal musical clock and transport: sample-accurate scheduling with PPQ, BPM automation, swing, time signatures, and loop regions. Zero browser or Node depe… |
| [`@suaveplan/control-surface-schema`](./media/audio/control-surface-schema/README.md) | Universal canonical schema for DAW-style control surfaces. Owns the `ControlSurface` root document, the `ControlNode = Group \| Control` discriminated tree, the… |
| [`@suaveplan/midi`](./media/audio/midi/README.md) | Universal MIDI primitives for the Suaveplan platform. Owns the canonical types, byte parsers, message constructors, control-surface dispatchers (NRPN, RPN, 14-… |
| [`@suaveplan/third-party-elevenlabs`](./media/audio/third-party-elevenlabs/README.md) | <!-- begin:auto-docs --> |
| [`@suaveplan/voice`](./media/audio/voice/README.md) | Provider-agnostic voice core for Suaveplan. Defines the abstract interfaces, shared types, Zod schemas, and structured error hierarchy that every voice provide… |
| [`@suaveplan/voice-elevenlabs`](./media/audio/voice-elevenlabs/README.md) | ElevenLabs domain adapter for the `@suaveplan/voice` interface family. |
#### `audio-fx`
Real-time audio effects and DSP plugins.
| Package | Description |
|---|---|
| [`@suaveplan/audio-fx-plugin-audio-analyzer`](./media/audio-fx/audio-fx-plugin-audio-analyzer/README.md) | audio-analyzer plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-audio-player`](./media/audio-fx/audio-fx-plugin-audio-player/README.md) | audio-player plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-auto-filter`](./media/audio-fx/audio-fx-plugin-auto-filter/README.md) | auto-filter plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-base`](./media/audio-fx/audio-fx-plugin-base/README.md) | Foundation for the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Zero `@suaveplan/audio-fx-plugin-*` deps… |
| [`@suaveplan/audio-fx-plugin-chorus`](./media/audio-fx/audio-fx-plugin-chorus/README.md) | chorus plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-compressor`](./media/audio-fx/audio-fx-plugin-compressor/README.md) | compressor plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-delay`](./media/audio-fx/audio-fx-plugin-delay/README.md) | delay plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-distortion`](./media/audio-fx/audio-fx-plugin-distortion/README.md) | distortion plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-eq`](./media/audio-fx/audio-fx-plugin-eq/README.md) | Parametric / shelving / high-pass / low-pass equaliser for the `@suaveplan/audio-fx-plugin-*` family. Multi-band biquad implementation, sample-accurate paramet… |
| [`@suaveplan/audio-fx-plugin-filter`](./media/audio-fx/audio-fx-plugin-filter/README.md) | filter plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-flanger`](./media/audio-fx/audio-fx-plugin-flanger/README.md) | flanger plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-game-audio`](./media/audio-fx/audio-fx-plugin-game-audio/README.md) | game-audio plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-gate`](./media/audio-fx/audio-fx-plugin-gate/README.md) | gate plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-glados`](./media/audio-fx/audio-fx-plugin-glados/README.md) | glados plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-limiter`](./media/audio-fx/audio-fx-plugin-limiter/README.md) | limiter plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-microphone`](./media/audio-fx/audio-fx-plugin-microphone/README.md) | microphone plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-mixer`](./media/audio-fx/audio-fx-plugin-mixer/README.md) | mixer plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-phaser`](./media/audio-fx/audio-fx-plugin-phaser/README.md) | phaser plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-pitch-shifter`](./media/audio-fx/audio-fx-plugin-pitch-shifter/README.md) | pitch-shifter plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-reverb`](./media/audio-fx/audio-fx-plugin-reverb/README.md) | reverb plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-sampler`](./media/audio-fx/audio-fx-plugin-sampler/README.md) | sampler plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-spatial`](./media/audio-fx/audio-fx-plugin-spatial/README.md) | spatial plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-spessasynth`](./media/audio-fx/audio-fx-plugin-spessasynth/README.md) | SF2/SF3 SoundFont synthesizer plugin for the `@suaveplan/audio-fx-plugin-*` family. Wraps the MIT-licensed SpessaSynth engine as a standard `AudioFxPlugin`, so… |
| [`@suaveplan/audio-fx-plugin-synthesizer`](./media/audio-fx/audio-fx-plugin-synthesizer/README.md) | synthesizer plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-tremolo`](./media/audio-fx/audio-fx-plugin-tremolo/README.md) | tremolo plugin for the @suaveplan/audio-fx-plugin-* family. |
| [`@suaveplan/audio-fx-plugin-widener`](./media/audio-fx/audio-fx-plugin-widener/README.md) | widener plugin for the @suaveplan/audio-fx-plugin-* family. |
#### `audio-ir`
Impulse-response capture, loading, and room-acoustic plugins.
| Package | Description |
|---|---|
| [`@suaveplan/audio-ir-core`](./media/audio-ir/audio-ir-core/README.md) | Foundation contract for the `@suaveplan/audio-ir-*` family. Universal; zero `@suaveplan/audio-*` runtime deps. Every sibling package (`audio-ir-loader`, `audio… |
| [`@suaveplan/audio-ir-loader`](./media/audio-ir/audio-ir-loader/README.md) | Pipeline package for the `@suaveplan/audio-ir-*` family. Composes `IrRegistry` (from `@suaveplan/audio-ir-core`), a `StorageUriResolver` (from `@suaveplan/stor… |
| [`@suaveplan/audio-ir-room-plugin-club`](./media/audio-ir/audio-ir-room-plugin-club/README.md) | Original audio-ir content package. Ships two bundled room impulse responses (`club1.wav`, `club2.wav`) wired through the `@suaveplan/audio-ir-*` family. |
| [`@suaveplan/audio-ir-room-plugin-pool`](./media/audio-ir/audio-ir-room-plugin-pool/README.md) | Original audio-ir content package. Ships a single `room`-family impulse response — `pool-outdoor-swimming-pool` — sourced from the outdoor-swimming-pool captur… |
#### `fonts`
Font packing, compression, subsetting, and conversion tools.
| Package | Description |
|---|---|
| [`@suaveplan/font-compress`](./media/fonts/font-compress/README.md) | BTF (Binary TypeFace) binary packing and unpacking. Converts `TypefaceFont` (typeface.js JSON format) to a compact `Uint8Array` using dictionary-based integer… |
| [`@suaveplan/font-convert`](./media/fonts/font-convert/README.md) | Converts TTF/OTF/WOFF/WOFF2 font files to the typeface.js JSON format used by Three.js `FontLoader`. Also defines the canonical `TypefaceFont`, `TypefaceGlyph`… |
| [`@suaveplan/font-decompress`](./media/fonts/font-decompress/README.md) | BTF (Binary TypeFace) binary unpacking. Reverses `@suaveplan/font-compress`'s `BinaryFontPacker` output into a typeface.js JSON shape ready for Three.js / R3F… |
| [`@suaveplan/font-subset`](./media/fonts/font-subset/README.md) | Font subsetting and compression library with a codec-agnostic adapter interface. Supports Unicode range subsetting, WOFF2/WOFF/OTF/TTF output, OpenType feature… |
#### `image`
Image compression, colour conversion, EXIF handling, and optimisation.
| Package | Description |
|---|---|
| [`@suaveplan/caddy-imgproxy`](./media/image/caddy-imgproxy/README.md) | Universal TypeScript port of the Caddy imgproxy URL rewriter. Parses the compact `/u<op><gravity><dims><modifiers>/<path>.<format>` grammar, resolves aspect-ra… |
| [`@suaveplan/colour-convert-adapt`](./media/image/colour-convert-adapt/README.md) | Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's appearance so that it looks the same under different light so… |
| [`@suaveplan/colour-convert-cieluv`](./media/image/colour-convert-cieluv/README.md) | CIELUV, xyY, LMS, and Bradford chromatic adaptation conversions. This plugin provides colour-space transforms between RGB, CIELUV, xyY, and LMS cone-response s… |
| [`@suaveplan/colour-convert-cmyk`](./media/image/colour-convert-cmyk/README.md) | Bidirectional conversion between RGB and CMYK colour spaces using the standard Euclidean algorithm with black generation. CMYK is the subtractive model used in… |
| [`@suaveplan/colour-convert-hsluv`](./media/image/colour-convert-hsluv/README.md) | HSLuv perceptually-uniform colour space with gamut-mapped ray intersection for sRGB. This plugin provides bidirectional RGB↔HSLuv/HPLuv conversions and uniform… |
| [`@suaveplan/colour-filter-cct`](./media/image/colour-filter-cct/README.md) | Correlated Colour Temperature (CCT) conversions using the McCamy formula and black-body RGB approximation. Convert any RGB colour to its approximate colour tem… |
| [`@suaveplan/colour-filter-cmc`](./media/image/colour-filter-cmc/README.md) | CMC l:c Delta-E colour difference metric with hue-dependent chroma scaling and low-luminance branch handling. This plugin computes perceptual distance between… |
| [`@suaveplan/colour-filter-greyscale`](./media/image/colour-filter-greyscale/README.md) | Converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coefficients. Unlike simple channel averaging, this formula wei… |
| [`@suaveplan/colour-filter-sepia`](./media/image/colour-filter-sepia/README.md) | Applies a photographic sepia tone filter to RGB colours using the standard W3C CSS Filters 3×3 matrix. This plugin converts any colour into a warm brownish ton… |
| [`@suaveplan/colours`](./media/image/colours/README.md) | Universal colour manipulation library: parsing, conversion, analysis, mixing, blending, and palette generation. Parses hex (3/4/6/8 digit), CSS `rgb()`, `hsl()… |
| [`@suaveplan/compress-image`](./media/image/compress-image/README.md) | Runtime-agnostic image compression utilities with pluggable adapters for Node.js (Sharp) and testing (MemoryAdapter). Supports resize, quality reduction, forma… |
| [`@suaveplan/emblem-solver`](./media/image/emblem-solver/README.md) | Minimal-rectangle SVG converter for pixelated sprite emblems. |
| [`@suaveplan/exif`](./media/image/exif/README.md) | Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode… |
| [`@suaveplan/image`](./media/image/image/README.md) | Universal image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. Zero browser or Node dependencies — runs… |
| [`@suaveplan/image-quantisation`](./media/image/image-quantisation/README.md) | Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms. Desi… |
#### `video`
Video encoding, decoding, and thumbnail generation.
| Package | Description |
|---|---|
| [`@suaveplan/video`](./media/video/video/README.md) | Universal video metadata extraction, magic-byte format detection, resolution math, transform-pipeline planning, and a fluent pipeline builder. Zero browser-onl… |
| [`@suaveplan/video-thumbnails`](./media/video/video-thumbnails/README.md) | Video thumbnail extraction and sprite-sheet generation backed by an injectable `FfmpegRunner`. The default runner — `FluentFfmpegRunner` — wraps `fluent-ffmpeg… |
### `money`
Payments, currencies, cryptocurrency, and financial primitives.
#### `finance`
Currency conversion, cryptocurrency utilities, and exchange-rate providers.
| Package | Description |
|---|---|
| [`@suaveplan/cryptocurrency`](./money/finance/cryptocurrency/README.md) | Universal cryptocurrency utilities for wallet address validation, unit conversion, chain registry, gas estimation, ERC-20 token registry, calldata / function-s… |
| [`@suaveplan/currency`](./money/finance/currency/README.md) | Universal currency conversion engine — pluggable `CurrencyProvider` interface, TTL-bounded `RateCache` with direct / inverse / triangulated lookup, primary + f… |
| [`@suaveplan/currency-provider-binance`](./money/finance/currency-provider-binance/README.md) | Binance public-ticker crypto-rates provider for `@suaveplan/currency`. Sibling package in the foundation-with-sibling-plugins shape: implements the `CurrencyPr… |
| [`@suaveplan/currency-provider-coingecko`](./money/finance/currency-provider-coingecko/README.md) | Universal CoinGecko sibling for `@suaveplan/currency-providers`. Implements the `CurrencyProvider` contract from `@suaveplan/currency` against the public `/sim… |
| [`@suaveplan/currency-provider-coinmarketcap`](./money/finance/currency-provider-coinmarketcap/README.md) | CoinMarketCap crypto-rates sibling provider for `@suaveplan/currency`. One of the nine `@suaveplan/currency-provider-<name>` sibling packages spun out of the 2… |
| [`@suaveplan/currency-provider-cryptocompare`](./money/finance/currency-provider-cryptocompare/README.md) | Universal CryptoCompare crypto-rates provider for `@suaveplan/currency`. Sibling provider package on top of the slim `@suaveplan/currency-providers` core (`htt… |
| [`@suaveplan/currency-provider-ecb`](./money/finance/currency-provider-ecb/README.md) | Universal `@suaveplan/currency` rate provider for the European Central Bank's daily exchange-rates XML feed. Free, no API key required. Built on the slim `@sua… |
| [`@suaveplan/currency-provider-fixer`](./money/finance/currency-provider-fixer/README.md) | Fixer.io fiat-rates sibling provider for the slim `@suaveplan/currency-providers` core. Implements the `CurrencyProvider` contract from `@suaveplan/currency`,… |
| [`@suaveplan/currency-provider-kraken`](./money/finance/currency-provider-kraken/README.md) | Universal Kraken public-ticker rate provider for `@suaveplan/currency`. Wraps Kraken's free, no-API-key `/0/public/Ticker` endpoint into a `CurrencyProvider` s… |
| [`@suaveplan/currency-provider-open-exchange-rates`](./money/finance/currency-provider-open-exchange-rates/README.md) | Universal `@suaveplan/currency` provider for openexchangerates.org. Routes `/latest.json` through the slim-core `httpRequest()` helper, validates the upstream… |
| [`@suaveplan/currency-provider-virtual`](./money/finance/currency-provider-virtual/README.md) | In-memory, programmable virtual / game currency provider for `@suaveplan/currency`. Sibling of the `@suaveplan/currency-providers` slim plugin core. No HTTP, n… |
| [`@suaveplan/currency-providers`](./money/finance/currency-providers/README.md) | Slim plugin core for `@suaveplan/currency` rate providers. Ships the `CurrencyProviderRegistry` (name-keyed lookup), the `defineCurrencyProvider` factory, the… |
| [`@suaveplan/money`](./money/finance/money/README.md) | Universal precise currency and money arithmetic using integer minor-unit representation. All operations use `bigint` so consumers never hit floating-point roun… |
#### `payment`
Payment intent lifecycle, provider adapters, and fraud detection.
| Package | Description |
|---|---|
| [`@suaveplan/payment`](./money/payment/payment/README.md) | Universal payment intent lifecycle, webhook signature verification, receipt generation, fraud scoring, refund tracking, tax calculation, retry orchestration, r… |
| [`@suaveplan/payment-provider-adyen`](./money/payment/payment-provider-adyen/README.md) | Universal **Adyen Checkout API v71 adapter** for `@suaveplan/payment`. Implements the `PaymentProvider` interface via the slim `@suaveplan/payment-providers` c… |
| [`@suaveplan/payment-provider-mollie`](./money/payment/payment-provider-mollie/README.md) | Universal Mollie Payments API v2 adapter for `@suaveplan/payment`. Sibling provider package on the slim `@suaveplan/payment-providers` core: implements `Paymen… |
| [`@suaveplan/payment-provider-paypal`](./money/payment/payment-provider-paypal/README.md) | PayPal sibling adapter for the slim `@suaveplan/payment-providers` foundation. Ships `PayPalProvider` — a `PaymentProvider` implementation against the PayPal O… |
| [`@suaveplan/payment-provider-square`](./money/payment/payment-provider-square/README.md) | Universal Square Payments API adapter for `@suaveplan/payment`. Ships the `SquareProvider` class (implements the structural `PaymentProvider` contract with lit… |
| [`@suaveplan/payment-provider-stripe`](./money/payment/payment-provider-stripe/README.md) | Universal Stripe adapter for `@suaveplan/payment`. Implements `PaymentProvider` against the Stripe REST API using the slim `@suaveplan/payment-providers` found… |
| [`@suaveplan/payment-providers`](./money/payment/payment-providers/README.md) | Universal **foundation** for the `@suaveplan/payment-provider-*` sibling-package family. This package ships only the shared abstractions every concrete adapter… |
### `networking`
Distributed networking, event buses, drive mesh, and network discovery.
#### `drive`
Virtual filesystem, mesh networking, and chunk-addressed storage plugins.
| Package | Description |
|---|---|
| [`@suaveplan/drive`](./networking/drive/drive/README.md) | Modular virtual filesystem core for Suaveplan. File-level operations over `AbstractStorage` with a plugin slot for middleware, chunking, encryption, and P2P me… |
| [`@suaveplan/drive-mesh`](./networking/drive/drive-mesh/README.md) | P2P mesh orchestrator over the drive chunks plugin. Consumes `ChunkSource` (from `@suaveplan/drive-plugin-chunks`) and one or more `DriveTransport` implementat… |
| [`@suaveplan/drive-mesh-protocol`](./networking/drive/drive-mesh-protocol/README.md) | Wire protocol for chunk discovery, request, and delivery across the mesh. Three envelope shapes (`cd:request`, `cd:advertise`, `cd:response`) with three reques… |
| [`@suaveplan/drive-plugin-cache`](./networking/drive/drive-plugin-cache/README.md) | Read-through cache middleware for `@suaveplan/drive`. Accepts a caller-supplied `AbstractCache<Uint8Array>` (from `@suaveplan/cache`) and intercepts every `dri… |
| [`@suaveplan/drive-plugin-chunks`](./networking/drive/drive-plugin-chunks/README.md) | Chunk-addressing plugin for `@suaveplan/drive`. Splits files into signed, encrypted chunks using the locked `version 0x0001` binary format, with server-signed… |
| [`@suaveplan/drive-react-sharing`](./networking/drive/drive-react-sharing/README.md) | React-tier UI surface for `@suaveplan/drive-sharing`. Three controlled components plus a real-engine-backed hook. |
| [`@suaveplan/drive-sharing`](./networking/drive/drive-sharing/README.md) | Partitioned drive sharing engine for the Suaveplan ecosystem. Ships envelope encryption with mandatory AAD, per-user key wrapping, partition policies with dete… |
| [`@suaveplan/drive-storage-archive-core`](./networking/drive/drive-storage-archive-core/README.md) | Shared archive-mounting primitives for the Suaveplan drive: magic-byte format sniffing, split-volume sibling resolution, password hook contract, mutation-mode… |
| [`@suaveplan/drive-storage-zip`](./networking/drive/drive-storage-zip/README.md) | ZIP archive adapter for `@suaveplan/drive`. Reads and writes standards-compliant zip archives through the structural `DriveStorage` view; supports ZIP64, split… |
| [`@suaveplan/drive-template`](./networking/drive/drive-template/README.md) | Server-signed drive templates for the dehydrated-drive workflow. A `DriveTemplate` describes a drive's full folder structure and per-file metadata; the actual… |
| [`@suaveplan/drive-transport-https`](./networking/drive/drive-transport-https/README.md) | HTTPS-relay-backed `TransportLike<MessageEnvelope>` for the Suaveplan drive mesh. Implements long-poll receive, stateless POST send, exponential backoff with e… |
| [`@suaveplan/drive-transport-webrtc`](./networking/drive/drive-transport-webrtc/README.md) | WebRTC datachannel transport for the drive mesh. Implements the canonical `DriveTransport` interface from `@suaveplan/drive-mesh` over a `@suaveplan/drive-webr… |
| [`@suaveplan/drive-uri`](./networking/drive/drive-uri/README.md) | URI plugins (`drive://`, `drive+mesh://`, `drive+template://`) for `@suaveplan/storage-uri`'s `StorageUriResolver`. Wraps a caller-supplied `Drive` instance in… |
| [`@suaveplan/drive-webrtc`](./networking/drive/drive-webrtc/README.md) | Browser-tier WebRTC P2P chunk-distribution plugin for `@suaveplan/drive`. Provides a complete mesh networking stack — signalling client, ICE-aware WebRTC clien… |
#### `events`
Typed event bus, event sourcing, metrics, and transport bridges.
| Package | Description |
|---|---|
| [`@suaveplan/broadcast-channel`](./networking/events/broadcast-channel/README.md) | Typed cross-tab messaging built on the Web `BroadcastChannel` API. Browser-tier package — leader election, tab presence and heartbeats, message-type handler re… |
| [`@suaveplan/event-sourcing`](./networking/events/event-sourcing/README.md) | Event sourcing patterns for Suaveplan: typed aggregate root, in-memory event store with optimistic concurrency, snapshot support, projections, upcasting, and O… |
| [`@suaveplan/events`](./networking/events/events/README.md) | Typed event-bus substrate for Suaveplan. Pub/sub with deterministic FIFO delivery, interceptor API, bounded-queue backpressure, idempotent listeners, Zod-valid… |
| [`@suaveplan/events-metrics`](./networking/events/events-metrics/README.md) | OTel metrics companion for `@suaveplan/events`. Attaches via the public interceptor API to record dispatch totals, listener error counts, and latency histogram… |
| [`@suaveplan/events-otel`](./networking/events/events-otel/README.md) | OpenTelemetry tracing companion for `@suaveplan/events`. Wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span e… |
| [`@suaveplan/events-schema`](./networking/events/events-schema/README.md) | Runtime schema-validation companion for `@suaveplan/events`. Validates event payloads at dispatch boundaries via Zod schemas with strict/warn/sample modes and… |
| [`@suaveplan/events-transport`](./networking/events/events-transport/README.md) | Cross-context event bridges for `@suaveplan/events`: `MessageChannel` (at-least-once with ack/retry) and `BroadcastChannel` (at-most-once, fan-out) transports… |
#### `network`
Mesh networking, WebSocket transports, bootstrap, and discovery protocols.
| Package | Description |
|---|---|
| [`@suaveplan/network`](./networking/network/network/README.md) | Core network interfaces and runtime for the mesh system. |
| [`@suaveplan/network-bootstrap`](./networking/network/network-bootstrap/README.md) | Pluggable peer-discovery chain, persistent peer cache, and `NetworkPlugin` wrapper for the `@suaveplan/network` mesh. Provides the canonical way for a freshly… |
| [`@suaveplan/network-discovery-crdt`](./networking/network/network-discovery-crdt/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Wraps an awareness-document CRDT (Yjs *or* Automerge — chosen at construction) and produce… |
| [`@suaveplan/network-discovery-gundb`](./networking/network/network-discovery-gundb/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` chains. Wraps a GunDB instance — gossip-over- WebSocket decentralised graph database — and… |
| [`@suaveplan/network-discovery-hybrid`](./networking/network/network-discovery-hybrid/README.md) | Browser-tier multi-strategy DiscoveryStrategy orchestrator: composes any subset of the five Wave-2 sibling strategies (peerjs / crdt / trystero / webtorrent / gundb) into a single HybridDiscoveryManager with priority-based peer selection, per-strategy health tracking, automatic fallback, de-duplicated peer set with PeerSource provenance, and unified lifecycle. Implements the network-bootstrap DiscoveryStrategy contract so it can itself be chained. |
| [`@suaveplan/network-discovery-peerjs`](./networking/network/network-discovery-peerjs/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` that wraps a PeerJS signalling-server client + WebRTC `DataConnection`s into a deduped peer… |
| [`@suaveplan/network-discovery-trystero`](./networking/network/network-discovery-trystero/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Serverless WebRTC peer discovery via Trystero, with four interchangeable transports — BitT… |
| [`@suaveplan/network-discovery-webtorrent`](./networking/network/network-discovery-webtorrent/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` chains. Wraps a `p2pt` instance — WebTorrent-tracker-driven WebRTC peer discovery — and exp… |
| [`@suaveplan/network-gundb-server`](./networking/network/network-gundb-server/README.md) | Server-tier GunDB relay daemon. Hosts a Gun relay peer plus standard `/health` / `/ready` / `/live` probes on a single Node `http.Server`. Browser-tier clients… |
| [`@suaveplan/network-libp2p-server`](./networking/network/network-libp2p-server/README.md) | Server-tier libp2p relay daemon paired with `@suaveplan/network-transport-libp2p`. Hosts a libp2p node configured as a bootstrap + circuit-relay-v2 server alon… |
| [`@suaveplan/network-plugin-assignment`](./networking/network/network-plugin-assignment/README.md) | Mesh auto-assignment and load balancing: live `MeshCapacityTracker`, weighted-multi-criteria `AssignmentEngine`, imbalance-detecting `MeshBalancer` planner, an… |
| [`@suaveplan/network-plugin-audit-mesh`](./networking/network/network-plugin-audit-mesh/README.md) | Tamper-evident, cryptographically signed, append-only audit log chain for the Suaveplan mesh. `SignedLog` produces hash-chained `LogEntry` records keyed by mon… |
| [`@suaveplan/network-plugin-auth`](./networking/network/network-plugin-auth/README.md) | Mesh membership authentication and cross-mesh relay authorization. Bridges `@suaveplan/auth` into the mesh: peer join verification, capability-tier-bound token… |
| [`@suaveplan/network-plugin-circuit-breaker`](./networking/network/network-plugin-circuit-breaker/README.md) | Mesh-level circuit breakers: per-path state machine (`CLOSED → OPEN → HALF_OPEN`), registry, half-open probe loop, and relay middleware integration. Fast-fails… |
| [`@suaveplan/network-plugin-content-delivery`](./networking/network/network-plugin-content-delivery/README.md) | Mesh-as-CDN core for the Suaveplan network. `ChunkIndex` maintains a distributed holder map keyed by chunk hash with gossip-style merge over `ChunkAdvertisemen… |
| [`@suaveplan/network-plugin-crypto-messages`](./networking/network/network-plugin-crypto-messages/README.md) | Cryptographic integrity layer for the Suaveplan mesh. Ed25519-backed `MessageSigner` (sign on send, verify against the registry on receive); `SessionTokenAutho… |
| [`@suaveplan/network-plugin-devtools`](./networking/network/network-plugin-devtools/README.md) | Developer-tools plugin for the Suaveplan mesh. Bundles four subsystems behind a single `NetworkPlugin` lifecycle so engineers can inspect, visualise, and profi… |
| [`@suaveplan/network-plugin-failover`](./networking/network/network-plugin-failover/README.md) | Mesh failover plugin: per-peer heartbeat manager, alive→suspect→dead→recovering health monitor, debounced promoter that triggers emergency elections, and a coo… |
| [`@suaveplan/network-plugin-governance`](./networking/network/network-plugin-governance/README.md) | Election engine for the Suaveplan mesh: candidate scoring, reputation tracking, voting, and host transitions. |
| [`@suaveplan/network-plugin-intra-relay`](./networking/network/network-plugin-intra-relay/README.md) | Intra-mesh peer-to-peer relay for the Suaveplan network. When direct WebRTC fails (symmetric NAT, restrictive firewall), `IntraRelayPlugin` routes the message… |
| [`@suaveplan/network-plugin-middleware`](./networking/network/network-plugin-middleware/README.md) | Priority-ordered middleware chain for the Suaveplan mesh. Inbound and outbound message flow each pass through the same five-band pipeline (CRITICAL → HIGH → NO… |
| [`@suaveplan/network-plugin-migration`](./networking/network/network-plugin-migration/README.md) | Peer state migration between meshes: 8-step `MigrationCoordinator`, snapshot serialize/deserialize via `StateTransferProtocol`, graceful-handoff `ForwardingTun… |
| [`@suaveplan/network-plugin-monitor`](./networking/network/network-plugin-monitor/README.md) | Production monitoring + distributed tracing for the Suaveplan mesh. Collects metrics across four categories (node / connection / message / hierarchy), supports… |
| [`@suaveplan/network-plugin-partition`](./networking/network/network-plugin-partition/README.md) | Split-brain detection and resolution for the mesh: epoch-based `PartitionDetector` (deterministic partition-id from sorted reachable set), `QuorumResolver` wit… |
| [`@suaveplan/network-plugin-protocol`](./networking/network/network-plugin-protocol/README.md) | Network protocol plugin for the mesh system. |
| [`@suaveplan/network-plugin-qos`](./networking/network/network-plugin-qos/README.md) | Bandwidth management and quality-of-service for the mesh: bandwidth-quota enforcer, message-priority classifier, QoS router (bypass / drop / queue / defer), co… |
| [`@suaveplan/network-plugin-registry`](./networking/network/network-plugin-registry/README.md) | Fixed-capacity peer registry with bitpacker-based binary encoding. |
| [`@suaveplan/network-plugin-relay`](./networking/network/network-plugin-relay/README.md) | Cross-mesh relay — the federation layer that lets Tier 0 hosts of different meshes route messages to each other. Composes `RelayRouter`, `HostBackboneTransport… |
| [`@suaveplan/network-plugin-server-bridge`](./networking/network/network-plugin-server-bridge/README.md) | Tier-0 host ↔ central-server bridge for the Suaveplan mesh. |
| [`@suaveplan/network-plugin-session`](./networking/network/network-plugin-session/README.md) | Hierarchical session management for the Suaveplan mesh. `SessionTree` maintains a parent-child session graph rooted at a single auto-created root; `SessionMana… |
| [`@suaveplan/network-plugin-simulation`](./networking/network/network-plugin-simulation/README.md) | Deterministic virtual-network simulation framework for the Suaveplan mesh. `NetworkSimulator` runs scenarios against a seeded RNG with simulated time (accelera… |
| [`@suaveplan/network-plugin-state`](./networking/network/network-plugin-state/README.md) | Mesh-wide synchronized state for the Suaveplan network. Path-based `StateManager` (versioned history with sequence numbers + per-path subscriptions); `DeltaCom… |
| [`@suaveplan/network-plugin-topology`](./networking/network/network-plugin-topology/README.md) | Tiered mesh topology plugin: 3-tier `HierarchicalTopology` (Host / SubHost / Leaf) with promote/demote, latency-bucket-based `LatencyAssigner`, `GroupManager`,… |
| [`@suaveplan/network-plugin-transport-manager`](./networking/network/network-plugin-transport-manager/README.md) | Multi-transport orchestration for the Suaveplan mesh. Maintains a pool of registered transports per peer, negotiates capability bitmaps during handshake, route… |
| [`@suaveplan/network-testing`](./networking/network/network-testing/README.md) | <!-- begin:auto-docs --> |
| [`@suaveplan/network-transport-hyperswarm`](./networking/network/network-transport-hyperswarm/README.md) | Server-tier Hyperswarm DHT transport for `@suaveplan/network`. Provides hole-punched peer-to-peer streams discovered via Kademlia DHT topics. |
| [`@suaveplan/network-transport-libp2p`](./networking/network/network-transport-libp2p/README.md) | Universal libp2p `Transport` for `@suaveplan/network`. Composes WebSockets or WebRTC sub-transports with Noise + Yamux, identify + ping + bootstrap + circuit-r… |
| [`@suaveplan/network-transport-standby`](./networking/network/network-transport-standby/README.md) | Pre-emptive WebRTC standby transport for `@suaveplan/network`. Maintains a pool of warm `RTCPeerConnection`-like slots so peer attach completes in ~10 ms inste… |
| [`@suaveplan/network-transport-webrtc`](./networking/network/network-transport-webrtc/README.md) | WebRTC transport plugin for mesh networking. |
| [`@suaveplan/network-transport-websockets`](./networking/network/network-transport-websockets/README.md) | WebSocket client utilities with typed message framing, connection lifecycle management, heartbeat, backpressure queue, and injectable transport adapters. |
| [`@suaveplan/network-trystero-server`](./networking/network/network-trystero-server/README.md) | Server-tier Trystero signaling daemon. Hosts an MQTT-style or Nostr-style WebSocket broker shim plus standard `/health` / `/ready` / `/live` probes on a single… |
| [`@suaveplan/react-network-libp2p-demo`](./networking/network/react-network-libp2p-demo/README.md) | React-tier demo UI for `@suaveplan/network-transport-libp2p`. Splits the upstream 749-LOC monolithic demo into per-concern subcomponents (`<NodeStatusPanel>`,… |
| [`@suaveplan/service-discovery`](./networking/network/service-discovery/README.md) | Universal service-registry primitives. Provides the shared `ServiceDescriptor`, `ServiceQuery`, `RegistryEvent`, and `ServiceRegistry` contract consumed by eve… |
| [`@suaveplan/websocket-gateway`](./networking/network/websocket-gateway/README.md) | Server-tier WebSocket gateway daemon with rooms, presence tracking, and channel-multiplexed pub/sub. Pairs with `@suaveplan/network-transport-websockets` on th… |
### `observability`
Monitoring, logging, health checks, audit trails, and analytics.
#### `monitor`
Logging, health checks, audit logging, and analytics instrumentation.
| Package | Description |
|---|---|
| [`@suaveplan/analytics-browser`](./observability/monitor/analytics-browser/README.md) | Browser-tier client-side product analytics — `AnalyticsTracker`, `BatchTransport`, `BeaconTransport`, and `SessionManager` with consent gate, PII scrubber, hea… |
| [`@suaveplan/audit-logging`](./observability/monitor/audit-logging/README.md) | Tamper-proof, GDPR-compliant audit logging for Suaveplan. Provides a cryptographic chain-integrity engine, pluggable storage adapters, GDPR data export, config… |
| [`@suaveplan/health-check`](./observability/monitor/health-check/README.md) | Server-tier health-check core for `/health`, `/ready`, `/live` endpoints with dependency checks (HTTP, TCP, custom), aggregated status, caching, and typed life… |
| [`@suaveplan/logger`](./observability/monitor/logger/README.md) | Enterprise-grade structured logger for Suaveplan. Provides a full pipeline: level filtering → OTel trace enrichment → sampling → redaction → sinks. Supports mu… |
| [`@suaveplan/opentelemetry`](./observability/monitor/opentelemetry/README.md) | OpenTelemetry SDK setup and configuration helpers for Suaveplan. Provides batteries-included OTel initialization helpers, resource detection, ergonomic span/me… |
### `persistence`
Caching, database adapters, search engines, and storage abstractions.
#### `cache`
Caching abstractions, memory caches, and Redis backends.
| Package | Description |
|---|---|
| [`@suaveplan/cache`](./persistence/cache/cache/README.md) | Type-safe caching abstraction for Suaveplan. Provides `MemoryCache` with LRU/LFU/FIFO/TTL eviction, `NamespacedCache` for shared-storage isolation, a `CacheFac… |
| [`@suaveplan/cache-redis`](./persistence/cache/cache-redis/README.md) | `AbstractCache<T>` backend over a minimal `RedisLike` interface — native `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN`. Reuses `@suaveplan/cache-storage`… |
| [`@suaveplan/cache-storage`](./persistence/cache/cache-storage/README.md) | `AbstractCache<T>` backend that bridges any `@suaveplan/storage` `AbstractStorage` into a typed cache via a 32-byte fixed-width TTL header and pluggable `Codec… |
#### `database`
Database adapters, migrations, seeding, and SQLite support.
| Package | Description |
|---|---|
| [`@suaveplan/db`](./persistence/database/db/README.md) | Universal relational database adapter interface for Suaveplan. Provides the `DatabaseAdapter` contract (extending `DatabaseLike` from `@suaveplan/types`), inje… |
| [`@suaveplan/db-sqlite`](./persistence/database/db-sqlite/README.md) | Universal SQLite adapter for Suaveplan. Runs on `bun:sqlite` on the Bun server and `@sqlite.org/sqlite-wasm` (Web Worker) in the browser, unified behind a sing… |
| [`@suaveplan/migration`](./persistence/database/migration/README.md) | Database-agnostic schema migration runner with ledger tracking, checksum validation, dry-run reporting, distributed locking, event bus integration, and OTel in… |
| [`@suaveplan/seed`](./persistence/database/seed/README.md) | Universal declarative database seeding and fixture framework. Define seed data once, let the runner handle dependency ordering, environment filtering, idempote… |
| [`@suaveplan/surrealdb`](./persistence/database/surrealdb/README.md) | Universal SurrealDB client wrapper for Suaveplan. Implements `@suaveplan/db`'s `DatabaseAdapter` interface with connection lifecycle management, typed CRUD, sc… |
#### `search`
Full-text search, BM25, fuzzy matching, and faceted search.
| Package | Description |
|---|---|
| [`@suaveplan/search`](./persistence/search/search/README.md) | Full-text and fuzzy search primitives with in-memory BM25 engine, trigram fuzzy matching, faceted search, and pagination support. |
#### `storage`
Storage abstractions, browser backends, S3, CDN, and URI routing.
| Package | Description |
|---|---|
| [`@suaveplan/storage`](./persistence/storage/storage/README.md) | Enterprise key-value storage abstractions for Suaveplan. Provides `AbstractStorage` (implementing `StorageLike`), `MemoryStorage` with TTL and zero-copy stream… |
| [`@suaveplan/storage-browser-indexeddb`](./persistence/storage/storage-browser-indexeddb/README.md) | Browser-tier `AbstractStorage` adapter backed by the W3C IndexedDB API (`globalThis.indexedDB`). Provides persistent, origin-scoped key-value storage for brows… |
| [`@suaveplan/storage-browser-localstorage`](./persistence/storage/storage-browser-localstorage/README.md) | Browser-tier `AbstractStorage` adapter backed by the synchronous `Storage` API (`globalThis.localStorage`). Provides namespaced, origin-scoped key-value storag… |
| [`@suaveplan/storage-browser-opfs`](./persistence/storage/storage-browser-opfs/README.md) | Browser-tier `AbstractStorage` adapter backed by the Origin Private File System (OPFS). Provides persistent, origin-scoped key-value storage for browser consum… |
| [`@suaveplan/storage-cdn`](./persistence/storage/storage-cdn/README.md) | Types-only foundation for CDN-flavoured `@suaveplan/storage` adapters. Declares `CdnAdapter` (an abstract base class extending `AbstractStorage` with `purge`,… |
| [`@suaveplan/storage-cdn-bunnycdn`](./persistence/storage/storage-cdn-bunnycdn/README.md) | BunnyCDN Storage Zone + Pull Zone provider for `@suaveplan/storage-cdn`. Full `AbstractStorage` and `CdnAdapter` surface — `get`/`set`/`delete`/`exists`/`getMe… |
| [`@suaveplan/storage-cdn-rackspace`](./persistence/storage/storage-cdn-rackspace/README.md) | Rackspace Cloud Files (OpenStack Swift) origin + Rackspace CDN provider for `@suaveplan/storage-cdn`. Full `AbstractStorage` and `CdnAdapter` surface — `get`/`… |
| [`@suaveplan/storage-file`](./persistence/storage/storage-file/README.md) | Node.js filesystem `AbstractStorage` adapter for `@suaveplan/storage`. Implements atomic writes via temp-file-then-rename, AbortSignal threaded through every a… |
| [`@suaveplan/storage-http`](./persistence/storage/storage-http/README.md) | Universal HTTP/CDN-backed `AbstractStorage` adapter for `@suaveplan/storage`. Reads via `GET`/`HEAD`/`Range` against any `<baseUrl><key>` origin, with optional… |
| [`@suaveplan/storage-plugin-cache`](./persistence/storage/storage-plugin-cache/README.md) | Read-through cache decorator for `AbstractStorage`. Wraps any `@suaveplan/storage` instance with an injectable `AbstractCache<Uint8Array>`; hit/miss/populate o… |
| [`@suaveplan/storage-s3`](./persistence/storage/storage-s3/README.md) | S3-compatible object storage adapter for `@suaveplan/storage`. Extends `AbstractStorage` backed by `@aws-sdk/client-s3` with presigned URL generation, multipar… |
| [`@suaveplan/storage-uri`](./persistence/storage/storage-uri/README.md) | Universal URI-routed dispatch over `@suaveplan` `StorageLike` adapters. |
| [`@suaveplan/storage-uri-plugin-bundle`](./persistence/storage/storage-uri-plugin-bundle/README.md) | Per-package `bundle://` URI scheme for `@suaveplan/storage-uri`. |
| [`@suaveplan/storage-uri-plugin-fallback`](./persistence/storage/storage-uri-plugin-fallback/README.md) | Universal `fallback://` URI plugin for `@suaveplan/storage-uri`. Ships ordered multi-source resolution with cycle detection, depth cap, mirror writes, list uni… |
### `security-stack`
Cryptography, vaults, security utilities, and threat-detection primitives.
#### `crypto`
Cryptographic primitives, ciphers, hashes, curves, and key management.
| Package | Description |
|---|---|
| [`@suaveplan/crypto`](./security-stack/crypto/crypto/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
| [`@suaveplan/crypto-bin`](./security-stack/crypto/crypto-bin/README.md) | Suaveplan password-hashing CLI. Ships the `suaveplan-hash-password` binary — Argon2id (default) and BLAKE2b password hashing + verification with PHC/MCF-format… |
| [`@suaveplan/crypto-browser`](./security-stack/crypto/crypto-browser/README.md) | WebCrypto-backed engine implementations for `@suaveplan/crypto` — AES-256-GCM, Ed25519, ECDSA-P256/P384, X25519, ECDH-P256/P384, RSA, AES-KW, and non-extractab… |
| [`@suaveplan/crypto-cipher`](./security-stack/crypto/crypto-cipher/README.md) | Low-level AEAD (Authenticated Encryption with Associated Data) cipher primitives for the `@suaveplan/crypto` stack. Provides three algorithms — AES-256-GCM, Ch… |
| [`@suaveplan/crypto-cli`](./security-stack/crypto/crypto-cli/README.md) | Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke, inspect, verify, export-public, restore) |
| [`@suaveplan/crypto-core`](./security-stack/crypto/crypto-core/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
| [`@suaveplan/crypto-curve`](./security-stack/crypto/crypto-curve/README.md) | Low-level elliptic-curve building blocks for the `@suaveplan/crypto` stack. Exposes four curves — secp256k1, Ed25519, X25519, and NIST P-256 — each as a standa… |
| [`@suaveplan/crypto-hash`](./security-stack/crypto/crypto-hash/README.md) | Pure-TypeScript hashing substrate. SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id, and hex/Base64 encoding — no WASM, no browser globals, fully tree-shakeable. |
| [`@suaveplan/crypto-keys`](./security-stack/crypto/crypto-keys/README.md) | Hierarchical keychain chain-signing for the crypto ecosystem. Store-agnostic, free of `node:` / `bun:` imports. |
| [`@suaveplan/crypto-rsa`](./security-stack/crypto/crypto-rsa/README.md) | RSA primitives for Suaveplan — RFC 8017 PKCS #1 v2.2, RFC 5208 / RFC 5958 PKCS #8, RFC 5280 SPKI, RFC 7517 / RFC 7518 JWK, RFC 7468 PEM. Universal package (zer… |
| [`@suaveplan/crypto-sodium`](./security-stack/crypto/crypto-sodium/README.md) | libsodium-backed engine implementations for `@suaveplan/crypto`. Provides AEAD encryption, Ed25519 signing, X25519 key agreement, sealed-box, auth-box, determi… |
| [`@suaveplan/crypto-utils`](./security-stack/crypto/crypto-utils/README.md) | Shared cryptographic utilities consumed internally by all other `@suaveplan/crypto-*` packages. Provides the low-level primitives that every crypto implementat… |
| [`@suaveplan/crypto-vault`](./security-stack/crypto/crypto-vault/README.md) | Cryptographic foundation for the Vaulted password-manager ecosystem. Provides BIP39 mnemonic generation, OWASP-grade Argon2id derivation, two-secret key deriva… |
#### `security`
Have-I-Been-Pwned checks, JWS, licensing, and OTP utilities.
| Package | Description |
|---|---|
| [`@suaveplan/hibp`](./security-stack/security/hibp/README.md) | Privacy-preserving Have I Been Pwned v3 client. Universal — works in Node, Bun, Deno, browsers, edge workers. The only required runtime primitive is `fetch`. |
| [`@suaveplan/jws`](./security-stack/security/jws/README.md) | RFC 7515 **JSON Web Signature** (verify + sign). Pure TypeScript over `Uint8Array` + `DataView` plus the workspace crypto primitives (`@suaveplan/crypto-rsa`,… |
| [`@suaveplan/license`](./security-stack/security/license/README.md) | Compact, offline-verifiable, secp256k1-signed software-license-key format. Bit-for-bit wire-format compatible with upstream `@suaveplan/license`. runs unchange… |
| [`@suaveplan/otp`](./security-stack/security/otp/README.md) | RFC 4226 (HOTP) + RFC 6238 (TOTP) + RFC 6287 (OCRA) + Google Authenticator Key URI Format + Base32 (RFC 4648 §6) + recovery codes + clock-drift detector — zero browser/Node deps |
| [`@suaveplan/password-formats`](./security-stack/security/password-formats/README.md) | Universal import/export layer for common password-manager export formats. Parses Apple Passwords CSV, Bitwarden JSON, Chrome CSV, and KeePass XML into a normal… |
| [`@suaveplan/password-generator`](./security-stack/security/password-generator/README.md) | Cryptographically-strong password and passphrase generator — char-class, pronounceable, EFF Diceware, and PIN modes with bias-free RNG, real entropy estimates,… |
| [`@suaveplan/password-strength`](./security-stack/security/password-strength/README.md) | zxcvbn-style password strength estimator — pattern-matching + dictionary cracker model per Wheeler 2016. Universal; zero browser/Node/React deps. Re-implementa… |
| [`@suaveplan/security-codes`](./security-stack/security/security-codes/README.md) | Detect and sanitise sensitive security-code leakage in strings. Handles API keys, JWTs, credit cards, GitHub tokens, Stripe keys, SSH keys, PEM blocks, AWS acc… |
| [`@suaveplan/server-keys`](./security-stack/security/server-keys/README.md) | Operator-facing CLI bundle for the Suaveplan server-key lifecycle: 18 bins covering root + server keychain generation (including hierarchical `ChainedKeySigner… |
| [`@suaveplan/slsa`](./security-stack/security/slsa/README.md) | SLSA L1-L2 supply-chain security for Suaveplan. Generates and verifies in-toto / SLSA v1.0 provenance, CycloneDX 1.5 SBOMs, DSSE attestation envelopes, and RFC… |
| [`@suaveplan/srp`](./security-stack/security/srp/README.md) | SRP-6a zero-knowledge password proof per RFC 5054 / RFC 2945. Universal — client and server roles in one package, bundled 2048-bit safe-prime group, optional 1… |
| [`@suaveplan/tpm-struct`](./security-stack/security/tpm-struct/README.md) | Zero-runtime-dep TPM 2.0 attestation-structure parser and serialiser. Decodes `TPMS_ATTEST` (TCG TPM 2.0 Library Part 2 §10.12), decodes `TPMT_PUBLIC` (§12.2),… |
| [`@suaveplan/vault`](./security-stack/security/vault/README.md) | Vault orchestration layer for the Vaulted password-manager ecosystem. Provides a drive-backed encrypted SQLite-style container, a strict `locked` ↔ `unlocked`… |
| [`@suaveplan/vault-sync`](./security-stack/security/vault-sync/README.md) | Domain-specific sync and conflict-resolution engine for the Vaulted password-manager ecosystem. Builds on `@suaveplan/crdt` and `@suaveplan/diff` (RFC 6902 JSO… |
| [`@suaveplan/x509`](./security-stack/security/x509/README.md) | RFC 5280 X.509 v3 certificate parser and certification-path validator. Universal (zero browser, Node, or React deps). Pure TypeScript over `Uint8Array` / `Data… |
### `testkit`
Testing frameworks, A/B testing, and quality-assurance utilities.
#### `testing`
Testing runners, DOM helpers, E2E harnesses, and A/B testing.
| Package | Description |
|---|---|
| [`@suaveplan/ab-testing`](./testkit/testing/ab-testing/README.md) | Enterprise A/B testing and experimentation engine with deterministic FNV-1a bucketing, targeting rules, variant assignment, conversion tracking, result aggrega… |
| [`@suaveplan/testing`](./testkit/testing/testing/README.md) | Shared test infrastructure for Suaveplan. Provides runner primitives (Vitest or bun-test via conditional exports), structured builders, a deterministic FakeClo… |
| [`@suaveplan/testing-dom`](./testkit/testing/testing-dom/README.md) | DOM and React component testing utilities for Suaveplan. Provides a single install that covers vitest config generation with WSL2-safe defaults, `@testing-libr… |
| [`@suaveplan/testing-e2e`](./testkit/testing/testing-e2e/README.md) | Playwright E2E testing utilities for Suaveplan, with the default `browser` worker fixture launching `cloakbrowser` — a real Chromium binary with 49 source-leve… |
| [`@suaveplan/testing-integration`](./testkit/testing/testing-integration/README.md) | Core integration test infrastructure for Vitest: fixture lifecycle management, health polling, and the global-setup bridge. |
| [`@suaveplan/testing-integration-garage-s3`](./testkit/testing/testing-integration-garage-s3/README.md) | Garage S3 v2.0.0 testcontainers fixture for `@suaveplan/testing-integration`. |
| [`@suaveplan/testing-integration-mailpit`](./testkit/testing/testing-integration-mailpit/README.md) | Mailpit testcontainers fixture + `assertMail()` helper for `@suaveplan/testing-integration`. |
| [`@suaveplan/testing-integration-oauth-mock`](./testkit/testing/testing-integration-oauth-mock/README.md) | Real-HTTP mock OAuth 2.0 Identity Provider for integration tests. |
| [`@suaveplan/testing-integration-postgres`](./testkit/testing/testing-integration-postgres/README.md) | PostgreSQL 16 testcontainers fixture for `@suaveplan/testing-integration`. |
| [`@suaveplan/testing-integration-redis`](./testkit/testing/testing-integration-redis/README.md) | Redis 7 testcontainers fixture for `@suaveplan/testing-integration`. |
| [`@suaveplan/testing-integration-step-ca`](./testkit/testing/testing-integration-step-ca/README.md) | A `ServiceFixture` for `smallstep/step-ca` that gives any integration test suite a real, ephemeral certificate authority — root cert, JWK provisioner, and an `… |
| [`@suaveplan/testing-integration-surrealdb`](./testkit/testing/testing-integration-surrealdb/README.md) | SurrealDB v3 testcontainers fixture for `@suaveplan/testing-integration`. |
| [`@suaveplan/testing-integration-typesense`](./testkit/testing/testing-integration-typesense/README.md) | Typesense 27 testcontainers fixture for `@suaveplan/testing-integration`. |
### `threejs`
Three.js and React Three Fiber integrations.
#### `r3f`
React Three Fiber networking and scene components.
| Package | Description |
|---|---|
| [`@suaveplan/r3f-networking`](./threejs/r3f/r3f-networking/README.md) | R3F-tier multiplayer state-synchronisation for React-Three-Fiber scenes. Composes any `@suaveplan/network` `Transport` (canonical: `@suaveplan/network-transpor… |
### `ui`
React components, forms, and user-interface primitives.
#### `forms`
Form builders, validation, and React form components.
| Package | Description |
|---|---|
| [`@suaveplan/form-builder`](./ui/forms/form-builder/README.md) | Universal dynamic form schema engine. Renderer-agnostic — no DOM, no React, no Node-specific globals. Drives every dynamic form surface in the Suaveplan. |
| [`@suaveplan/react-forms`](./ui/forms/react-forms/README.md) | React forms library for the Suaveplan ecosystem. Wraps react-hook-form + Zod with a primitives + factory + recipes architecture: every user-facing string is ty… |
#### `react`
React hooks and components for audio, auth, and UI surfaces.
| Package | Description |
|---|---|
| [`@suaveplan/react-audio`](./ui/react/react-audio/README.md) | React surface for the Suaveplan audio family. **Phase 1 of 3 — foundation** (provider + hooks + stores + error codes + a11y helpers). Components ship in Phase… |
| [`@suaveplan/react-audio-fingerprint`](./ui/react/react-audio-fingerprint/README.md) | React 19 surface for client-side audio fingerprinting and identity matching. The package implements the classic Shazam-style constellation algorithm (2D-peak d… |
| [`@suaveplan/react-audio-player`](./ui/react/react-audio-player/README.md) | Track-style media-player surface for the Suaveplan audio family. The package ships two kinds of exports: |
| [`@suaveplan/react-audio-stream`](./ui/react/react-audio-stream/README.md) | React 19 surface for live audio streaming sessions: a Provider, six hooks, and four accessible components. Chunk C of the four-chunk `enterprise-react-audio-st… |
| [`@suaveplan/react-auth`](./ui/react/react-auth/README.md) | React authentication core for Suaveplan apps — a typed wrapper around `better-auth/client`, an `AuthProvider` React context, an `AuthGuard` route component, `u… |
| [`@suaveplan/react-auth-whitelabel`](./ui/react/react-auth-whitelabel/README.md) | White-label authentication React components with tenant branding wired through the `@suaveplan/react-ui` runtime theme-token mutator. |
| [`@suaveplan/react-control-surface`](./ui/react/react-control-surface/README.md) | React 19 atomic-control surface renderer for `@suaveplan/control-surface-schema`. |
| [`@suaveplan/react-midi`](./ui/react/react-midi/README.md) | React 19 adapter for the Web MIDI API. Provides a strict-mode-safe `<MidiProvider>` that requests `MIDIAccess` exactly once, seven typed hooks for common consu… |
| [`@suaveplan/react-ui`](./ui/react/react-ui/README.md) | React UI component library — opinionated providers, theme/pack plugin contracts, bundled translations, telemetry, shadcn-compatible registry generator. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1,3 @@
# `packages`
Suaveplan package tree. Every `@suaveplan/*` library lives under a category (`packages/<category>/`) and a subcategory (`packages/<category>/<subcategory>/`).

View File

@@ -0,0 +1,106 @@
# `api-stack`
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.
<!-- begin:auto-docs -->
## Subcategories
1 subcategory, 90 packages.
### `api`
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
| Package | Description |
|---|---|
| [`@suaveplan/api`](./api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
| [`@suaveplan/api-2pc`](./api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
| [`@suaveplan/api-audit-file`](./api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
| [`@suaveplan/api-audit-stdout`](./api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
| [`@suaveplan/api-auth`](./api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
| [`@suaveplan/api-auth-anonymous`](./api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
| [`@suaveplan/api-auth-bearer-static`](./api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
| [`@suaveplan/api-auth-better-auth`](./api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
| [`@suaveplan/api-auth-jwt`](./api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
| [`@suaveplan/api-auth-provider`](./api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
| [`@suaveplan/api-auth-rolled`](./api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
| [`@suaveplan/api-auth-static-principal`](./api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
| [`@suaveplan/api-db-adapter`](./api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
| [`@suaveplan/api-db-sqlite-memory`](./api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
| [`@suaveplan/api-db-worker-proxy`](./api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
| [`@suaveplan/api-events-in-memory`](./api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
| [`@suaveplan/api-events-worker-proxy`](./api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
| [`@suaveplan/api-graphql`](./api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
| [`@suaveplan/api-graphql-apollo`](./api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
| [`@suaveplan/api-graphql-ui-altair`](./api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
| [`@suaveplan/api-graphql-ui-graphiql`](./api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
| [`@suaveplan/api-graphql-yoga`](./api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
| [`@suaveplan/api-host`](./api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
| [`@suaveplan/api-jobs-worker-proxy`](./api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
| [`@suaveplan/api-lifecycle`](./api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
| [`@suaveplan/api-loader`](./api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-logger-worker-proxy`](./api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
| [`@suaveplan/api-manifest`](./api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
| [`@suaveplan/api-migrations`](./api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
| [`@suaveplan/api-openapi`](./api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
| [`@suaveplan/api-openapi-ui-redoc`](./api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
| [`@suaveplan/api-openapi-ui-scalar`](./api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
| [`@suaveplan/api-openapi-ui-swagger`](./api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
| [`@suaveplan/api-plugin`](./api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
| [`@suaveplan/api-plugin-audit`](./api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
| [`@suaveplan/api-plugin-auth`](./api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
| [`@suaveplan/api-plugin-campaigns`](./api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
| [`@suaveplan/api-plugin-cms`](./api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
| [`@suaveplan/api-plugin-database`](./api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
| [`@suaveplan/api-plugin-event-bus`](./api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
| [`@suaveplan/api-plugin-events`](./api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
| [`@suaveplan/api-plugin-example`](./api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
| [`@suaveplan/api-plugin-export`](./api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
| [`@suaveplan/api-plugin-file-upload`](./api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
| [`@suaveplan/api-plugin-graphql`](./api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
| [`@suaveplan/api-plugin-import`](./api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
| [`@suaveplan/api-plugin-notifications`](./api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
| [`@suaveplan/api-plugin-payment`](./api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
| [`@suaveplan/api-plugin-rate-limit`](./api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
| [`@suaveplan/api-plugin-search`](./api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
| [`@suaveplan/api-plugin-site`](./api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
| [`@suaveplan/api-plugin-sse`](./api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
| [`@suaveplan/api-plugin-tenant`](./api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
| [`@suaveplan/api-plugin-user`](./api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
| [`@suaveplan/api-plugin-vault`](./api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
| [`@suaveplan/api-plugin-webhook`](./api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
| [`@suaveplan/api-rate-limit`](./api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
| [`@suaveplan/api-rbac`](./api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
| [`@suaveplan/api-router`](./api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-router-hono`](./api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
| [`@suaveplan/api-router-in-memory`](./api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
| [`@suaveplan/api-router-worker-bridge`](./api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
| [`@suaveplan/api-saga`](./api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
| [`@suaveplan/api-sandbox-permissions`](./api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
| [`@suaveplan/api-sandbox-vm`](./api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
| [`@suaveplan/api-sandbox-wasm`](./api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
| [`@suaveplan/api-schemas`](./api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
| [`@suaveplan/api-secrets`](./api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
| [`@suaveplan/api-secrets-bun`](./api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
| [`@suaveplan/api-secrets-env`](./api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
| [`@suaveplan/api-secrets-vault`](./api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
| [`@suaveplan/api-secrets-worker-proxy`](./api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
| [`@suaveplan/api-sse`](./api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
| [`@suaveplan/api-sse-bun`](./api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
| [`@suaveplan/api-tenant`](./api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
| [`@suaveplan/api-tenant-provider`](./api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
| [`@suaveplan/api-tenant-provider-memory`](./api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
| [`@suaveplan/api-tenant-resolver-header`](./api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
| [`@suaveplan/api-tenant-resolver-jwt`](./api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
| [`@suaveplan/api-types`](./api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
| [`@suaveplan/api-worker-host`](./api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
| [`@suaveplan/api-worker-rpc`](./api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
| [`@suaveplan/api-ws`](./api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
| [`@suaveplan/api-ws-broker-in-memory`](./api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
| [`@suaveplan/api-ws-broker-redis`](./api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
| [`@suaveplan/api-ws-bun`](./api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
| [`@suaveplan/wasm-compiler-javy`](./wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
<!-- end:auto-docs -->

View File

@@ -0,0 +1,3 @@
# `api-stack`
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.

View File

@@ -0,0 +1,102 @@
# `api`
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
<!-- begin:auto-docs -->
## Packages in `api`
90 packages.
| Package | Description |
|---|---|
| [`@suaveplan/api`](./api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
| [`@suaveplan/api-2pc`](./api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
| [`@suaveplan/api-audit-file`](./api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
| [`@suaveplan/api-audit-stdout`](./api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
| [`@suaveplan/api-auth`](./api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
| [`@suaveplan/api-auth-anonymous`](./api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
| [`@suaveplan/api-auth-bearer-static`](./api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
| [`@suaveplan/api-auth-better-auth`](./api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
| [`@suaveplan/api-auth-jwt`](./api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
| [`@suaveplan/api-auth-provider`](./api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
| [`@suaveplan/api-auth-rolled`](./api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
| [`@suaveplan/api-auth-static-principal`](./api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
| [`@suaveplan/api-db-adapter`](./api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
| [`@suaveplan/api-db-sqlite-memory`](./api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
| [`@suaveplan/api-db-worker-proxy`](./api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
| [`@suaveplan/api-events-in-memory`](./api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
| [`@suaveplan/api-events-worker-proxy`](./api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
| [`@suaveplan/api-graphql`](./api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
| [`@suaveplan/api-graphql-apollo`](./api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
| [`@suaveplan/api-graphql-ui-altair`](./api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
| [`@suaveplan/api-graphql-ui-graphiql`](./api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
| [`@suaveplan/api-graphql-yoga`](./api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
| [`@suaveplan/api-host`](./api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
| [`@suaveplan/api-jobs-worker-proxy`](./api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
| [`@suaveplan/api-lifecycle`](./api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
| [`@suaveplan/api-loader`](./api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-logger-worker-proxy`](./api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
| [`@suaveplan/api-manifest`](./api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
| [`@suaveplan/api-migrations`](./api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
| [`@suaveplan/api-openapi`](./api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
| [`@suaveplan/api-openapi-ui-redoc`](./api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
| [`@suaveplan/api-openapi-ui-scalar`](./api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
| [`@suaveplan/api-openapi-ui-swagger`](./api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
| [`@suaveplan/api-plugin`](./api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
| [`@suaveplan/api-plugin-audit`](./api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
| [`@suaveplan/api-plugin-auth`](./api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
| [`@suaveplan/api-plugin-campaigns`](./api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
| [`@suaveplan/api-plugin-cms`](./api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
| [`@suaveplan/api-plugin-database`](./api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
| [`@suaveplan/api-plugin-event-bus`](./api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
| [`@suaveplan/api-plugin-events`](./api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
| [`@suaveplan/api-plugin-example`](./api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
| [`@suaveplan/api-plugin-export`](./api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
| [`@suaveplan/api-plugin-file-upload`](./api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
| [`@suaveplan/api-plugin-graphql`](./api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
| [`@suaveplan/api-plugin-import`](./api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
| [`@suaveplan/api-plugin-notifications`](./api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
| [`@suaveplan/api-plugin-payment`](./api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
| [`@suaveplan/api-plugin-rate-limit`](./api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
| [`@suaveplan/api-plugin-search`](./api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
| [`@suaveplan/api-plugin-site`](./api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
| [`@suaveplan/api-plugin-sse`](./api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
| [`@suaveplan/api-plugin-tenant`](./api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
| [`@suaveplan/api-plugin-user`](./api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
| [`@suaveplan/api-plugin-vault`](./api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
| [`@suaveplan/api-plugin-webhook`](./api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
| [`@suaveplan/api-rate-limit`](./api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
| [`@suaveplan/api-rbac`](./api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
| [`@suaveplan/api-router`](./api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
| [`@suaveplan/api-router-hono`](./api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
| [`@suaveplan/api-router-in-memory`](./api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
| [`@suaveplan/api-router-worker-bridge`](./api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
| [`@suaveplan/api-saga`](./api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
| [`@suaveplan/api-sandbox-permissions`](./api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
| [`@suaveplan/api-sandbox-vm`](./api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
| [`@suaveplan/api-sandbox-wasm`](./api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
| [`@suaveplan/api-schemas`](./api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
| [`@suaveplan/api-secrets`](./api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
| [`@suaveplan/api-secrets-bun`](./api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
| [`@suaveplan/api-secrets-env`](./api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
| [`@suaveplan/api-secrets-vault`](./api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
| [`@suaveplan/api-secrets-worker-proxy`](./api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
| [`@suaveplan/api-sse`](./api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
| [`@suaveplan/api-sse-bun`](./api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
| [`@suaveplan/api-tenant`](./api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
| [`@suaveplan/api-tenant-provider`](./api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
| [`@suaveplan/api-tenant-provider-memory`](./api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
| [`@suaveplan/api-tenant-resolver-header`](./api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
| [`@suaveplan/api-tenant-resolver-jwt`](./api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
| [`@suaveplan/api-types`](./api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
| [`@suaveplan/api-worker-host`](./api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
| [`@suaveplan/api-worker-rpc`](./api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
| [`@suaveplan/api-ws`](./api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
| [`@suaveplan/api-ws-broker-in-memory`](./api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
| [`@suaveplan/api-ws-broker-redis`](./api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
| [`@suaveplan/api-ws-bun`](./api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
| [`@suaveplan/wasm-compiler-javy`](./wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.

View File

@@ -0,0 +1,109 @@
# @suaveplan/api-2pc - Two-phase commit coordinator for the Suaveplan API plugin system.
Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered participants with a host-owned ledger in suaveplan_2pc_transactions. Refuses non-preparable engines as writers (SQLite, SurrealDB) but accepts them as read-only voters. Default timeoutMs 5000, hard ceiling 30000, default outstanding-prepared cap 256, all operator-tunable. Ships boot-time orphan resolution via pg_prepared_xacts, the full TWOPC_* error namespace, OTel transaction/prepare/commit/rollback spans plus prepare_duration_ms / commit_duration_ms histograms / transactions_total counter / outstanding_prepared gauge, and run2pcParticipantConformance on the ./testing subpath. Server-tier — Bun + Node.
> **Postgres-only writer surface.** SQLite and SurrealDB cannot honour
> `PREPARE TRANSACTION` and are refused as writers at install time.
> They MAY participate as read-only voters that contribute a
> `commit`/`abort` vote based on their own consistency check, but the
> coordinator never sends them prepare / commit / rollback SQL.
<!-- begin:auto-docs -->
## Documentation tree
- [coordinator](./src/coordinator/coordinator.md) — Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator` builds a fully-wired coordinator that holds a pa…
- [coordinator](./src/coordinator/coordinator.md) — Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator` builds a fully-wired coordinator that holds a pa…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the `TWOPC_*` error-code namespace and the `TwoPcError` concrete class for the `@suaveplan/api-2pc` pa…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the `TWOPC_*` error-code namespace and the `TwoPcError` concrete class for the `@suaveplan/api-2pc` pa…
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument registry the 2PC coordinator emits against on every transaction. Each instrument's name foll…
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument registry the 2PC coordinator emits against on every transaction. Each instrument's name foll…
- [outstanding-cap](./src/limits/outstanding-cap.md) — Admission control for in-flight 2PC transactions. The coordinator holds a fixed-size budget of "outstanding prepared tr…
- [timeout-ceiling](./src/limits/timeout-ceiling.md) — Enforces the hard 30 000 ms ceiling on caller-supplied `timeoutMs` values. The check fires at the public entry point of…
- [options](./src/options/options.md) — Defines the boundary-validated option shape consumed by `createTwoPcCoordinator`. Every tunable that affects coordinato…
- [options](./src/options/options.md) — Defines the boundary-validated option shape consumed by `createTwoPcCoordinator`. Every tunable that affects coordinato…
- [persistence](./src/persistence/persistence.md) — Persists every coordinator state transition into the `suaveplan_2pc_transactions` ledger table. The table is owned by `…
- [persistence](./src/persistence/persistence.md) — Persists every coordinator state transition into the `suaveplan_2pc_transactions` ledger table. The table is owned by `…
- [commit](./src/phases/commit.md) — Phase-2 commit for a single participant. Once every participant has returned `commit` from phase 1 and the coordinator…
- [gid](./src/phases/gid.md) — Builds and parses the global transaction id (gid) string passed to Postgres' `PREPARE TRANSACTION '<gid>'`. The format…
- [prepare](./src/phases/prepare.md) — Implements phase 1 of the 2PC protocol for a single participant. The coordinator iterates participants and calls this m…
- [rollback](./src/phases/rollback.md) — Phase-2 rollback for a single participant. Used both by the coordinator's abort path (vote_abort or prepare failure) an…
- [recovery](./src/recovery/recovery.md) — Boot-time orphan resolution for the 2PC coordinator. When a host crashes between phase 1 and phase 2, prepared transact…
- [recovery](./src/recovery/recovery.md) — Boot-time orphan resolution for the 2PC coordinator. When a host crashes between phase 1 and phase 2, prepared transact…
- [registry](./src/registry/participant-registry.md) — The participant registry is the install-time admission control between plugins and the 2PC coordinator. Plugins declare…
- [participant-registry](./src/registry/participant-registry.md) — The participant registry is the install-time admission control between plugins and the 2PC coordinator. Plugins declare…
- [testing](./src/testing/conformance.md) — Provides `run2pcParticipantConformance(factory)` — a runnable test suite that pins the contract every 2PC writer partic…
- [conformance](./src/testing/conformance.md) — Provides `run2pcParticipantConformance(factory)` — a runnable test suite that pins the contract every 2PC writer partic…
<!-- end:auto-docs -->
## Why
Some cross-database workflows demand atomic visibility — no partial
state, no eventual convergence. Per the API Plugin Foundation design
doc §9.2 the coordinator runs phase-1 prepare across every registered
participant, persists the vote tally to `suaveplan_2pc_transactions`
on the host's metadata DB, then issues `COMMIT PREPARED` or
`ROLLBACK PREPARED` against every writer participant. Recovery on the
next boot resolves any prepared transactions left over from a host
crash.
## Modules
- `coordinator/` — `createTwoPcCoordinator` + `run2pc` factory and
state machine.
- `registry/` — `TwoPcParticipantRegistry` (admission control).
- `phases/` — `preparePhaseOne`, `commitPhaseTwo`, `rollbackPhaseTwo`,
`gid` helpers.
- `persistence/` — `TwoPcLedgerStore` over the host-owned
`suaveplan_2pc_transactions` table.
- `recovery/` — boot-time orphan resolution via `pg_prepared_xacts`.
- `limits/` — `OutstandingPreparedCap` (admission), `assertTimeoutBelowCeiling`
(per-call ceiling).
- `instruments/` — OTel meter/tracer bundle.
- `options/` — Zod-validated coordinator options + defaults.
- `error-codes/` — 10 `TWOPC_*` codes + `TwoPcError`.
- `types/` — `Run2PcInput`, `Run2PcOutcome`, participant + state types.
- `testing/` — `run2pcParticipantConformance(factory)` suite.
## Quick start
```ts
import { createTwoPcCoordinator } from "@suaveplan/api-2pc";
const coordinator = createTwoPcCoordinator({
options: { hostInstanceId: "host-01" },
registry,
ledgerDb,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
});
const outcome = await coordinator.run2pc({
participants: [
{ id: "billing", input: { invoiceId: "inv-1" } },
{ id: "inventory", input: { sku: "abc-9" } },
],
});
// outcome.state is "committed" | "aborted" | "in_doubt"
```
## Defaults
- `timeoutMs`: **5000** (operator-tunable per-call up to the
30000 ms ceiling).
- `outstandingPreparedCap`: **256** (operator-tunable via host
config).
## Error namespace
Every production code path throws `TwoPcError` with one of the ten
`TWOPC_*` codes. See `src/error-codes/error-codes.md` for the full
catalogue.
## Conformance
Every adapter implementing a writer participant MUST run
`run2pcParticipantConformance({ create: ... })` in a
`tests/conformance/run-2pc-participant.test.ts` file. The harness
pins prepare / commit / rollback semantics and the idempotency
contract.

View File

@@ -0,0 +1,150 @@
# `coordinator/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator`
builds a fully-wired coordinator that holds a participant registry, a
ledger store, an admission counter, and an OTel instrument bundle.
The `run2pc({ participants, timeoutMs? })` entry point owns the full
state machine described in design doc §9.2:
1. Admission via the outstanding-prepared cap.
2. Ledger insert at `voting`.
3. Phase-1 prepare per participant, with the
`2pc.prepare_duration_ms` histogram and a `2pc.prepare` span per
participant.
4. Either unanimous commit → `markAllVotedCommit` → phase-2
`COMMIT PREPARED` per writer; OR any abort/error →
`markAborting` → phase-2 `ROLLBACK PREPARED` per prepared writer.
5. Terminal ledger stamp (`committed` / `aborted` / `in_doubt`).
The coordinator is the only path application code ever touches.
Every smaller module (phases, persistence, recovery, limits) is
internal plumbing this factory composes.
## Features
- Single public surface: `createTwoPcCoordinator(deps)` returns
`{ run2pc, outstandingCount, options }`.
- Default `timeoutMs` from options (5000), per-call override accepted
up to the 30000 ceiling (enforced via
`limits/timeout-ceiling`).
- Admission cap (default 256) enforced via
`limits/outstanding-cap`; over-cap calls fail with
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` before any participant is
contacted.
- Hooks every documented OTel instrument (`2pc.transaction` span,
`2pc.prepare` / `2pc.commit` / `2pc.rollback` child spans,
`2pc.transactions_total` counter, `prepare`/`commit_duration_ms`
histograms, `outstanding_prepared` gauge).
- Ledger persistence is bidirectional — every state transition writes
through `TwoPcLedgerStore`, so the recovery runner has a full audit
trail on restart.
- Abort handling: vote_abort, prepare_failed, and timeout all funnel
to the rollback branch; rollback errors escalate to `in_doubt`
rather than failing the rollback path itself.
## Usage: Basic
```ts
import { createTwoPcCoordinator } from "@suaveplan/api-2pc/coordinator";
const coordinator = createTwoPcCoordinator({
options: { hostInstanceId: "host-01" },
registry,
ledgerDb,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
});
const outcome = await coordinator.run2pc({
participants: [
{ id: "billing", input: { invoiceId: "inv-1" } },
{ id: "inventory", input: { sku: "abc-9" } },
],
});
console.log(outcome.state); // "committed" | "aborted" | "in_doubt"
```
## Usage: Advanced
Inject a custom instruments bundle to feed a hand-rolled
MeterProvider, override the default timeout per call, and consume the
per-participant vote summary on the outcome:
```ts
import { createTwoPcCoordinator } from "@suaveplan/api-2pc/coordinator";
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
const instruments = createTwoPcInstruments({
meterProvider,
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
});
const coordinator = createTwoPcCoordinator({
options: {
hostInstanceId: "host-01",
defaultTimeoutMs: 10_000,
outstandingPreparedCap: 64,
},
registry,
ledgerDb,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
instruments,
});
const outcome = await coordinator.run2pc({
participants: [
{ id: "billing", input: { invoiceId } },
{ id: "audit", input: { event: "invoice.created" } },
],
timeoutMs: 2_500,
});
for (const v of outcome.votes) {
auditLog.append({
txId: outcome.txId,
participant: v.participantId,
vote: v.vote,
});
}
```
## API Reference
- `interface TwoPcCoordinatorDeps`
- `options: TwoPcCoordinatorOptions`
- `registry: TwoPcParticipantRegistry`
- `ledgerDb: LedgerDb`
- `clock: Clock`
- `idGenerator: IdGenerator`
- `instruments?: TwoPcInstruments`
- `interface TwoPcCoordinator`
- `run2pc(input: Run2PcInput): Promise<Run2PcOutcome>`
- `outstandingCount(): number`
- `options: ResolvedTwoPcCoordinatorOptions`
- `createTwoPcCoordinator(deps): TwoPcCoordinator` — factory.
## Implementation Notes
The function is structured as one large `try` / `finally` so cleanup
(span end, deadline timer clear, admission release) runs regardless
of which branch fires. The pattern keeps the happy path linear and
the failure paths exhaustive without nested try blocks.
The "any commit failure → in_doubt" escalation is deliberate. Once
the coordinator has issued `COMMIT PREPARED` against at least one
participant, the only correct follow-up is to keep trying — but the
hot path cannot block on that. Instead, the row is marked `in_doubt`,
the OTel counter ticks the `in_doubt` outcome, and the recovery
runner picks it up on the next boot.
Vote-abort and prepare-failed paths both call `ac.abort()` BEFORE
running phase 2, so any prepared participants whose subsequent SQL
hits the signal map to `TWOPC_TIMEOUT` rather than getting stuck. The
deadline timer is cleared in the `finally` so a successful commit
does not leave a dangling abort handle on the AbortController.

View File

@@ -0,0 +1,97 @@
# `error-codes/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the `TWOPC_*` error-code namespace and the `TwoPcError`
concrete class for the `@suaveplan/api-2pc` package. The registry is the
canonical contract every production code path in the coordinator throws
against — observability dashboards, support runbooks, and integrating
plugins all join on these stable string codes.
The codes are intentionally enumerated up-front rather than grown
ad-hoc: a missing code is a spec deviation that a CI gate can detect,
and the closed set lets operators write alerting rules without
constantly chasing schema drift.
## Features
- Ten stable codes (`TIMEOUT`, `VOTE_ABORT`, `PREPARE_FAILED`,
`COMMIT_FAILED`, `ROLLBACK_FAILED`, `IN_DOUBT`,
`PARTICIPANT_NOT_PREPARABLE`, `READ_ONLY_PARTICIPANT_CANNOT_WRITE`,
`OUTSTANDING_LIMIT_EXCEEDED`, `TIMEOUT_CEILING_EXCEEDED`).
- Each code is exposed via `TWOPC_CODES.<NAME>` returning the prefixed
string `"TWOPC.<NAME>"` produced by `defineCodes`.
- `TwoPcError` extends `SuaveplanError` so cause-chain traversal,
Zod-validated wire format, and the OTel span-event adapter from
`@suaveplan/error` all apply unchanged.
## Usage: Basic
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
throw new TwoPcError(
TWOPC_CODES.TIMEOUT,
"Phase-1 deadline elapsed",
{ context: { txId, timeoutMs: 5000 } }
);
```
## Usage: Advanced
Branching on error code at the boundary between coordinator and caller
is the recommended consumer pattern. The narrowed `twoPcCode` field
gives an exhaustive switch the compiler can check:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
try {
await run2pc({ participants, timeoutMs: 5000 }, ctx);
} catch (err) {
if (err instanceof TwoPcError) {
switch (err.twoPcCode) {
case TWOPC_CODES.TIMEOUT:
metrics.incr("billing.2pc.timeout");
return retryWithBackoff();
case TWOPC_CODES.VOTE_ABORT:
return surfaceUserError();
case TWOPC_CODES.IN_DOUBT:
pageOncall(err);
return;
default:
throw err;
}
}
throw err;
}
```
## API Reference
- `TWOPC_CODES` — frozen object `{ TIMEOUT: "TWOPC.TIMEOUT", ... }`.
- `type TwoPcErrorCode` — string-literal union of every value in
`TWOPC_CODES`.
- `class TwoPcError extends SuaveplanError<TwoPcErrorCode>` — exposes
`twoPcCode` as a same-typed accessor for ergonomic narrowing without
re-asserting at every catch site.
## Implementation Notes
`defineCodes` from `@suaveplan/error` produces the prefixed values at
runtime, so every code string starts with `"TWOPC."`. This matches the
prefix convention used by sibling api-stack packages (`HOST.*`,
`PLUGIN.*`, `RELOAD.*`, `LEDGER.*`). The `TwoPcError` class carries a
redundant `twoPcCode` field — duplicating `code` — purely to give
callers a name they don't have to import the generic
`SuaveplanError<C>` type to narrow against. The duplication is
deliberate and harmless because the base class makes `code` readonly.
The registry is closed; adding a code requires an OpenSpec change so
every consumer dashboard and runbook stays in sync. Coverage assertions
ensure every code is actually thrown by a production path before this
package can graduate to `1.0.0`.

View File

@@ -0,0 +1,105 @@
# `instruments/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Builds the OTel instrument registry the 2PC coordinator emits against
on every transaction. Each instrument's name follows design doc §9.2
exactly so a missing entry is a spec deviation a CI gate could detect.
Centralising creation keeps the coordinator code free of meter / tracer
plumbing — every phase module simply records against the frozen
bundle returned here.
The observable gauge wires into the coordinator's outstanding-prepared
counter via a snapshot callback, so dashboards see the live admission
state without the coordinator pushing on every change.
## Features
- One span: `2pc.transaction` per transaction lifetime; per-phase
child spans `2pc.prepare`, `2pc.commit`, `2pc.rollback` (started by
the phase modules themselves, using the tracer this bundle exposes).
- One counter: `2pc.transactions_total` labelled `{ outcome }`.
- Two histograms: `2pc.prepare_duration_ms`, `2pc.commit_duration_ms`
labelled `{ outcome }`.
- One observable gauge: `2pc.outstanding_prepared` wired to a
snapshot callback supplied at construction.
- Provider defaults to the OTel globals (no-op when no SDK is
installed) so unit tests do not have to wire a provider.
## Usage: Basic
```ts
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
const instruments = createTwoPcInstruments({
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
});
instruments.transactionsTotal.add(1, { outcome: "committed" });
```
## Usage: Advanced
Inject a real OTel SDK and a custom snapshot to feed a live dashboard:
```ts
import { metrics, trace } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
const meterProvider = new MeterProvider({ /* readers/exporters */ });
metrics.setGlobalMeterProvider(meterProvider);
const instruments = createTwoPcInstruments({
meterProvider,
tracerProvider: trace.getTracerProvider(),
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
});
const span = instruments.tracer.startSpan("2pc.transaction", {
attributes: { "tx.id": txId, participant_count: 2 },
});
try {
// ...
instruments.prepareDurationMs.record(prepareElapsedMs, {
outcome: "committed",
});
} finally {
span.end();
}
```
## API Reference
- `type TwoPcOutcome = "committed" | "aborted" | "in_doubt"` — values
attached to the `outcome` attribute on every counter / histogram
observation.
- `interface TwoPcInstrumentsConfig`
- `meterProvider?: MeterProvider`
- `tracerProvider?: TracerProvider`
- `outstandingPreparedSnapshot: () => number`
- `interface TwoPcInstruments` — frozen bundle exposing `meter`,
`tracer`, `transactionsTotal`, `prepareDurationMs`,
`commitDurationMs`, `outstandingPreparedGauge`.
- `createTwoPcInstruments(config): TwoPcInstruments` — factory.
## Implementation Notes
The observable gauge's callback fires whenever the SDK collects metrics
(typically on a fixed interval driven by the exporter). The snapshot
returned by `outstandingPreparedSnapshot` is therefore an
eventually-consistent value — operators must not treat it as a strict
upper bound for admission decisions. The strict upper bound is enforced
inside `limits/outstanding-cap.ts`; the gauge is purely a dashboard
signal.
Histogram unit is set explicitly to `"ms"` per OTel naming convention.
The two histograms are named after the phases they measure so a single
dashboard panel can render side-by-side latency for prepare and commit
without re-labelling — operators flagging long prepares typically
care more about phase-1 latency because that is when row-level locks
are held on every participant.

View File

@@ -0,0 +1,104 @@
# `limits/outstanding-cap`
## Purpose
Admission control for in-flight 2PC transactions. The coordinator
holds a fixed-size budget of "outstanding prepared transactions"; once
the budget is exhausted new `run2pc` calls refuse rather than queue.
The default is 256 (design doc §14's open question, pinned by this
package's spec) and operators can tune it via host config.
Refusing rather than queuing is intentional: Postgres holds row-level
locks for every prepared transaction, so a backed-up coordinator
would otherwise convert a transient overload into a global lock
storm. Failing fast lets callers route to a degraded path
(degraded-write, retry queue, etc.).
## Features
- `OutstandingPreparedCap` — synchronous counter with `admit()`,
`release()`, `snapshot()`, `capacity()`.
- Constructor validates that the cap is a positive integer (rejects
zero, negatives, fractions, `NaN`).
- `release()` is idempotent below zero — a buggy double-release in
caller code cannot silently expand the real cap.
- Synchronous-only — relies on the event-loop atomicity guarantee
between `admit` / `release` pairs in JavaScript runtimes.
## Usage: Basic
```ts
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
const cap = new OutstandingPreparedCap(256);
cap.admit();
try {
// run the 2PC transaction
} finally {
cap.release();
}
```
## Usage: Advanced
Wrap admission inside the coordinator's public entry to centralise the
release in one `finally`. The snapshot fuels the OTel gauge:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
class MiniCoordinator {
private readonly cap = new OutstandingPreparedCap(256);
snapshot() {
return this.cap.snapshot();
}
async run<T>(fn: () => Promise<T>): Promise<T> {
try {
this.cap.admit();
} catch (err) {
if (
err instanceof TwoPcError &&
err.twoPcCode === TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED
) {
// route to degraded path
throw err;
}
throw err;
}
try {
return await fn();
} finally {
this.cap.release();
}
}
}
```
## API Reference
- `class OutstandingPreparedCap`
- `constructor(cap: number)` — throws
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` on invalid `cap`.
- `admit(): void` — throws
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` when at capacity.
- `release(): void` — idempotent below zero.
- `snapshot(): number` — current outstanding count.
- `capacity(): number` — configured cap.
## Implementation Notes
Construction-time validation re-uses the same error code as the
runtime cap-exceeded path. This is deliberate: operators tuning the
cap via host config want one error string to alert on, regardless of
whether the misconfiguration was a fractional value at startup or a
real overload at steady state. The context object on the thrown
`TwoPcError` distinguishes the two cases (`cap` only vs.
`cap` + `outstanding`).
The class is intentionally tiny — no async, no internal queue, no
external dependencies. Adding a queue here would re-introduce the
lock-storm risk the cap exists to prevent.

View File

@@ -0,0 +1,77 @@
# `limits/timeout-ceiling`
## Purpose
Enforces the hard 30 000 ms ceiling on caller-supplied `timeoutMs`
values. The check fires at the public entry point of `run2pc` so
above-ceiling calls never contact a participant; the operational
warning in design doc §9.2 is that long prepares hold row-level locks,
so surfacing the misuse loudly is more useful than silently clamping.
This module is intentionally tiny — one function — because the
behaviour is small but the gate is one of the most-asserted contracts
in the spec.
## Features
- `assertTimeoutBelowCeiling(timeoutMs)` — throws
`TWOPC_TIMEOUT_CEILING_EXCEEDED` for above-ceiling, non-positive,
non-finite, or `NaN` inputs.
- Re-uses the `TIMEOUT_CEILING_MS` constant from `options/` so the
ceiling stays a single source of truth.
## Usage: Basic
```ts
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
assertTimeoutBelowCeiling(5000); // ok
assertTimeoutBelowCeiling(30001); // throws TWOPC_TIMEOUT_CEILING_EXCEEDED
```
## Usage: Advanced
The function is intentionally side-effect-free so callers can wrap it
in a per-tenant override path:
```ts
import { TIMEOUT_CEILING_MS } from "@suaveplan/api-2pc/options";
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
function resolveTimeout(
requested: number | undefined,
tenantMax: number
): number {
const effective = requested ?? DEFAULT_TIMEOUT_MS;
assertTimeoutBelowCeiling(effective);
if (effective > tenantMax) {
return tenantMax;
}
if (effective > TIMEOUT_CEILING_MS) {
// unreachable given the assert above — included only to
// illustrate that the ceiling acts as the strict upper bound.
return TIMEOUT_CEILING_MS;
}
return effective;
}
```
## API Reference
- `function assertTimeoutBelowCeiling(timeoutMs: number): void`
- Throws `TwoPcError(TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED)` for any
of: `NaN`, `Infinity`, `-Infinity`, values `<= 0`, values
`> TIMEOUT_CEILING_MS`.
## Implementation Notes
The check rejects non-positive values for the same reason it rejects
above-ceiling ones: an immediate or backwards-in-time deadline would
collapse phase 1 to a single tick and the rollback path would fire
before any participant could finish a real prepare. Treating that as
the same misuse class as "30 minutes" is a deliberate ergonomic
choice — operators get one error code to grep for.
`Number.isFinite` is used in preference to `Number.isNaN` because the
former covers `Infinity` and `-Infinity` in addition to `NaN`. The
explicit `<= 0` check then excludes zero and negative-finite values.

View File

@@ -0,0 +1,96 @@
# `options/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the boundary-validated option shape consumed by
`createTwoPcCoordinator`. Every tunable that affects coordinator
behaviour (host instance id, outstanding-prepared cap, default phase-1
timeout) lives here, with Zod validation at the seam and explicit
defaults pinned to the design doc §9.2 numbers.
Centralising option resolution in one module keeps the coordinator
free of conditional fallback logic — by the time it sees the resolved
shape every field is concrete, so subsequent code never has to
re-check `undefined`.
## Features
- `DEFAULT_TIMEOUT_MS = 5000` — phase-1 deadline default per design
doc §9.2.
- `TIMEOUT_CEILING_MS = 30000` — hard ceiling enforced at call time
(the coordinator's `limits/timeout-ceiling` module imports this).
- `DEFAULT_OUTSTANDING_PREPARED_CAP = 256` — admission-control default
per design doc §14.
- `twoPcCoordinatorOptionsSchema` — Zod schema, `strict()` so typos
surface as Zod errors instead of being silently ignored.
- `resolveTwoPcCoordinatorOptions(raw)` — validates and fills in
defaults; returns the fully-populated `ResolvedTwoPcCoordinatorOptions`
shape the coordinator consumes.
## Usage: Basic
```ts
import {
DEFAULT_TIMEOUT_MS,
resolveTwoPcCoordinatorOptions,
} from "@suaveplan/api-2pc/options";
const resolved = resolveTwoPcCoordinatorOptions({
hostInstanceId: "host-01",
});
console.log(resolved.defaultTimeoutMs === DEFAULT_TIMEOUT_MS); // true
```
## Usage: Advanced
Override every default — useful for canary hosts where operators want
a tighter admission cap and a longer prepare window:
```ts
import { resolveTwoPcCoordinatorOptions } from "@suaveplan/api-2pc/options";
const resolved = resolveTwoPcCoordinatorOptions({
hostInstanceId: "canary-eu-west-1",
outstandingPreparedCap: 64,
defaultTimeoutMs: 15_000,
});
```
Calls supplying `defaultTimeoutMs` above `TIMEOUT_CEILING_MS` raise a
Zod error at this boundary — the coordinator's runtime ceiling check
in `limits/timeout-ceiling.ts` then surfaces the misuse on per-call
overrides as well.
## API Reference
- `const DEFAULT_TIMEOUT_MS: number` — 5000.
- `const TIMEOUT_CEILING_MS: number` — 30000.
- `const DEFAULT_OUTSTANDING_PREPARED_CAP: number` — 256.
- `interface TwoPcCoordinatorOptions` — caller-supplied input shape.
- `interface ResolvedTwoPcCoordinatorOptions` — internal shape after
defaults.
- `const twoPcCoordinatorOptionsSchema: z.ZodType` — strict-shape
Zod schema.
- `function resolveTwoPcCoordinatorOptions(raw): ResolvedTwoPcCoordinatorOptions`
— validates and resolves defaults.
## Implementation Notes
The schema uses `.strict()` so unknown keys fail validation rather than
silently being dropped. The default for `defaultTimeoutMs` is applied
post-parse with `?? DEFAULT_TIMEOUT_MS` rather than via
`z.default(...)` so the schema continues to reject above-ceiling
values regardless of whether the caller supplied the field — Zod's
`default` would short-circuit `.max()` checks when the field is
omitted, which is the wrong shape for a ceiling-enforced option.
The module deliberately does NOT re-throw Zod failures as
`TwoPcError` — that's the responsibility of the coordinator's
public entry points. Keeping this module pure of cross-package error
mapping makes it reusable from tests that want a raw Zod surface for
property-based shape testing.

View File

@@ -0,0 +1,114 @@
# `persistence/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Persists every coordinator state transition into the
`suaveplan_2pc_transactions` ledger table. The table is owned by
`@suaveplan/api-host` (migration id 2, `saga-2pc-reserved`); this
module only reads and writes rows so the host stays the single source
of truth for schema evolution.
The ledger is what makes 2PC durable across coordinator restarts.
Without it, a host that crashes mid-prepare would lose track of which
participants were `PREPARE TRANSACTION`'d and could not safely choose
between commit and rollback on the next boot.
## Features
- `TwoPcLedgerStore` — CRUD facade over `suaveplan_2pc_transactions`
with statements for every state transition (`insert`,
`markAllVotedCommit`, `markAborting`, `markCommitted`, `markAborted`,
`markInDoubt`).
- `findById` — single-row lookup for state inspection.
- `findRecoverable` — scan returning every non-terminal row for the
current host instance id. Drives `recovery/`.
- `TwoPcLedgerStore.decodeParticipants` — static helper that parses the
`participants_json` blob back into a typed array.
- Structural `LedgerDb` / `LedgerStatement` interfaces so the store
works with `bun:sqlite`, `@suaveplan/db-sqlite`'s thin wrapper, and
`better-sqlite3` without any concrete dependency.
## Usage: Basic
```ts
import { Database } from "bun:sqlite";
import { TwoPcLedgerStore } from "@suaveplan/api-2pc/persistence";
const db = new Database(":memory:");
// Schema is created by api-host's migration runner — see migrations.ts
// migration id 2 (`saga-2pc-reserved`). In tests, run the host runner
// against the same database.
const store = new TwoPcLedgerStore(db as never);
store.insert({
id: "tx-1",
coordinator: "host-instance-01",
participants: [
{ participantId: "billing", voterMode: "writer", vote: null },
],
});
```
## Usage: Advanced
Walk the recovery candidates and decode each blob — the typical shape
used inside `recovery/recovery.ts`:
```ts
import { TwoPcLedgerStore } from "@suaveplan/api-2pc/persistence";
function listRecoverable(store: TwoPcLedgerStore, hostId: string) {
const rows = store.findRecoverable(hostId);
return rows.map((row) => ({
id: row.id,
state: row.state,
participants: TwoPcLedgerStore.decodeParticipants(row),
}));
}
```
The static `decodeParticipants` is deliberately exposed so callers
working with raw rows (e.g. ad-hoc admin scripts) do not have to
recreate the JSON contract themselves.
## API Reference
- `interface LedgerStatement` / `interface LedgerDb` — structural
SQLite-shaped views.
- `interface TwoPcLedgerRow` — exact row shape returned by `findById`
/ `findRecoverable`.
- `interface LedgerParticipantEntry` — per-participant entry in
`participants_json`.
- `class TwoPcLedgerStore`
- `constructor(db: LedgerDb)`
- `insert({ id, coordinator, participants })`
- `markAllVotedCommit({ id, participants, preparedAt })`
- `markAborting({ id, participants })`
- `markCommitted(id, committedAt)`
- `markAborted(id, abortedAt)`
- `markInDoubt(id)`
- `findById(id): TwoPcLedgerRow | null`
- `findRecoverable(coordinator): readonly TwoPcLedgerRow[]`
- `static decodeParticipants(row): readonly LedgerParticipantEntry[]`
## Implementation Notes
Every UPDATE uses a single statement with the new `state` value
matched against `TwoPcCoordinatorState`'s string-literal union — the
`satisfies TwoPcCoordinatorState` annotation catches typos at compile
time. The store does NOT enforce a state machine on top of the table;
that responsibility belongs to the coordinator. Keeping the
persistence layer state-machine-free lets recovery use the same store
without conflating "what changed in this lifetime" with "what the row
looked like after a crash".
The `participants_json` blob is stored as a JSON string rather than a
side table — the coordinator never queries by participant id, only by
transaction id, so a JSON blob has zero indexing cost and survives
schema evolution painlessly. `decodeParticipants` returns an empty
array when the blob is not an array — defensive against rows
hand-written by ad-hoc admin scripts.

View File

@@ -0,0 +1,94 @@
# `phases/commit`
## Purpose
Phase-2 commit for a single participant. Once every participant has
returned `commit` from phase 1 and the coordinator has persisted the
`all-voted-commit` state to the ledger, this module emits
`COMMIT PREPARED '<gid>'` against each writer participant and records
`2pc.commit_duration_ms`.
Phase 2 is intentionally unbounded by timeout — the protocol cannot
tolerate giving up on a commit-prepared mid-flight, because doing so
would leave the system in `in_doubt` state and require the recovery
runner to resolve it on the next boot.
## Features
- Skips participants that did not actually prepare (`outcome.prepared
=== false`) — read-only voters and abort-voters never see commit
SQL.
- Emits a `2pc.commit` span with `tx.id`, `participant.id`, and `gid`
attributes.
- Records `2pc.commit_duration_ms` tagged with `outcome: "committed"`
on success.
- Failure preserves the underlying cause on a thrown
`TwoPcError(TWOPC_COMMIT_FAILED)`; the coordinator escalates to
`TWOPC_IN_DOUBT` once it has tried every participant.
## Usage: Basic
```ts
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
await commitPhaseTwo({
txId,
participant,
outcome: preparedOutcome,
instruments,
});
```
## Usage: Advanced
The coordinator collects per-participant failures rather than
short-circuiting — every participant gets attempted exactly once so
operators can see the full picture in the ledger:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
const failures: TwoPcError[] = [];
for (const outcome of preparedOutcomes) {
if (!outcome.prepared) continue;
try {
await commitPhaseTwo({
txId,
participant: registry.get(outcome.participantId)!,
outcome,
instruments,
});
} catch (err) {
if (err instanceof TwoPcError) failures.push(err);
else throw err;
}
}
if (failures.length > 0) {
// ledger.markInDoubt(txId); recovery resolves on next boot
}
```
## API Reference
- `interface CommitPhaseInput`
- `txId: string`
- `participant: RegisteredTwoPcParticipant`
- `outcome: ParticipantPrepareOutcome` — the phase-1 result.
- `instruments: TwoPcInstruments`
- `function commitPhaseTwo(input): Promise<void>`
- no-op for `outcome.prepared === false`
- throws `TwoPcError(TWOPC_COMMIT_FAILED)` on SQL failure.
## Implementation Notes
The gid stored on the prepare outcome is the single source of truth —
this module never rebuilds it. Reusing the prepare-time value
defends against the unlikely case where a host-config change between
phases would have produced a different gid: the SQL the coordinator
needs to send is exactly the one it prepared with.
The SQL escape (`replace(/'/g, "''")`) matches the prepare-phase
treatment so a gid containing the unlikely quote character would
still produce valid SQL. The escape is cheap; the defence-in-depth
costs nothing on the happy path.

View File

@@ -0,0 +1,85 @@
# `phases/gid`
## Purpose
Builds and parses the global transaction id (gid) string passed to
Postgres' `PREPARE TRANSACTION '<gid>'`. The format embeds the host
instance id so the recovery scanner can filter `pg_prepared_xacts` to
only the rows this coordinator owns — without that anchor, two
coordinators sharing a Postgres host would clobber each other's
recovery decisions.
The module is intentionally pure: no SQL, no client. Centralising the
gid format here makes the recovery filter and the per-participant
prepare path share a single source of truth.
## Features
- `buildGid(hostInstanceId, txId, participantId)` — produces
`2pc-<host>-<tx>-<participant>`.
- `parseGid(gid, hostInstanceId)` — extracts `{ txId, participantId }`
for gids written by the supplied host; returns `null` otherwise so
recovery skips foreign prepared transactions silently. The
hostInstanceId is supplied by the caller (the recovery runner)
because the host id itself may contain hyphens and the parser cannot
recover it unambiguously from the gid alone.
- `gidPrefixFor(hostInstanceId)` — the prefix the recovery SQL filter
uses; centralised so SQL and parser never drift.
## Usage: Basic
```ts
import { buildGid, parseGid } from "@suaveplan/api-2pc/phases";
const gid = buildGid("host-01", "tx-abc", "billing");
// "2pc-host-01-tx-abc-billing"
const parts = parseGid(gid, "host-01");
// { txId: "tx-abc", participantId: "billing" }
```
## Usage: Advanced
Recovery uses the prefix filter to ignore prepared transactions
written by other components sharing the database:
```ts
import { gidPrefixFor, parseGid } from "@suaveplan/api-2pc/phases";
const prefix = gidPrefixFor("host-01");
const rows = await db.run<{ gid: string }>(
`SELECT gid FROM pg_prepared_xacts WHERE gid LIKE $1`,
[`${prefix}%`]
);
for (const row of rows) {
const parts = parseGid(row.gid, "host-01");
if (parts === null) continue; // foreign prepared transaction
// ...
}
```
## API Reference
- `buildGid(hostInstanceId: string, txId: string, participantId: string): string`
- `parseGid(gid: string, hostInstanceId: string): { txId, participantId } | null`
- `gidPrefixFor(hostInstanceId: string): string`
## Implementation Notes
`parseGid` takes the caller-supplied `hostInstanceId` and strips the
prefix `2pc-<host>-` before extracting `txId` and `participantId`.
The host instance id is supplied explicitly because hyphens are
allowed inside it (production hosts often look like `host-eu-west-1`)
and the parser cannot recover the boundary from the gid alone. The
recovery runner always knows its own host id, so this constraint is
ergonomic in practice.
Participant ids are constrained by the registry (and by host config)
to not contain hyphens, so the LAST hyphen after the prefix
unambiguously delimits the `participantId` from the `txId`.
The function returns `null` rather than throwing on unrecognised gids
because the recovery scan iterates over rows that may include prepared
transactions written by code outside this package's control — Postgres
has no participant-tag column, only the gid string itself, so the
shared-database scenario is a legitimate concern.

View File

@@ -0,0 +1,105 @@
# `phases/prepare`
## Purpose
Implements phase 1 of the 2PC protocol for a single participant. The
coordinator iterates participants and calls this module for each;
unanimous commit votes trigger phase 2, any abort or prepare failure
short-circuits to rollback. This module is the only place the
`PREPARE TRANSACTION '<gid>'` SQL is emitted, so the gid format
(`phases/gid`) and the SQL emission point stay co-located.
Phase 1 is the latency-sensitive half of 2PC: every participant holds
row-level locks for the duration. The module records
`2pc.prepare_duration_ms` so operators can spot creeping lock holds
before they cause throughput regressions.
## Features
- Runs the participant's `prepare` callback, then the
`PREPARE TRANSACTION` SQL when the vote is `commit` AND the
participant is a writer.
- Skips the SQL for read-only voters — they participate in the vote
only.
- Refuses a read-only voter that returned `writeIntent: true` with
`TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`.
- Honours an `AbortSignal` for the phase-1 deadline; aborted SQL is
remapped to `TWOPC_TIMEOUT`.
- Emits a `2pc.prepare` span around the whole call and records a
`2pc.prepare_duration_ms` observation tagged with the outcome.
- Failure cases preserve the underlying cause on the thrown
`TwoPcError`.
## Usage: Basic
```ts
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
const outcome = await preparePhaseOne({
hostInstanceId: "host-01",
txId,
participant,
payload: { invoiceId: "inv-1" },
instruments,
signal: ac.signal,
});
if (outcome.vote === "abort") {
// run rollback for every participant that voted commit
}
```
## Usage: Advanced
The coordinator runs this in a loop. Read-only voters interleave
freely with writers — the outcome's `prepared` flag tells phase 2
whether to send commit/rollback SQL:
```ts
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
const outcomes: ParticipantPrepareOutcome[] = [];
for (const p of registry.list()) {
const o = await preparePhaseOne({
hostInstanceId,
txId,
participant: p,
payload: payloadFor(p),
instruments,
signal,
});
outcomes.push(o);
if (o.vote === "abort") break; // short-circuit
}
// phase 2: commit only those with prepared === true
const writers = outcomes.filter((o) => o.prepared);
```
## API Reference
- `interface ParticipantPrepareOutcome` — `{ participantId, vote,
prepared, gid }`.
- `interface PreparePhaseInput` — `{ hostInstanceId, txId,
participant, payload, instruments, signal }`.
- `function preparePhaseOne(input): Promise<ParticipantPrepareOutcome>`
- throws `TWOPC_PREPARE_FAILED` when the participant callback or
SQL raises (signal not aborted),
- throws `TWOPC_TIMEOUT` when the signal aborted around the SQL,
- throws `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE` when a read-only
voter returned `writeIntent: true`.
## Implementation Notes
The SQL escape strategy is conservative: gids are built from the host
instance id, transaction id, and participant id — all three are
already validated at host config / id-generator / registry time — but
the function still calls `.replace(/'/g, "''")` before interpolating
into the `PREPARE TRANSACTION` statement. The runtime cost is zero on
the happy path and the defence-in-depth is cheap. The coordinator
never accepts user-supplied SQL fragments.
The span is started before the prepare callback so the participant's
own work shows up in the trace as a child of the `2pc.prepare` span if
the participant opens its own spans against the same tracer. The
`finally`-block `span.end()` guarantees the span is closed regardless
of which branch fires.

View File

@@ -0,0 +1,92 @@
# `phases/rollback`
## Purpose
Phase-2 rollback for a single participant. Used both by the
coordinator's abort path (vote_abort or prepare failure) and by the
recovery runner (orphan with no `all-voted-commit` ledger row).
The function is idempotent at the protocol level: Postgres returns
`42704` (`undefined_object`, "prepared transaction does not exist")
for a gid that was already rolled back, and this module catches that
specific error and treats it as a no-op. That property is what makes
recovery safe to re-run on every boot.
## Features
- Emits `ROLLBACK PREPARED '<gid>'` against the participant's
connection.
- Idempotent against Postgres `42704` — a second rollback against the
same gid is a no-op.
- Skips participants that did not actually prepare (`outcome.prepared
=== false`).
- Emits a `2pc.rollback` span with `tx.id`, `participant.id`, and
`gid` attributes.
- Failure preserves the underlying cause on a thrown
`TwoPcError(TWOPC_ROLLBACK_FAILED)`.
- Exports `isUnknownPreparedTransaction(cause)` so the recovery
runner can apply the same idempotency check without duplicating the
SQLSTATE sniff.
## Usage: Basic
```ts
import { rollbackPhaseTwo } from "@suaveplan/api-2pc/phases";
await rollbackPhaseTwo({
txId,
participant,
outcome: preparedOutcome,
instruments,
});
```
## Usage: Advanced
Recovery uses the same idempotency check directly to short-circuit
when the row was concurrently resolved:
```ts
import {
isUnknownPreparedTransaction,
rollbackPhaseTwo,
} from "@suaveplan/api-2pc/phases";
try {
await rollbackPhaseTwo({ txId, participant, outcome, instruments });
} catch (err) {
if (
err instanceof TwoPcError &&
err.twoPcCode === TWOPC_CODES.ROLLBACK_FAILED &&
isUnknownPreparedTransaction(err.cause)
) {
// race: another process already rolled this back — treat as success
} else {
throw err;
}
}
```
## API Reference
- `interface RollbackPhaseInput` — same fields as `CommitPhaseInput`.
- `function rollbackPhaseTwo(input): Promise<void>`
- no-op for `outcome.prepared === false`
- throws `TwoPcError(TWOPC_ROLLBACK_FAILED)` on non-idempotency SQL
failure.
- `function isUnknownPreparedTransaction(cause: unknown): boolean`
- matches SQLSTATE `42704` AND the substring `"does not exist"` as
a fallback for drivers that omit the SQLSTATE.
## Implementation Notes
The idempotency check sniffs both `cause.code` and `cause.message`
because the two postgres clients in use (`postgres`/porsager and
`pg.Client`) surface the SQLSTATE differently. Using both lets the
module work against either driver without a wrapper layer.
The function is split into a small public `rollbackPhaseTwo` and an
exported `isUnknownPreparedTransaction` helper so recovery can re-use
the idempotency predicate without re-implementing it. Recovery sees
this exact race regularly: the coordinator and the recovery runner
both target the same prepared gid when a host restarts mid-rollback.

View File

@@ -0,0 +1,133 @@
# `recovery/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Boot-time orphan resolution for the 2PC coordinator. When a host
crashes between phase 1 and phase 2, prepared transactions remain
held in `pg_prepared_xacts` on every writer participant. The recovery
runner sweeps those rows on the next boot, consults the host-owned
ledger, and either commits or rolls back each one — guaranteeing the
"once prepared, eventually resolved" half of the 2PC durability
property.
The runner is the only path that should ever resolve an in-doubt
transaction. The coordinator's hot path NEVER auto-recovers; it
escalates to `TWOPC_IN_DOUBT` and lets the next-boot recovery sweep
handle it. That separation keeps the hot path's failure handling
small and the recovery path testable in isolation.
## Features
- `resolveRecovery({ hostInstanceId, ledger, probes, onResolved })` —
the entry point. Returns a `RecoveryReport` enumerating committed,
rolled-back, skipped, and errored gids.
- Idempotent — re-running the function on a host with no outstanding
prepared transactions is a no-op (the inner `queryOrphans` returns
an empty list).
- Treats Postgres `42704` ("does not exist") as success during
rollback — covers the race where another process already resolved
the orphan.
- Marks rows as `in_doubt` on unresolvable errors so the next sweep
can retry; errored gids are surfaced on the report rather than
thrown.
- `buildProbeFromRegistry(registry)` — convenience adapter that
spins up one probe per writer participant.
## Usage: Basic
```ts
import {
buildProbeFromRegistry,
resolveRecovery,
} from "@suaveplan/api-2pc/recovery";
const report = await resolveRecovery({
hostInstanceId: "host-01",
ledger,
probes: buildProbeFromRegistry(registry),
});
console.log(
`committed=${report.committed.length} ` +
`rolledBack=${report.rolledBack.length} ` +
`errors=${report.errors.length}`
);
```
## Usage: Advanced
Wire a custom probe to feed an alternate identifier convention or a
replicated-read connection — useful when the recovery sweep should
hit a read replica to avoid contending with the live writer:
```ts
import { gidPrefixFor } from "@suaveplan/api-2pc/phases";
import { resolveRecovery, type RecoveryProbe } from "@suaveplan/api-2pc/recovery";
const probes: RecoveryProbe[] = [
{
participantId: "billing",
async queryOrphans(prefix) {
const rows = await readReplica.unsafe<{ gid: string }[]>(
`SELECT gid FROM pg_prepared_xacts WHERE gid LIKE $1`,
[`${prefix}%`]
);
return rows.map((r) => r.gid);
},
async commit(gid) {
await primary.unsafe(`COMMIT PREPARED $1`, [gid]);
},
async rollback(gid) {
await primary.unsafe(`ROLLBACK PREPARED $1`, [gid]);
},
},
];
const report = await resolveRecovery({
hostInstanceId: "host-01",
ledger,
probes,
onResolved: (e) => log.info(e),
});
```
## API Reference
- `interface RecoveryProbe`
- `participantId: string`
- `queryOrphans(prefix: string): Promise<readonly string[]>`
- `commit(gid: string): Promise<void>`
- `rollback(gid: string): Promise<void>`
- `interface RecoveryReport`
- `committed: readonly string[]`
- `rolledBack: readonly string[]`
- `skipped: readonly string[]`
- `errors: readonly { gid; error: TwoPcError }[]`
- `interface RecoveryInput`
- `hostInstanceId, ledger, probes, onResolved?`
- `function resolveRecovery(input): Promise<RecoveryReport>`
- `function buildProbeFromRegistry(registry): readonly RecoveryProbe[]`
## Implementation Notes
The ledger lookup uses the gid's parsed `txId` as the primary key —
the gid format from `phases/gid.ts` is what makes that mapping
unambiguous. Rows whose ledger state is `committing` OR `committed`
trigger `COMMIT PREPARED`; everything else (including missing rows)
triggers `ROLLBACK PREPARED`. That asymmetric default ("rollback by
default") is the safe choice — committing a row whose ledger does
not explicitly say "all voted commit" would violate the design
guarantee.
The `42704` swallow during rollback is what makes the runner safe to
re-execute: if the previous sweep crashed AFTER rolling back the
prepared transaction but BEFORE updating the ledger, the second
sweep will see the orphan listed in `pg_prepared_xacts` (or not, if
truly gone) and will not throw on the "does not exist" follow-up.
Errors that cannot be classified as idempotency are captured per-gid
and surfaced on the report — operators page-on `errors.length > 0`,
not on a generic throw.

View File

@@ -0,0 +1,111 @@
# `registry/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The participant registry is the install-time admission control between
plugins and the 2PC coordinator. Plugins declare
`twoPhaseCommit.participates: true` in their manifest; the host
registers them with this registry; the registry refuses any writer
whose bound database engine cannot honour `PREPARE TRANSACTION`.
This is where the design doc §9.2 rule "SQLite and SurrealDB MUST NOT
participate as writers" is enforced. The capability check is injected
as a callback so the registry stays decoupled from
`@suaveplan/api-host`'s concrete `CapabilityRegistry`.
## Features
- `TwoPcParticipantRegistry.register(participant)` — refuses writers
without `db.preparable`; accepts read-only voters regardless of
engine.
- `get(id)` / `list()` / `size()` / `unregister(id)` — standard
registry surface.
- Injection seam: `PreparableCapabilityCheck` is a plain
`(id: string) => boolean` callback — zero coupling to api-host.
## Usage: Basic
```ts
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
const isPreparable = (pluginId: string) =>
pluginId === "billing" || pluginId === "inventory";
const registry = new TwoPcParticipantRegistry(isPreparable);
registry.register({
id: "billing",
voterMode: "writer",
connection: { engine: "postgres", run: pg.unsafe.bind(pg) },
prepare: billingPrepareCallback,
});
```
## Usage: Advanced
Refusing a SQLite-backed plugin as a writer happens at register-time
— wrap the registration in a try/catch to surface the failure to the
plugin loader without crashing the host boot:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
function safeRegisterParticipants(
registry: TwoPcParticipantRegistry,
participants: ReadonlyArray<RegisteredTwoPcParticipant>
) {
const failures: { id: string; reason: string }[] = [];
for (const p of participants) {
try {
registry.register(p);
} catch (err) {
if (
err instanceof TwoPcError &&
err.twoPcCode === TWOPC_CODES.PARTICIPANT_NOT_PREPARABLE
) {
failures.push({ id: p.id, reason: err.message });
continue;
}
throw err;
}
}
return failures;
}
```
The same plugin can then re-register as a `read-only` voter — the
registry accepts read-only voters regardless of engine, because the
coordinator never sends them prepare/commit SQL.
## API Reference
- `type PreparableCapabilityCheck = (participantId: string) => boolean`
— capability resolver injected at construction.
- `class TwoPcParticipantRegistry`
- `constructor(isPreparable: PreparableCapabilityCheck)`
- `register(participant): void` — throws
`TWOPC_PARTICIPANT_NOT_PREPARABLE` on writer refusal.
- `get(id): RegisteredTwoPcParticipant | undefined`
- `list(): readonly RegisteredTwoPcParticipant[]`
- `size(): number`
- `unregister(id): boolean`
## Implementation Notes
The registry refuses a writer at `register`-time rather than at
`run2pc`-time. This is deliberate — admission failures should surface
during plugin install, not when the first 2PC transaction is attempted
in production. The capability check fires only for writers; read-only
voters are accepted unconditionally because the coordinator drops their
connection from phase-2 SQL traffic. The second-half guard against
read-only voters that try to write a payload lives in
`phases/prepare.ts` (raising `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`
when the participant returns `writeIntent: true`).
`unregister` exists for hot-reload paths: when a plugin is removed
from the running host, the registry drops its participant entry so
subsequent `run2pc` calls cannot accidentally route to it.

View File

@@ -0,0 +1,100 @@
# `testing/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `run2pcParticipantConformance(factory)` — a runnable test
suite that pins the contract every 2PC writer participant MUST honour.
Adapter authors drop a single import + invocation into a
`tests/conformance/run-2pc-participant.test.ts` file in their package;
the harness handles the rest.
Shipping the harness on this package's `./testing` subpath keeps
participants accountable to the same prepare/commit/rollback semantics
the coordinator depends on at runtime. Without this harness, drift
between participant implementations would only surface during
integration testing — too late.
## Features
- Six pinned invariants (engine identifier, voter mode, prepare-then-
prepare-SQL, commit-prepared, rollback-prepared, idempotency-on-
second-rollback).
- Per-scenario factory: every test starts from a fresh participant +
teardown so the suite is isolation-safe under parallel runs.
- Uses `@suaveplan/testing/runner` — works under both Vitest and
Bun:test without re-importing.
- Generates unique gids per scenario so re-runs do not collide.
## Usage: Basic
```ts
// In an adapter package's tests/conformance/run-2pc-participant.test.ts
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
import { createMyParticipant, teardownMyParticipant } from "../helpers";
run2pcParticipantConformance({
async create() {
const participant = await createMyParticipant();
return {
participant,
teardown: () => teardownMyParticipant(participant),
};
},
});
```
## Usage: Advanced
The harness exposes no surface beyond `run2pcParticipantConformance` —
deliberately. Adapter-specific assertions belong in adapter test
files, not in the conformance harness. If your adapter needs to
exercise behaviour the suite does not cover (e.g. cancellation,
custom serialization), add a sibling test file:
```ts
import { describe, it } from "@suaveplan/testing/runner";
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
describe("my-participant", () => {
run2pcParticipantConformance({ create: createMyParticipant });
it("cancels a stuck prepare on signal abort", { timeout: 30_000 }, async () => {
// adapter-specific assertion
});
});
```
## API Reference
- `interface TwoPcParticipantFactory`
- `create(): Promise<{ participant: RegisteredTwoPcParticipant; teardown: () => Promise<void> }>`
- `function run2pcParticipantConformance(factory): void`
- Registers six `describe`/`it` blocks against the surrounding
runner. Returns nothing.
## Implementation Notes
The harness deliberately does NOT use a shared Postgres connection
across scenarios. Each scenario calls `factory.create()` to get a
fresh participant; the factory is expected to wire its own connection
(directly to a Postgres container, against a shared pool, or against
an `env-bridge` passthrough URL). This isolation prevents one
scenario's leftover prepared transactions from contaminating the
next.
Gids embed `Date.now()` + a random suffix so re-runs within the same
millisecond never collide on the underlying `pg_prepared_xacts`
table. The cleanup `ROLLBACK PREPARED` at the end of the
prepare-only scenario is best-effort: failure to roll back leaves
the gid prepared until the next teardown sweep, but the harness's
own teardown handle is the primary cleanup path.
The harness is intentionally NOT exported from the package root (`./`
subpath) — only `./testing`. This mirrors the convention used by
sibling packages (`@suaveplan/i18n-shared/testing`,
`@suaveplan/api-host/testing`) so production code can not accidentally
depend on test-only symbols.

View File

@@ -0,0 +1,108 @@
# `types/`
## Purpose
Shared cross-module type definitions for the 2PC coordinator. Holds
the `TwoPcCoordinatorState` state-machine alphabet, the
`PreparableConnection` and `RegisteredTwoPcParticipant` participant
shapes, the `Run2PcInput` / `Run2PcOutcome` public input/output
contracts, and the `TwoPcPrepareResult` per-participant prepare
return type.
Centralising these types here prevents the cyclic-import smell that
would otherwise emerge from `coordinator/`, `phases/`, `recovery/`,
and `registry/` all needing to know the same wire shapes. The
sibling modules import from this barrel and stay decoupled from each
other at the type level.
## Features
- `TwoPcCoordinatorState` — string-literal union covering
`voting`, `committing`, `aborting`, `committed`, `aborted`,
`in_doubt`.
- `PreparableConnection` — structural connection shape exposing
`engine` (a `DatabaseEngine` from `@suaveplan/api-types/db`) and
`run(sql, params?)` returning a `Promise<R>`.
- `RegisteredTwoPcParticipant` — coordinator-side participant view
carrying `id`, `voterMode`, `connection`, and an async
`prepare(txId, payload) => TwoPcPrepareResult` callback.
- `TwoPcPrepareResult` — `{ vote, writeIntent }` returned by a
participant's `prepare` callback.
- `Run2PcInput` / `Run2PcOutcome` — public input/output contracts
accepted and returned by `run2pc`.
## Usage: Basic
```ts
import type {
Run2PcInput,
RegisteredTwoPcParticipant,
} from "@suaveplan/api-2pc";
const input: Run2PcInput = {
participants: [{ id: "billing", input: { invoiceId: "inv-1" } }],
timeoutMs: 5000,
};
const billing: RegisteredTwoPcParticipant = {
id: "billing",
voterMode: "writer",
connection: { engine: "postgres", run: pg.unsafe.bind(pg) },
prepare: async (_txId) => ({ vote: "commit", writeIntent: true }),
};
```
## Usage: Advanced
Read-only voter typing — combine `voterMode: "read-only"` with a
`writeIntent: false` invariant on the `prepare` return shape:
```ts
import type {
RegisteredTwoPcParticipant,
TwoPcPrepareResult,
} from "@suaveplan/api-2pc";
async function auditPrepare(): Promise<TwoPcPrepareResult> {
const ok = await checkConsistency();
return { vote: ok ? "commit" : "abort", writeIntent: false };
}
const audit: RegisteredTwoPcParticipant = {
id: "audit",
voterMode: "read-only",
connection: { engine: "surrealdb", run: surrealUnsafe },
prepare: () => auditPrepare(),
};
```
The coordinator refuses a read-only voter whose `prepare` returns
`writeIntent: true` with `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`
— the static type doesn't enforce that pairing, but the runtime
boundary does.
## API Reference
- `type TwoPcCoordinatorState`
- `interface PreparableConnection`
- `interface RegisteredTwoPcParticipant`
- `interface TwoPcPrepareResult`
- `interface Run2PcInput`
- `interface Run2PcOutcome`
## Implementation Notes
Every interface is `readonly` on every field. This is deliberate —
the coordinator caches inputs (participant lookups, ledger entries)
and a mutation by accident would corrupt in-flight state. The
strictness is enforced at the type level via `readonly` plus
`ReadonlyArray<...>` and at the runtime level by the boundary Zod
schemas in `options/` (for caller-facing inputs).
The `Run2PcOutcome.state` field uses `Extract<TwoPcCoordinatorState,
"committed" | "aborted" | "in_doubt">` so callers do not have to
exhaustively switch over the in-flight states (`voting`,
`committing`, `aborting`) that never appear on a returned outcome.
The structural typing of `PreparableConnection` keeps the package
independent of `pg` and `postgres` (porsager) — both clients can be
adapted at the seam with a one-line wrapper.

View File

@@ -0,0 +1,85 @@
# @suaveplan/api-audit-file — audit sink writing JSON-per-line, HMAC-SHA-256 signed records to a rotated file. Size + time rotation, maxFiles retention, bounded-queue backpressure with observable drops, append + strict-sync write modes. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`.
File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`.
Implements `RequestAuditSink` from
`@suaveplan/api-host/request-pipeline`: writes one JSON line per
record with an HMAC-SHA-256 signature, rolls the file on a size OR age
trigger, and prunes the oldest rotated siblings to a configurable
`maxFiles` retention cap.
Suitable for single-host deployments, on-prem environments without a
log shipper, and any host that prefers durable local storage of the
audit trail. The sibling `@suaveplan/api-audit-stdout` covers the
log-shipped use case; both sinks share the wire shape so dashboards
union on metric name and partition by the `sink` attribute.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by the file-rotating audit sink, plus the concrete…
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by the file-rotating audit sink, plus the concrete…
- [fs-helpers](./src/fs-helpers/fs-helpers.md) — Isolates the `node:fs` surface the sink touches behind an injectable `FsAdapter` interface and concrete `nodeFsAdapter`…
- [fs-helpers](./src/fs-helpers/fs-helpers.md) — Isolates the `node:fs` surface the sink touches behind an injectable `FsAdapter` interface and concrete `nodeFsAdapter`…
- [hmac](./src/hmac/hmac.md) — Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the file sink needs: signing a canonical record on…
- [hmac](./src/hmac/hmac.md) — Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the file sink needs: signing a canonical record on…
- [instruments](./src/instruments/instruments.md) — Constructs the OTel counter bundle the file sink emits and exposes the canonical `sink="file"` attribute set. Every obs…
- [instruments](./src/instruments/instruments.md) — Constructs the OTel counter bundle the file sink emits and exposes the canonical `sink="file"` attribute set. Every obs…
- [retention](./src/retention/retention.md) — Enforces the `maxFiles` retention dimension of the rotation policy by deleting the oldest rotated files until at most `…
- [retention](./src/retention/retention.md) — Enforces the `maxFiles` retention dimension of the rotation policy by deleting the oldest rotated files until at most `…
- [rotation](./src/rotation/rotation.md) — Resolves the rotation policy carried by every `createFileAuditSink` instance and implements the rotation primitive itse…
- [rotation](./src/rotation/rotation.md) — Resolves the rotation policy carried by every `createFileAuditSink` instance and implements the rotation primitive itse…
- [schema](./src/schema/schema.md) — Defines the Zod schema and canonical-bytes function that bracket every audit record before it is signed and written. Th…
- [schema](./src/schema/schema.md) — Defines the Zod schema and canonical-bytes function that bracket every audit record before it is signed and written. Th…
- [sink](./src/sink/sink.md) — The public entry point of `@suaveplan/api-audit-file`. The `createFileAuditSink` factory composes the schema, hmac, rot…
- [sink](./src/sink/sink.md) — The public entry point of `@suaveplan/api-audit-file`. The `createFileAuditSink` factory composes the schema, hmac, rot…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/api-audit-file
```
## Quickstart
```ts
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
const sink = await createFileAuditSink({
path: "/var/log/suaveplan/audit.log",
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
rotation: { maxSizeBytes: 100 * 1024 * 1024, maxAgeMs: 86_400_000, maxFiles: 30 },
appendMode: "strict-sync",
});
await sink.emit(envelopeFromHostPipeline);
await sink.dispose();
```
## Modules
- `./sink` — `createFileAuditSink`, `FileAuditSink`, `CreateFileAuditSinkOptions`, `AppendMode`.
- `./rotation` — `RotationPolicy`, `RotationState`, `resolveRotationPolicy`, `shouldRotate`, `rotate`.
- `./retention` — `enforceRetention`.
- `./schema` — `AuditRecord`, `AuditRecordSchema`, `canonicalise`, `buildSignedLine`.
- `./hmac` — `signRecord`, `verifyRecord`.
- `./instruments` — `AuditFileInstruments`, `createAuditFileInstruments`, `SINK_ATTRIBUTE`.
- `./fs-helpers` — `FsAdapter`, `nodeFsAdapter`, `listRotatedFiles`, `rotatedFilePathAt`.
- `./error-codes` — `AUDIT_FILE_CODES`, `AuditFileError`, `AuditFileCode`.
## Error codes
`AUDIT_FILE.INVALID_RECORD`, `AUDIT_FILE.WRITER_FAILED`,
`AUDIT_FILE.QUEUE_FULL`, `AUDIT_FILE.ROTATION_FAILED`,
`AUDIT_FILE.RETENTION_FAILED`, `AUDIT_FILE.PATH_NOT_WRITEABLE`.
## OTel instruments
- `api.audit.records_total{sink="file"}`
- `api.audit.dropped_total{sink="file"}`
- `api.audit.rotation_evicted_total{sink="file"}`
## Tier
Server-tier — depends on `node:fs/promises`.

View File

@@ -0,0 +1,83 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by
the file-rotating audit sink, plus the concrete
`AuditFileError extends SuaveplanError` subclass that carries them.
Operators rely on the codes for dashboard slicing; package consumers
rely on the typed subclass to differentiate sink failures from generic
host errors without parsing message strings.
The registry is built with `defineCodes("AUDIT_FILE", [...])` from
`@suaveplan/error`, so every code is recorded in the global registry
exactly once at module load. The OpenSpec conformance gate verifies
every entry here is also raised by a production code path elsewhere in
`src/` — no dead codes are permitted.
## Features
- Six stable codes: `INVALID_RECORD`, `WRITER_FAILED`, `QUEUE_FULL`,
`ROTATION_FAILED`, `RETENTION_FAILED`, `PATH_NOT_WRITEABLE`.
- Typed union `AuditFileCode` for narrowing on `error.code`.
- `AuditFileError` subclass with stable `error.name === "AuditFileError"`.
- Forwards `cause`, `context`, `redacted`, and other
`SuaveplanErrorOptions` to the base constructor unchanged.
## Usage: Basic
```ts
import {
AUDIT_FILE_CODES,
AuditFileError,
} from "@suaveplan/api-audit-file/error-codes";
throw new AuditFileError(
AUDIT_FILE_CODES.PATH_NOT_WRITEABLE,
"error.audit.file.path_not_writeable",
{ context: { path: "/var/log/suaveplan/audit.log" } }
);
```
## Usage: Advanced
Narrow on the union after catching to drive code-specific recovery:
```ts
import {
AUDIT_FILE_CODES,
type AuditFileCode,
AuditFileError,
} from "@suaveplan/api-audit-file/error-codes";
function recover(err: unknown): "retry" | "drop" {
if (!(err instanceof AuditFileError)) throw err;
const code: AuditFileCode = err.code;
if (code === AUDIT_FILE_CODES.QUEUE_FULL) return "drop";
if (code === AUDIT_FILE_CODES.WRITER_FAILED) return "retry";
if (code === AUDIT_FILE_CODES.ROTATION_FAILED) return "retry";
throw err;
}
```
## API Reference
- `AUDIT_FILE_CODES: Readonly<Record<Name, "AUDIT_FILE.<Name>">>`
- `type AuditFileCode = (typeof AUDIT_FILE_CODES)[keyof typeof AUDIT_FILE_CODES]`
- `AUDIT_FILE_ERROR_NAME = "AuditFileError"`
- `class AuditFileError extends SuaveplanError<AuditFileCode>`
- `new AuditFileError(code, messageKey, options?)`
## Implementation Notes
The codes are declared `as const` so TypeScript freezes both the keys
and the fully-qualified string values. The `name` literal is exported
separately because some structured loggers group errors on `name`
before they inspect `code` — pinning a constant keeps that grouping
stable across releases. The module is side-effect-imported from
`src/index.ts` so a consumer who only loads a subpath
(`@suaveplan/api-audit-file/sink`) still triggers code registration.

View File

@@ -0,0 +1,86 @@
# `fs-helpers`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Isolates the `node:fs` surface the sink touches behind an injectable
`FsAdapter` interface and concrete `nodeFsAdapter` implementation. The
indirection lets tests run the sink against a real filesystem under a
temp directory (the default for this package's test suite) while
keeping a clean seam for in-memory adapters used by external consumers.
The module also exports the rotated-file enumeration helper used by
the retention module — keeping the file-name conventions next to the
filesystem primitives means rotation, retention, and sink all share
the same view of "what counts as a rotated sibling of this base path".
## Features
- `FsAdapter` — six-method surface: `appendLine`, `fileSize`, `rename`,
`unlink`, `readdir`, `ensureWriteableDir`.
- `nodeFsAdapter` — production adapter against `node:fs/promises`.
- `listRotatedFiles(fs, basePath)` — returns full paths of every
rotated sibling, sorted oldest-first by embedded timestamp.
- `rotatedFilePathAt(basePath, iso)` — builds the rotated path for the
supplied ISO timestamp; `:` is replaced with `-` so the name is
Windows-safe and grep-friendly.
## Usage: Basic
```ts
import { nodeFsAdapter } from "@suaveplan/api-audit-file/fs-helpers";
await nodeFsAdapter.ensureWriteableDir("/var/log/suaveplan");
await nodeFsAdapter.appendLine("/var/log/suaveplan/audit.log", "...\n", false);
```
## Usage: Advanced
Provide an injectable adapter to drive the sink against an in-memory
filesystem:
```ts
import type { FsAdapter } from "@suaveplan/api-audit-file/fs-helpers";
const inMemory = new Map<string, string>();
const adapter: FsAdapter = {
async appendLine(p, line) {
inMemory.set(p, (inMemory.get(p) ?? "") + line);
},
async fileSize(p) {
return Buffer.byteLength(inMemory.get(p) ?? "", "utf8");
},
async rename(from, to) {
inMemory.set(to, inMemory.get(from) ?? "");
inMemory.delete(from);
},
async unlink(p) {
inMemory.delete(p);
},
async readdir() {
return [...inMemory.keys()];
},
async ensureWriteableDir() {
/* always writeable */
},
};
```
## API Reference
- `interface FsAdapter` — see Features above.
- `const nodeFsAdapter: FsAdapter`
- `listRotatedFiles(fs: FsAdapter, basePath: string): Promise<string[]>`
- `rotatedFilePathAt(basePath: string, isoTimestamp: string): string`
## Implementation Notes
`ensureWriteableDir` calls `mkdir(..., { recursive: true })` then a
`W_OK` `access` probe so a fresh boot creates the directory and a
borked permission setup throws `PATH_NOT_WRITEABLE` synchronously
during sink construction. `fileSize` swallows `ENOENT` and returns `0`
so the rotation logic naturally treats a not-yet-created file as
empty — that simplifies the open-on-first-write code path.

View File

@@ -0,0 +1,76 @@
# `hmac`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the
file sink needs: signing a canonical record on write and verifying a
record + signature on read. The wrapper hex-encodes the digest so the
on-disk line stays human-greppable and ASCII-clean, and exposes a
constant-time verifier so tampering checks do not leak position
information through timing.
## Features
- `signRecord(key, bytes): string` — 64-char lowercase hex of
HMAC-SHA-256 over `bytes` using `key`.
- `verifyRecord(key, bytes, hmacHex): boolean` — constant-time
comparison of recomputed HMAC against the provided hex.
- Empty-key guard raises a typed `Error` so misconfiguration cannot
silently produce a deterministic null-MAC.
## Usage: Basic
```ts
import { signRecord, verifyRecord } from "@suaveplan/api-audit-file/hmac";
const key = new TextEncoder().encode("super-secret-rotating-key");
const bytes = new TextEncoder().encode('{"requestId":"r-1"}');
const hmac = signRecord(key, bytes);
verifyRecord(key, bytes, hmac); // → true
```
## Usage: Advanced
Use with the canonical-bytes helper to verify an on-disk line:
```ts
import {
AuditRecordSchema,
canonicalise,
} from "@suaveplan/api-audit-file/schema";
import { verifyRecord } from "@suaveplan/api-audit-file/hmac";
for await (const line of lines) {
const { record, hmac } = JSON.parse(line) as {
record: unknown;
hmac: string;
};
const parsed = AuditRecordSchema.parse(record);
const ok = verifyRecord(key, canonicalise(parsed), hmac);
if (!ok) throw new Error("hmac mismatch");
}
```
## API Reference
- `signRecord(key: Uint8Array, bytes: Uint8Array): string`
- `verifyRecord(key: Uint8Array, bytes: Uint8Array, hmacHex: string): boolean`
## Implementation Notes
The hex encoder is hand-rolled rather than borrowed from
`@suaveplan/crypto-utils` so this module stays a leaf of the dependency
graph — the sink runs on every audit-emitting request, and keeping the
hot path's transitive deps minimal helps cold-start latency on
serverless deploys. Constant-time comparison XORs each character code
and accumulates into a bitmask; the boolean conversion at the end keeps
the early-return branch (length mismatch) bounded to attacker-visible
metadata only. The 64-character length check up front is intentionally
outside the constant-time region because the length of the supplied
HMAC is not secret — an attacker who supplies a malformed hex string
sees an immediate rejection but learns nothing about the recomputed
digest.

View File

@@ -0,0 +1,78 @@
# `instruments`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Constructs the OTel counter bundle the file sink emits and exposes the
canonical `sink="file"` attribute set. Every observation carries the
attribute so a dashboard tracking `api.audit.records_total` across the
sink family — `stdout`, `file`, and future siblings — can union the
metric name and partition by `sink`. The three counters answer the
operator questions "are records flowing?", "are records dropping?",
and "is retention pruning old files?".
## Features
- `createAuditFileInstruments({ meterProvider? })` — returns a frozen
bundle of three counters bound to the supplied or global
`MeterProvider`.
- `SINK_ATTRIBUTE` — frozen `{ sink: "file" }` literal exported for
reuse by the sink, retention, and test harnesses.
- Counters: `api.audit.records_total`, `api.audit.dropped_total`,
`api.audit.rotation_evicted_total`.
## Usage: Basic
```ts
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
const instruments = createAuditFileInstruments();
instruments.recordsTotal.add(1, { sink: "file" });
```
## Usage: Advanced
Install a real `MeterProvider` and assert deltas in tests:
```ts
import {
MeterProvider,
InMemoryMetricExporter,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
const exporter = new InMemoryMetricExporter(0);
const reader = new PeriodicExportingMetricReader({ exporter });
const provider = new MeterProvider({ readers: [reader] });
const instruments = createAuditFileInstruments({ meterProvider: provider });
instruments.recordsTotal.add(1, { sink: "file" });
await reader.collect();
```
## API Reference
- `interface AuditFileInstruments`
- `meter: Meter`
- `recordsTotal: Counter`
- `droppedTotal: Counter`
- `rotationEvictedTotal: Counter`
- `interface AuditFileInstrumentsConfig`
- `meterProvider?: MeterProvider`
- `createAuditFileInstruments(config?): AuditFileInstruments`
- `SINK_ATTRIBUTE: Readonly<{ sink: "file" }>`
## Implementation Notes
The bundle is intentionally NOT module-level cached — every sink
instance gets its own bundle so an operator who reconstructs the host
under a fresh `MeterProvider` (a test-host pattern in
`@suaveplan/api-host/testing`) sees deltas against the right provider.
Counters from the sibling `api-audit-stdout` sink share the metric
NAMES with this package so a single dashboard panel covers the family;
the `sink="file"` attribute distinguishes the source.

View File

@@ -0,0 +1,67 @@
# `retention`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Enforces the `maxFiles` retention dimension of the rotation policy by
deleting the oldest rotated files until at most `maxFiles` remain.
Retention runs immediately after each rotation succeeds so the disk
footprint is bounded across long-running deployments — an audit
directory left to grow unchecked is the most common operator pain
point with file-based sinks, and this module's existence is exactly
the answer to it.
The increment on `api.audit.rotation_evicted_total{sink="file"}` makes
eviction observable. Operators alert on an unexpectedly high rate so a
runaway rotation cycle (e.g. clock skew, misconfigured `maxAgeMs`) is
visible before the retention sweep drops historical data.
## Features
- `enforceRetention(fs, basePath, maxFiles, instruments)` — deletes
the oldest rotated files until at most `maxFiles` remain. Idempotent:
re-running with no surplus is a no-op.
- Counter increment per deletion with the canonical `sink="file"`
attribute set.
- Surfaces `AUDIT_FILE.RETENTION_FAILED` on a delete failure with the
offending path in `context.path` and the underlying `cause`.
## Usage: Basic
```ts
import { enforceRetention } from "@suaveplan/api-audit-file/retention";
await enforceRetention(fs, "/var/log/audit.log", 30, instruments);
```
## Usage: Advanced
Call from a custom rotation cadence — e.g. an external cron that wants
to prune without rotating:
```ts
import { enforceRetention } from "@suaveplan/api-audit-file/retention";
// Cron job: every hour, prune to a smaller retention than the sink's
// default to claw back disk during high-traffic windows.
setInterval(async () => {
await enforceRetention(fs, basePath, 10, instruments);
}, 60 * 60 * 1000);
```
## API Reference
- `enforceRetention(fs: FsAdapter, basePath: string, maxFiles: number, instruments: AuditFileInstruments): Promise<void>`
## Implementation Notes
`listRotatedFiles` returns paths sorted oldest-first by the embedded
ISO timestamp; we slice off the first `(count - maxFiles)` entries and
unlink them. Sorting is lexicographic on the ISO string, which is also
chronological for the `YYYY-MM-DDTHH-MM-SS.sssZ` shape — no extra
parsing required. If `unlink` fails midway, the previously-evicted
deletions are not undone; the throw surfaces the failure and the next
rotation re-attempts retention against whatever survived.

View File

@@ -0,0 +1,95 @@
# `rotation`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Resolves the rotation policy carried by every `createFileAuditSink`
instance and implements the rotation primitive itself — the
`rename(currentFile → currentFile.<utc-iso>.log)` step plus the
post-rotation state reset. The module owns both dimensions documented
in the spec: size-based (`maxSizeBytes`, default 100 MiB) and
time-based (`maxAgeMs`, default 24h). Either condition reaching its
limit triggers a rotation on the next write.
Rotation lives in a separate module so the sink stays focused on
queueing + writing. The retention module reads the policy through the
same `RotationState` so the operator-facing knobs are documented in
exactly one place.
## Features
- `resolveRotationPolicy(input?)` — Zod-equivalent validator returning
a frozen `RotationPolicy`. Defaults filled from the design doc.
- `shouldRotate(state, nowMs)` — pure predicate; size OR age satisfies.
- `rotate(fs, basePath, state, clock)` — performs the rename, updates
state, returns the rotated path for retention to inspect.
- Raises `AUDIT_FILE.ROTATION_FAILED` on rename failure with full
`cause` for forensic logging.
## Usage: Basic
```ts
import {
resolveRotationPolicy,
rotate,
shouldRotate,
} from "@suaveplan/api-audit-file/rotation";
const policy = resolveRotationPolicy({ maxSizeBytes: 1024 * 1024 });
const state = { openedAtMs: Date.now(), bytesWritten: 0, policy };
if (shouldRotate(state, Date.now())) {
await rotate(fs, "/var/log/audit.log", state, () => Date.now());
}
```
## Usage: Advanced
Disable a dimension by setting it to `Number.POSITIVE_INFINITY` —
useful when an operator wants pure time rotation regardless of size:
```ts
const policy = resolveRotationPolicy({
maxSizeBytes: Number.POSITIVE_INFINITY,
maxAgeMs: 60 * 60 * 1000, // hourly
});
```
In tests, drive rotation deterministically with a `FakeClock` from
`@suaveplan/testing`:
```ts
import { FakeClock } from "@suaveplan/testing";
const clock = new FakeClock(0);
const state = { openedAtMs: 0, bytesWritten: 0, policy };
clock.advance(61_000);
await rotate(fs, basePath, state, clock.read);
```
## API Reference
- `interface RotationPolicy`
- `maxSizeBytes: number` — bytes before size rotation; default 100 MiB.
- `maxAgeMs: number` — ms before time rotation; default 24h.
- `maxFiles: number` — kept rotated files; default 30.
- `interface RotationState`
- `openedAtMs: number` — wall-clock open time of current segment.
- `bytesWritten: number` — running byte count.
- `policy: RotationPolicy` — frozen at construction.
- `resolveRotationPolicy(input?): RotationPolicy`
- `shouldRotate(state, nowMs): boolean`
- `rotate(fs, basePath, state, clock): Promise<string>` — returns the
rotated file path.
## Implementation Notes
The rotated suffix replaces `:` with `-` so the resulting filename is
Windows-portable and grep-friendly across shells. State mutation
happens AFTER the rename succeeds — a failed rename leaves the original
state intact, so the next call retries against the same segment rather
than corrupting accounting. `bytesWritten` resets to 0 instead of
re-stat'ing the new empty file; the next successful append updates it
in lock-step with the on-disk length.

View File

@@ -0,0 +1,79 @@
# `schema`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod schema and canonical-bytes function that bracket every
audit record before it is signed and written. The schema mirrors the
`RequestAuditEnvelope` interface exported by
`@suaveplan/api-host/request-pipeline`; the canonical-bytes function
gives every other module a deterministic input for HMAC signing,
file-line construction, and signature verification.
The package re-validates at the sink boundary even though the host
constructs envelopes from typed data — operators frequently hand-craft
audit records in tests, replay tooling, or migration scripts. The
boundary check makes corrupt records fail loudly with a typed
`AUDIT_FILE.INVALID_RECORD` instead of silently bypassing the HMAC.
## Features
- `AuditRecordSchema` — strict Zod object accepting only the
envelope's documented fields. Unknown keys reject.
- `AuditRecord` — `z.infer<typeof AuditRecordSchema>`; structurally
assignable to `RequestAuditEnvelope`.
- `canonicalise(record)` — UTF-8 bytes of the record JSON-stringified
with sorted top-level keys.
- `buildSignedLine(bytes, hmacHex)` — emits the on-disk JSON line
carrying the record + HMAC, with a trailing `\n`.
## Usage: Basic
```ts
import {
AuditRecordSchema,
buildSignedLine,
canonicalise,
} from "@suaveplan/api-audit-file/schema";
const record = AuditRecordSchema.parse(rawEnvelope);
const bytes = canonicalise(record);
const line = buildSignedLine(bytes, "deadbeef...");
```
## Usage: Advanced
Verify an on-disk line:
```ts
import { hmac } from "@suaveplan/crypto-hash";
import { AuditRecordSchema, canonicalise } from "@suaveplan/api-audit-file/schema";
const { record, hmac: hmacHex } = JSON.parse(lineFromDisk);
const parsed = AuditRecordSchema.parse(record);
const bytes = canonicalise(parsed);
const expected = Buffer.from(hmac("SHA-256", key, bytes)).toString("hex");
if (expected !== hmacHex) {
throw new Error("hmac mismatch — record was tampered");
}
```
## API Reference
- `AuditRecordSchema: z.ZodObject` (strict)
- `type AuditRecord = z.infer<typeof AuditRecordSchema>`
- `canonicalise(record: AuditRecord): Uint8Array`
- `buildSignedLine(canonicalBytes: Uint8Array, hmacHex: string): string`
## Implementation Notes
The canonical bytes drop `undefined` values for the optional `cause`
slot so the absence of an error never affects the signature. Top-level
keys are sorted alphabetically to give every implementation (TypeScript,
Go log-reader, Python forensic tooling) the same input bytes for
verification regardless of construction order. `principal.scopes` is
preserved in array order — the order is semantically meaningful in the
pipeline so we do not collapse it.

View File

@@ -0,0 +1,128 @@
# `sink`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The public entry point of `@suaveplan/api-audit-file`. The
`createFileAuditSink` factory composes the schema, hmac, rotation,
retention, instruments, and fs-helpers modules into a single object
that implements the `RequestAuditSink` contract from
`@suaveplan/api-host/request-pipeline`. The sink writes each accepted
record as one JSON line with an HMAC-SHA-256 signature, rolls the file
when size OR age limits are reached, and keeps at most `maxFiles`
rotated siblings.
The sink is queue-buffered: callers `await emit()` and resolve as soon
as the record has been appended to disk. A bounded queue protects the
host from a stalled filesystem: records arriving while the queue is at
`maxQueueSize` are dropped with a typed `AUDIT_FILE.QUEUE_FULL` and an
`api.audit.dropped_total{sink="file"}` increment. `dispose()` flushes
the queue and is idempotent.
## Features
- `createFileAuditSink({ path, hmacKey, ... })` — async factory.
Returns a sink ready to `emit()`.
- Schema validation at the boundary: invalid records throw
`AUDIT_FILE.INVALID_RECORD` and increment `dropped_total`.
- `strict-sync` (default) and `append` modes for the durability /
throughput trade-off.
- Boot-time `mkdir -p` of the parent directory + `W_OK` probe;
unwriteable targets throw `AUDIT_FILE.PATH_NOT_WRITEABLE` from the
factory itself so misconfiguration is visible at host startup.
- Size + time rotation; `maxFiles` retention; per-eviction OTel counter.
- Idempotent `dispose()`; observable `queueDepth` getter for probes.
## Usage: Basic
```ts
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
const sink = await createFileAuditSink({
path: "/var/log/suaveplan/audit.log",
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
});
await sink.emit({
at: new Date().toISOString(),
requestId: "r-1",
traceId: null,
route: "/admin/users",
plugin: "core-admin",
method: "POST",
principal: { id: "user-1", scopes: ["admin"] },
tenantId: "tenant-a",
status: 200,
outcome: "success",
});
await sink.dispose();
```
## Usage: Advanced
Run with a custom rotation policy, `append` mode, real `MeterProvider`,
and an injected `FakeClock` for deterministic tests:
```ts
import { FakeClock } from "@suaveplan/testing";
import {
MeterProvider,
InMemoryMetricExporter,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
const clock = new FakeClock(0);
const exporter = new InMemoryMetricExporter(0);
const reader = new PeriodicExportingMetricReader({ exporter });
const meterProvider = new MeterProvider({ readers: [reader] });
const sink = await createFileAuditSink({
path: "/tmp/audit.log",
hmacKey: new TextEncoder().encode("secret"),
clock: clock.read,
rotation: { maxSizeBytes: 1024, maxAgeMs: 60_000, maxFiles: 5 },
appendMode: "append",
maxQueueSize: 256,
instruments: createAuditFileInstruments({ meterProvider }),
});
```
## API Reference
- `interface CreateFileAuditSinkOptions`
- `path: string` (required)
- `hmacKey: Uint8Array` (required, non-empty)
- `clock?: Clock` — default `() => Date.now()`
- `rotation?: Partial<RotationPolicy>`
- `appendMode?: "strict-sync" | "append"` — default `"strict-sync"`
- `maxQueueSize?: number` — default 1024
- `fs?: FsAdapter` — default `nodeFsAdapter`
- `logger?: LoggerLike` — default no-op
- `instruments?: AuditFileInstruments` — default global meter
- `interface FileAuditSink`
- `emit(record): Promise<void>`
- `dispose(): Promise<void>`
- `readonly queueDepth: number`
- `createFileAuditSink(options): Promise<FileAuditSink>`
## Implementation Notes
The drain loop is mutex-style: a single in-flight drain promise covers
every concurrent `emit()` call so writes are serialised against the
file handle even when callers fan out. Backpressure is signalled
synchronously — `emit()` throws `QUEUE_FULL` before any await when the
bounded queue is at capacity, so the host can apply load shedding
without observing a write-latency spike. Rotation runs in the same
drain frame as the triggering write so the rename always happens
before the segment grows past the cap by one extra line. Retention
runs after rotation; a retention failure is logged but does not
propagate — the sink stays available even if `unlink` is transiently
denied. The on-disk format is intentionally trivial (`{ record, hmac }`
per line) so an operator with `jq` and `openssl dgst -sha256 -hmac` can
verify integrity without any Suaveplan tooling on hand.

View File

@@ -0,0 +1,79 @@
# @suaveplan/api-audit-stdout - Stdout `RequestAuditSink` for @suaveplan/api-host. Writes JSON-per-line audit records to a Writable (default process.stdout) with an HMAC-SHA-256 signature over canonical bytes. Implements the design-doc §12.5 audit-record shape, Zod-validated at the boundary, with bounded backpressure (maxQueueMs default 100ms), observable drop counter, and a once-per-minute warning throttle. OTel counters api.audit.records_total{sink=stdout,type,outcome} and api.audit.dropped_total{sink=stdout}. Server-tier — Bun + Node.
Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line
audit records to a `LineWriter` (default `process.stdout`) with an
HMAC-SHA-256 signature appended as a `_hmac` field. Implements
`RequestAuditSink` from `@suaveplan/api-host/request-pipeline`, so the
host's request pipeline accepts the instance directly via its `audit`
capability slot.
The record shape is the design-doc §12.5 canonical wire format —
`ts`, `type`, `principal`, `tenant`, `request_id`, `trace_id`,
`subject`, `outcome`, and a conditional `cause`. Each record is
Zod-validated at the boundary before signing, so a malformed record
never reaches the log pipeline.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — This module declares the four stable error codes the stdout audit sink exposes to consumers. Every failure path inside…
- [error-codes](./src/error-codes/error-codes.md) — This module declares the four stable error codes the stdout audit sink exposes to consumers. Every failure path inside…
- [hmac](./src/hmac/hmac.md) — This module produces the HMAC-SHA-256 tag attached to every audit line. Signing operates on a *canonical* byte represen…
- [hmac](./src/hmac/hmac.md) — This module produces the HMAC-SHA-256 tag attached to every audit line. Signing operates on a *canonical* byte represen…
- [instruments](./src/instruments/instruments.md) — This module owns the two counters the stdout sink emits at runtime, matching design doc §12.3's metric surface for the…
- [instruments](./src/instruments/instruments.md) — This module owns the two counters the stdout sink emits at runtime, matching design doc §12.3's metric surface for the…
- [options](./src/options/options.md) — This module owns the Zod schema and defaults for the scalar slice of `createStdoutAuditSink({...})`'s input. Validation…
- [options](./src/options/options.md) — This module owns the Zod schema and defaults for the scalar slice of `createStdoutAuditSink({...})`'s input. Validation…
- [schema](./src/schema/schema.md) — This module locks the wire shape of the JSON line the stdout sink writes. The shape is exactly what design doc §12.5 sp…
- [schema](./src/schema/schema.md) — This module locks the wire shape of the JSON line the stdout sink writes. The shape is exactly what design doc §12.5 sp…
- [sink](./src/sink/sink.md) — This module wires the schema, HMAC signer, instruments, and options into a `RequestAuditSink` implementation that write…
- [sink](./src/sink/sink.md) — This module wires the schema, HMAC signer, instruments, and options into a `RequestAuditSink` implementation that write…
<!-- end:auto-docs -->
## Modules
- [`./sink`](./src/sink/sink.md) — `createStdoutAuditSink`, the
factory that ties everything together.
- [`./schema`](./src/schema/schema.md) — `AuditRecord` Zod schema and
boundary parser.
- [`./hmac`](./src/hmac/hmac.md) — canonical bytes and HMAC-SHA-256
signer / verifier.
- [`./instruments`](./src/instruments/instruments.md) — OTel counters
(`api.audit.records_total`, `api.audit.dropped_total`).
- [`./options`](./src/options/options.md) — scalar option schema +
defaults (`maxQueueMs = 100`, `warnThrottleMs = 60_000`).
- [`./error-codes`](./src/error-codes/error-codes.md) —
`AUDIT_STDOUT.*` registry (`INVALID_RECORD`, `WRITER_FAILED`,
`QUEUE_FULL`, `INVALID_OPTIONS`).
## Quick start
```ts
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout";
const sink = createStdoutAuditSink({
hmacKey: new TextEncoder().encode(process.env["AUDIT_HMAC_KEY"]!),
});
await sink.emit({
at: "2026-05-26T10:00:00.000Z",
requestId: "req_1",
traceId: null,
route: "/admin/plugin",
plugin: "@core/admin",
method: "POST",
principal: { id: "u1", scopes: ["admin"] },
tenantId: "t1",
status: 200,
outcome: "success",
});
```
## Drop policy
Backpressure is bounded by `maxQueueMs` (default 100ms). When the
queue wait exceeds the bound the record is dropped, the
`api.audit.dropped_total{sink,reason}` counter increments, and
(subject to a once-per-`warnThrottleMs` throttle) a single warning is
logged. Drops are NEVER silent.

View File

@@ -0,0 +1,87 @@
# `error-codes` — `AUDIT_STDOUT_*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module declares the four stable error codes the stdout audit sink
exposes to consumers. Every failure path inside `createStdoutAuditSink`
maps onto exactly one of them, so callers can branch on `error.code`
without parsing messages or stack traces. The registry uses
`@suaveplan/error`'s `defineCodes` so the union narrows under
`instanceof AuditStdoutError`.
The codes capture the four observable failure modes of a write-only,
fire-and-forget audit pipeline: a record that fails its own Zod schema
(`INVALID_RECORD`), an option bag that fails the constructor schema
(`INVALID_OPTIONS`), a transport error from the supplied writer
(`WRITER_FAILED`), and the bounded-backpressure drop case
(`QUEUE_FULL`). The codes are surfaced through the
`AuditStdoutError` subclass, which carries a `code` property typed as
the literal union — `instanceof` narrowing flows through.
## Features
- Stable string codes via `defineCodes("AUDIT_STDOUT", [...])`.
- `AuditStdoutError` subclass with public constructor (lint rule
`check-error-subclass-constructors` enforced).
- Code union exported as `AuditStdoutErrorCode` for callers who want
to switch over every member.
- No runtime side effects — the registry is a frozen object literal.
## Usage: Basic
```ts
import {
AUDIT_STDOUT_CODES,
AuditStdoutError,
} from "@suaveplan/api-audit-stdout/error-codes";
throw new AuditStdoutError(
AUDIT_STDOUT_CODES.INVALID_RECORD,
"error.audit_stdout.invalid_record"
);
```
## Usage: Advanced
```ts
import {
AUDIT_STDOUT_CODES,
AuditStdoutError,
type AuditStdoutErrorCode,
} from "@suaveplan/api-audit-stdout/error-codes";
function handle(err: unknown): "drop" | "rethrow" {
if (!(err instanceof AuditStdoutError)) return "rethrow";
const code: AuditStdoutErrorCode = err.code;
switch (code) {
case AUDIT_STDOUT_CODES.QUEUE_FULL:
case AUDIT_STDOUT_CODES.WRITER_FAILED:
return "drop";
case AUDIT_STDOUT_CODES.INVALID_RECORD:
case AUDIT_STDOUT_CODES.INVALID_OPTIONS:
return "rethrow";
}
}
```
## API Reference
- `AUDIT_STDOUT_CODES` — frozen `{ INVALID_RECORD, WRITER_FAILED,
QUEUE_FULL, INVALID_OPTIONS }` mapping to the corresponding
`"AUDIT_STDOUT.*"` literal.
- `AuditStdoutErrorCode` — union of the four literal strings.
- `AuditStdoutError` — `SuaveplanError<AuditStdoutErrorCode>` subclass
with a public constructor accepting `(code, message, options?)`.
## Implementation Notes
`defineCodes` returns a frozen object — adding a new code requires an
OpenSpec change. The subclass declares its own public constructor so
`new AuditStdoutError(...)` is callable from outside the module (the
base class is abstract with a protected constructor). The
`auditStdoutCode` field is a typed alias of `code` for callers who
want a non-generic accessor.

View File

@@ -0,0 +1,82 @@
# `hmac` — canonical bytes + HMAC-SHA-256 signing
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module produces the HMAC-SHA-256 tag attached to every audit
line. Signing operates on a *canonical* byte representation of the
record (keys serialised in a fixed order, `cause` omitted when
absent) so the same logical record always produces the same MAC under
the same key, regardless of how the source object was constructed.
Verification recomputes the MAC and constant-time-compares to the
hex-decoded signature.
The point isn't to encrypt the record — the line is plaintext JSON
written to stdout. The HMAC catches tampering downstream: a log
collector or storage tier that re-writes a field to mask an audited
action invalidates the MAC, and the auditor's verifier flags the
discrepancy. The key never leaves the host; rotation is the caller's
concern.
## Features
- `canonicalBytes(record)` — UTF-8 bytes of JSON with top-level keys
in design-doc §12.5 order and `cause` omitted when absent.
- `signRecord(record, key)` — lowercase hex HMAC-SHA-256, computed
via `@suaveplan/crypto-hash`'s `hmac("SHA-256", …)` primitive.
- `verifyRecord(record, key, signatureHex)` — constant-time match;
rejects bad-length, non-hex, and tampered inputs.
- `HMAC_FIELD` — the `"_hmac"` field name the sink appends to the
JSON line.
## Usage: Basic
```ts
import { signRecord, verifyRecord } from "@suaveplan/api-audit-stdout/hmac";
const key = new Uint8Array(32);
const tag = signRecord(record, key);
const ok = verifyRecord(record, key, tag);
```
## Usage: Advanced
```ts
import {
canonicalBytes,
HMAC_FIELD,
signRecord,
} from "@suaveplan/api-audit-stdout/hmac";
function emit(record: AuditRecord, key: Uint8Array): string {
// Equivalent to what the sink does internally, but caller-managed.
const line = JSON.parse(
new TextDecoder().decode(canonicalBytes(record))
) as Record<string, unknown>;
line[HMAC_FIELD] = signRecord(record, key);
return JSON.stringify(line);
}
```
## API Reference
- `canonicalBytes(record: AuditRecord): Uint8Array`
- `signRecord(record: AuditRecord, key: Uint8Array): string` — hex.
- `verifyRecord(record: AuditRecord, key: Uint8Array, sig: string): boolean`
- `HMAC_FIELD: "_hmac"`
## Implementation Notes
The canonical encoder lists top-level keys explicitly rather than
sorting alphabetically — the design doc fixes the order so callers
reading raw lines see fields in the documented sequence. Nested
objects use natural insertion order; their shape is locked by the
Zod schema so the JSON.stringify output is deterministic. `cause` is
omitted (not serialised as `null`) when absent so a `success` record
without `cause` and a `success` record with `cause: undefined`
produce the same MAC. Hex was chosen over base64 because hex is
case-insensitive trivially and stays ASCII-safe through every common
log pipeline.

View File

@@ -0,0 +1,70 @@
# `instruments` — OTel counters for the stdout audit sink
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the two counters the stdout sink emits at runtime,
matching design doc §12.3's metric surface for the audit pipeline.
Operators wire the `MeterProvider` once at process boot; the sink
picks up the configured provider through this factory. When no SDK
is installed, the OTel global resolves to a no-op meter so the sink
runs cleanly in tests without any ceremony.
The factory returns a frozen bundle so the sink does zero meter
lookups on the hot path — both counters are constructed once and the
record's `type` / `outcome` labels are attached at observation time.
## Features
- `api.audit.records_total{sink, type, outcome}` — successful write
counter.
- `api.audit.dropped_total{sink, reason}` — drop counter for
queue-full and writer-failed paths.
- Default meter provider falls back to the OTel global.
- Frozen bundle — references stable for the sink's lifetime.
## Usage: Basic
```ts
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
const instruments = createStdoutAuditInstruments();
instruments.recordsTotal.add(1, {
sink: "stdout",
type: "request.success",
outcome: "success",
});
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
const meterProvider = new MeterProvider();
const instruments = createStdoutAuditInstruments({ meterProvider });
// Hand them into createStdoutAuditSink so the sink emits through the
// SDK-installed provider rather than the global no-op.
```
## API Reference
- `createStdoutAuditInstruments(config?)`
- `meterProvider?: MeterProvider` — defaults to
`metrics.getMeterProvider()`.
- Returns `StdoutAuditInstruments` with `meter`, `recordsTotal`,
`droppedTotal`.
## Implementation Notes
Counters are constructed eagerly so the first write does zero
allocations beyond the attribute object. The bundle is frozen to
prevent accidental shadowing. The `sink="stdout"` attribute is
attached at observation time by the sink so the same provider can
serve multiple sink implementations side by side (file, vault, etc.)
without conflict on the metric name.

View File

@@ -0,0 +1,75 @@
# `options` — constructor option schema and defaults
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the Zod schema and defaults for the scalar slice of
`createStdoutAuditSink({...})`'s input. Validation happens at the
boundary — a typo on `maxQueueMs` raises
`AUDIT_STDOUT.INVALID_OPTIONS` rather than silently being ignored.
The schema is `.strict()` so unknown keys are rejected (same rationale
as the `AuditRecord` schema).
The non-scalar slots — writer, clock, timers, logger, instruments,
hmacKey — bypass Zod and are checked structurally by the sink itself
at construction. Mixing the two validation styles keeps the typed
surface narrow while still rejecting obvious typos.
## Features
- Strict Zod schema for scalar option keys.
- Defaults: `maxQueueMs = 100`, `warnThrottleMs = 60_000`,
`sinkLabel = "stdout"`.
- Resolver wraps any `ZodError` as `AUDIT_STDOUT.INVALID_OPTIONS`.
- Constants exported so callers can reference them when computing
test throughput budgets.
## Usage: Basic
```ts
import { resolveStdoutAuditScalarOptions } from "@suaveplan/api-audit-stdout/options";
const resolved = resolveStdoutAuditScalarOptions({ maxQueueMs: 250 });
// resolved.warnThrottleMs === 60_000 (default)
```
## Usage: Advanced
```ts
import {
DEFAULT_MAX_QUEUE_MS,
resolveStdoutAuditScalarOptions,
type StdoutAuditScalarOptions,
} from "@suaveplan/api-audit-stdout/options";
function tightenedForTests(): StdoutAuditScalarOptions {
return {
maxQueueMs: Math.max(1, DEFAULT_MAX_QUEUE_MS / 10),
warnThrottleMs: 1_000,
};
}
const resolved = resolveStdoutAuditScalarOptions(tightenedForTests());
```
## API Reference
- `stdoutAuditScalarOptionsSchema` — strict Zod object.
- `StdoutAuditScalarOptions` — inferred type.
- `ResolvedStdoutAuditScalarOptions` — defaults-applied shape.
- `resolveStdoutAuditScalarOptions(raw)` — boundary parser.
- `DEFAULT_MAX_QUEUE_MS`, `DEFAULT_WARN_THROTTLE_MS` — constants.
## Implementation Notes
`maxQueueMs` is bounded to positive integers because a value of 0
would mean "never queue" — that's a different semantics (synchronous
backpressure) and would need its own option. `warnThrottleMs`
governs the once-per-window log; tests use a `FakeClock` plus a
short throttle to exercise the throttle without sleeping. The
`sinkLabel` exists so a future spinoff (e.g. journald) can reuse
this module while still attaching a distinct `sink="…"` label to its
counters.

View File

@@ -0,0 +1,95 @@
# `schema` — `AuditRecord` Zod schema (design doc §12.5)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module locks the wire shape of the JSON line the stdout sink
writes. The shape is exactly what design doc §12.5 specifies for the
admin / audit pipeline: `ts`, `type`, `principal`, `tenant`,
`request_id`, `trace_id`, `subject`, `outcome`, and a conditional
`cause`. Every write goes through `parseAuditRecord(...)` so a
malformed record raises `AUDIT_STDOUT.INVALID_RECORD` *before* a line
is written — log pipelines downstream never see a half-finished
record.
The schema is `.strict()` (unknown top-level fields are rejected) and
carries a `refine` enforcing the §12.5 invariant that `cause` is
REQUIRED when `outcome === "failure"`. Optional fields surface via
`exactOptionalPropertyTypes` parity so a missing `cause` on a
`success` record is distinct from `cause: undefined` on a `failure`
record. The principal slice is its own sub-schema so the same shape
can be reused by other audit sinks that target the same wire format.
## Features
- Strict shape — extra keys rejected.
- `outcome === "failure"` requires `cause` (refine rule).
- Sub-schemas for `cause`, `principal`, and `outcome` so consumers can
type their own DTOs against the same atoms.
- `parseAuditRecord(input)` returns the validated record or throws
`AUDIT_STDOUT.INVALID_RECORD` wrapping the underlying `ZodError`.
- All types are `z.infer<...>` — no hand-written interfaces drift.
## Usage: Basic
```ts
import { parseAuditRecord } from "@suaveplan/api-audit-stdout/schema";
const record = parseAuditRecord({
ts: "2026-05-26T10:00:00.000Z",
type: "admin.plugin.disabled",
principal: { id: "user_1", scopes: ["admin"] },
tenant: "tenant_a",
request_id: "req_1",
trace_id: null,
subject: "plugin.exports",
outcome: "success",
});
```
## Usage: Advanced
```ts
import {
auditRecordSchema,
type AuditRecord,
} from "@suaveplan/api-audit-stdout/schema";
function adapt(envelope: HostEnvelope): AuditRecord {
return auditRecordSchema.parse({
ts: envelope.at,
type: `request.${envelope.outcome}`,
principal: envelope.principal,
tenant: envelope.tenantId,
request_id: envelope.requestId,
trace_id: envelope.traceId,
subject: `${envelope.method} ${envelope.route}`,
outcome: envelope.outcome,
...(envelope.cause !== undefined ? { cause: envelope.cause } : {}),
});
}
```
## API Reference
- `auditRecordSchema` — strict Zod object with the §12.5 invariant
refined.
- `auditCauseSchema`, `auditPrincipalSchema`, `auditOutcomeSchema` —
sub-schemas reused by consumers.
- `AuditRecord`, `AuditCause`, `AuditPrincipal`, `AuditOutcome` —
inferred types.
- `parseAuditRecord(input: unknown): AuditRecord` — boundary parser
that throws `AuditStdoutError(AUDIT_STDOUT.INVALID_RECORD, …)` on
failure.
## Implementation Notes
`.strict()` blocks unknown fields at the boundary — if a future
design-doc revision adds fields, this schema must be updated in the
same OpenSpec change as the spec. The `outcome === "failure"` /
`cause` refine is enforced as a Zod `.refine` rather than a
discriminated union so the failure path returns a `ZodError` with a
single readable `path: ["cause"]` issue.

View File

@@ -0,0 +1,108 @@
# `sink` — `createStdoutAuditSink`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module wires the schema, HMAC signer, instruments, and options
into a `RequestAuditSink` implementation that writes JSON-per-line
audit records to a `LineWriter` (default `process.stdout`). It is the
default audit sink shipped with `@suaveplan/api-host`: container
runtimes already redirect stdout into a log pipeline, so operators
inherit the records without touching host configuration.
The sink is single-flight: at most one write is on the wire at any
time. Subsequent records wait for the slot, but only up to
`maxQueueMs` (default 100ms) before being dropped. Drops are NEVER
silent — every drop increments `api.audit.dropped_total{sink,reason}`
and (subject to the once-per-`warnThrottleMs` throttle) emits a
single warning log. Validation, HMAC signing, and serialisation
happen on the calling side of the queue so a malformed record is
rejected synchronously without consuming the slot.
## Features
- Implements `RequestAuditSink` (`emit(envelope)`) from
`@suaveplan/api-host/request-pipeline` — host accepts the instance
directly.
- `write(record: AuditRecord)` overload for admin paths that already
hold a canonical record.
- Boundary Zod validation — `AUDIT_STDOUT.INVALID_RECORD` on a bad
record.
- Bounded queue — `AUDIT_STDOUT.QUEUE_FULL` on timeout, drop counter
+ throttled warning.
- HMAC-SHA-256 over canonical bytes appended as `_hmac` field.
- `dispose()` drains the in-flight write and rejects subsequent
writes as `closed` drops.
- Pluggable `clock`, `timers`, `logger`, `writer`, `instruments`.
## Usage: Basic
```ts
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout/sink";
const sink = createStdoutAuditSink({
hmacKey: new TextEncoder().encode(process.env["AUDIT_HMAC_KEY"]!),
});
await sink.write({
ts: new Date().toISOString(),
type: "admin.plugin.disabled",
principal: { id: "user_1", scopes: ["admin"] },
tenant: "tenant_a",
request_id: "req_1",
trace_id: null,
subject: "plugin.exports",
outcome: "success",
});
```
## Usage: Advanced
```ts
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout/sink";
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
const meterProvider = new MeterProvider({ readers: [...] });
const sink = createStdoutAuditSink({
hmacKey,
writer: {
async write(line) {
await myStream.write(line);
},
},
maxQueueMs: 250,
warnThrottleMs: 30_000,
instruments: createStdoutAuditInstruments({ meterProvider }),
logger: myLogger,
});
// Hand into createHost({ audit: sink }) per host wiring.
await sink.emit(envelope);
await sink.dispose();
```
## API Reference
- `createStdoutAuditSink(options)` → `StdoutAuditSink`.
- `StdoutAuditSinkOptions` — `{ hmacKey, writer?, clock?, timers?,
logger?, instruments?, maxQueueMs?, warnThrottleMs?, sinkLabel? }`.
- `StdoutAuditSink` — extends `RequestAuditSink`; adds `write(record)`
and `dispose()`.
- `LineWriter` — `{ write(line: string): void | Promise<void> }`.
- `adaptEnvelope(envelope)` — exposed for tests and bespoke
re-emission flows.
## Implementation Notes
The slot reservation uses an in-flight `Promise<void>` rather than a
queue + worker so cancellation flows trivially through GC — there's
no worker loop to shut down. `acquireSlot` races the in-flight write
against the `timers.setTimer` callback; whichever wins clears the
loser. The `closed` flag is checked before validation so a disposed
sink rejects fast without spending CPU on schema work.
`adaptEnvelope` is the only place the host's camelCase envelope
shape meets the canonical snake_case record — keeping that mapping
isolated lets the design-doc wire shape evolve without rippling
through unrelated callers.

View File

@@ -0,0 +1,60 @@
# @suaveplan/api-auth-anonymous — trivial session resolver for @suaveplan/api: always returns null Principal. Used as the tail of resolver chains for fully public APIs and as the foundation conformance seed for every other session-resolver in the stack.
Stage-4 trivial session resolver for `@suaveplan/api`.
Always returns `null` for every request. Use this as the tail of resolver
chains for public endpoints, and as the canonical adapter for fully public
APIs. The package also serves as the foundation conformance seed exercised by
every other concrete session-resolver in Stages 4 / 6 / 8.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
<!-- end:auto-docs -->
## Why does "no auth" need its own package?
Per the API foundation design doc §2.3, every host configures a session
resolver — even fully public APIs. Configuring `ANONYMOUS_AUTH_ADAPTER`
explicitly makes the "no auth" choice visible in `host.json` and lets the rest
of the host (RBAC gate, error envelope, OpenAPI emission) operate uniformly:
the principal is simply `null`, never `undefined`.
## Surface
| Subpath | Export |
|----------------|---------------------------------------------------|
| `./resolver` | `createAnonymousResolver(opts?): SessionResolver` |
| `./adapter` | `ANONYMOUS_AUTH_ADAPTER: AuthAdapter` |
| `./error-codes`| `AUTH_ANON_CODES`, `AuthAnonError` |
## Usage
```ts
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
host.registerAuth(ANONYMOUS_AUTH_ADAPTER);
// every request now produces `principal: null`; public routes proceed,
// non-public routes 401 via the host's `publicRoutes` decision.
```
For mixed chains, see `createAnonymousResolver()` + `SessionResolverChain`
in `@suaveplan/api-auth/chain`.
## Conformance
The resolver passes `runAuthSessionConformance` (null-only mode) from
`@suaveplan/api-auth/testing`. See
`src/resolver/resolver.test.ts`.
## Errors
`AUTH_ANON.INVALID_OPTIONS` — thrown only when the factory is handed a
non-object or unknown-key options bag. No per-request error paths exist by
design.

View File

@@ -0,0 +1,72 @@
# `adapter` — `ANONYMOUS_AUTH_ADAPTER`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous
resolver with the minimal adapter metadata the host needs to wire it into the
per-request pipeline. The adapter omits `securityScheme` on purpose:
`validateAuthAdapter` permits a missing scheme for the anonymous topology so
the emitted OpenAPI document contains no `security` entry on routes — the
correct shape for "no authentication required".
## Features
- Stateless singleton — safe to import once per host process.
- `csrf: false` baked in: anonymous traffic has no session to protect; the
generic CSRF middleware in `@suaveplan/api-auth/csrf-policy` skips routes
whose adapter pins `csrf: false`.
- Validates cleanly against `validateAuthAdapter` — no runtime error at host
registration time despite the omitted security scheme.
## Usage: Basic
```ts
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
import { validateAuthAdapter } from "@suaveplan/api-auth";
const adapter = validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER);
host.registerAuth(adapter);
```
## Usage: Advanced
For a chain that mixes typed resolvers with the anonymous tail, do not use
`ANONYMOUS_AUTH_ADAPTER` directly — assemble a `SessionResolverChain` so the
typed resolvers fire first:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver,
createAnonymousResolver(),
]);
const adapter = {
resolver: chain,
securityScheme: jwtSecurityScheme, // the typed resolver still emits its scheme
} as const;
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|------------------------------------------------|
| `ANONYMOUS_AUTH_ADAPTER` | const | `AuthAdapter` with anonymous resolver, no scheme, `csrf: false`. |
## Implementation Notes
- The constant is constructed once at module load and reused across requests
— the resolver itself is documented stateless, so a single instance is safe
to fan out across concurrent requests.
- `csrf: false` is deliberate. Anonymous requests carry no session cookie, so
applying CSRF defence would reject legitimate public traffic. Hosts that
want to additionally rate-limit anonymous traffic should layer
`api-rate-limit` (Stage 8) on top — that is not this package's concern.
- Test coverage exercises (a) `validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER)`
returns the adapter unchanged, (b) the embedded resolver still resolves
null, and (c) the singleton survives repeated import without re-allocation.

View File

@@ -0,0 +1,77 @@
# `error-codes` — `AUTH_ANON.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous
resolver has no per-request failure modes — every request resolves to `null` by
construction. The single `AUTH_ANON.INVALID_OPTIONS` code covers the only thing
that can fail, which is rejecting malformed options handed to
`createAnonymousResolver(opts)` at construction time. Keeping a dedicated
code-namespace per package (rather than re-using `AUTH.*`) means consumers can
disambiguate anonymous-side problems from general session-resolver issues when
multiple resolvers are mounted on the same chain.
## Features
- Defined via `defineCodes("AUTH_ANON", [...])` from `@suaveplan/error` so the
literal value is always `"AUTH_ANON.INVALID_OPTIONS"`.
- `AuthAnonError extends SuaveplanError<AuthAnonErrorCode>` — instance checks
narrow `error.code` to the registry type at the type level.
- Carries `authAnonCode` as a non-discriminating mirror so adapter code can
branch on `err.authAnonCode === AUTH_ANON_CODES.INVALID_OPTIONS` without
re-typing the parent class.
## Usage: Basic
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"createAnonymousResolver: options is not an object"
);
```
## Usage: Advanced
Wrap a parser failure (e.g. zod) and forward its details into the
`context.issues` slot so observability pipelines can render the schema diff:
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
import z from "zod";
const OptionsSchema = z.object({}).strict();
export function parseOptions(input: unknown): Record<string, never> {
const out = OptionsSchema.safeParse(input);
if (!out.success) {
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"Anonymous resolver options failed schema validation",
{ context: { issues: out.error.issues } }
);
}
return out.data;
}
```
## API Reference
| Symbol | Kind | Description |
|----------------------|----------|------------------------------------------------------------|
| `AUTH_ANON_CODES` | const | `{ INVALID_OPTIONS: "AUTH_ANON.INVALID_OPTIONS" }` |
| `AuthAnonErrorCode` | type | Union of values in `AUTH_ANON_CODES`. |
| `AuthAnonError` | class | `extends SuaveplanError<AuthAnonErrorCode>`. |
## Implementation Notes
- The single code is deliberate. The resolver is documented to never reject a
request; adding speculative codes for "unrecognised credential" or
"principal lookup failed" would be lying about a contract that the
implementation cannot break by construction.
- Tested in `error-codes.test.ts` for both string value and instance shape.

View File

@@ -0,0 +1,79 @@
# `resolver` — `createAnonymousResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory for the trivial `SessionResolver` that always returns `null`. This is
the canonical tail-of-chain resolver every public-API host configures: it
makes the "no auth" choice explicit in `host.json` and lets the rest of the
host (RBAC gate, error envelope, OpenAPI emission) operate uniformly because
`principal` is `null` not `undefined`. The factory shape exists — rather than
exposing a constant — so future option flags (e.g. `logAnonymousAccess`) can
be threaded in without a breaking surface change.
## Features
- Always resolves `null` — no header inspection, no network I/O, no state.
- Idempotent `dispose()` no-op (resolver holds no resources).
- Strict zod options schema rejects non-object input and unknown keys with
`AUTH_ANON.INVALID_OPTIONS`.
- Returned type is `SessionResolver` from `@suaveplan/api-auth/resolver` so
it composes directly into `SessionResolverChain`.
## Usage: Basic
```ts
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const resolver = createAnonymousResolver();
// Inside a request pipeline:
const principal = await resolver.resolve(request); // → null
await resolver.dispose(); // safe to call again
```
## Usage: Advanced
Compose as the tail of a resolver chain so a JWT-typed resolver runs first
and the anonymous resolver supplies the `null` short-circuit for unrecognised
requests on public routes:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver, // recognises Bearer tokens, else returns null
apiKeyResolver, // recognises x-api-key, else returns null
createAnonymousResolver(), // tail: always null
]);
const principal = await chain.resolve(request);
// principal is now `Principal | null`. The host applies its `publicRoutes`
// decision: a non-public route 401s; a public route proceeds with null.
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|-------------------------------------------------------|
| `createAnonymousResolver` | function | `(opts?: AnonymousResolverOptions) => SessionResolver`. |
| `AnonymousResolverOptions` | type | Reserved-for-future-flags options bag. |
## Implementation Notes
- The resolver returns `Promise<null>` literally rather than the wider
`Promise<Principal | null>` so type narrowing in tests can use
`expectTypeOf<...>().toEqualTypeOf<Promise<null>>()`. The chain widens it
back to `Principal | null` at the boundary.
- `dispose()` is a `Promise<void>` not just any promise — the conformance
harness asserts the resolved value is `undefined` so this is load-bearing.
- The factory is the public surface (not a const) because future flags must
be addable without a breaking change to call sites. Even today, passing
`{}` is the canonical call.
- No OTel instrumentation is emitted from this resolver. Anonymous-access
observability lives at the host layer (per-request span tag) — adding
counters here would force a foundation-tier package to depend on
`@suaveplan/opentelemetry`, which is undesirable for a hot path.

View File

@@ -0,0 +1,155 @@
# @suaveplan/api-auth-bearer-static — SessionResolver adapter for service-to-service static bearer tokens. Matches Authorization: Bearer <token> against a fixed token→Principal table using constant-time comparison from @suaveplan/crypto-utils; unknown tokens defer to the next chain entry. Supports an optional per-token mutual-TLS gate that rejects when the host did not surface request.tls.peerCertificate. Ships both a direct surface (createBearerStaticResolver) and a worker-proxy surface (createBearerStaticResolverWorkerProxy) over @suaveplan/api-worker-rpc; both pass runAuthSessionConformance from @suaveplan/api-auth/testing. OpenAPI security-scheme contribution (HTTP Bearer). Zod-validated options, AUTH_BEARER_STATIC.* error codes, OTel match counter partitioned by outcome.
Stage-4 service-to-service `SessionResolver` adapter for the
`@suaveplan/api` plugin system. Matches an inbound `Authorization:
Bearer <token>` header against a fixed `token → Principal` table using
constant-time comparison from `@suaveplan/crypto-utils`. Unknown tokens
return `null` so the host's `SessionResolverChain` falls through to the
next adapter (typically `api-auth-jwt` for user sessions).
Ships both the direct surface (`createBearerStaticResolver`) and the
worker-proxy surface (`createBearerStaticResolverWorkerProxy` +
`serveBearerStaticResolverOnPort`) over `@suaveplan/api-worker-rpc`; both
pass `runAuthSessionConformance` from `@suaveplan/api-auth/testing`.
<!-- begin:auto-docs -->
## Documentation tree
- [constant-time](./src/constant-time/token-match.md) — `findTokenIndex` performs a constant-time lookup of a presented bearer token against a fixed table, returning the match…
- [token-match](./src/constant-time/token-match.md) — `findTokenIndex` performs a constant-time lookup of a presented bearer token against a fixed table, returning the match…
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace that every public failure path in this packa…
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace that every public failure path in this packa…
- [factory](./src/factory/create-bearer-static-resolver.md) — `createBearerStaticResolver` is the direct-surface entry point of the package. It accepts validated `BearerStaticResolv…
- [create-bearer-static-resolver](./src/factory/create-bearer-static-resolver.md) — `createBearerStaticResolver` is the direct-surface entry point of the package. It accepts validated `BearerStaticResolv…
- [mtls](./src/mtls/mtls-gate.md) — The mTLS gate enforces an optional second factor on individual token table entries. When operators mark an entry with `…
- [mtls-gate](./src/mtls/mtls-gate.md) — The mTLS gate enforces an optional second factor on individual token table entries. When operators mark an entry with `…
- [options](./src/options/bearer-static-resolver-options.md) — The options module owns the public construction surface of every resolver in this package. It defines the `BearerStatic…
- [bearer-static-resolver-options](./src/options/bearer-static-resolver-options.md) — The options module owns the public construction surface of every resolver in this package. It defines the `BearerStatic…
- [proxy](./src/proxy/create-bearer-static-resolver-worker-proxy.md) — The worker-proxy surface is the second of the two Stage-4 surfaces this adapter ships (`§5.2a`). It lets the host insta…
- [create-bearer-static-resolver-worker-proxy](./src/proxy/create-bearer-static-resolver-worker-proxy.md) — The worker-proxy surface is the second of the two Stage-4 surfaces this adapter ships (`§5.2a`). It lets the host insta…
- [security-scheme](./src/security-scheme/security-scheme-contribution.md) — This module builds the OpenAPI 3.1 `components.securitySchemes` contribution the adapter publishes to the host's `AuthA…
- [security-scheme-contribution](./src/security-scheme/security-scheme-contribution.md) — This module builds the OpenAPI 3.1 `components.securitySchemes` contribution the adapter publishes to the host's `AuthA…
- [telemetry](./src/telemetry/instruments.md) — The telemetry module owns the single OpenTelemetry counter this package emits — `api.auth.bearer_static_match_total` —…
- [instruments](./src/telemetry/instruments.md) — The telemetry module owns the single OpenTelemetry counter this package emits — `api.auth.bearer_static_match_total` —…
<!-- end:auto-docs -->
## Install
```sh
bun add @suaveplan/api-auth-bearer-static
```
## Direct surface
```ts
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
import { SessionResolverChain } from "@suaveplan/api-auth";
const serviceAuth = createBearerStaticResolver({
tokens: [
{
token: process.env.CRON_TOKEN,
principal: {
id: "svc_cron",
scopes: ["jobs:run"],
claims: { kind: "service" },
},
},
{
token: process.env.ADMIN_TOKEN,
principal: {
id: "svc_admin",
scopes: ["admin:*"],
claims: { kind: "admin" },
},
mtlsRequired: true,
},
],
});
// Chained — service tokens first, JWT users fall through.
const chain = new SessionResolverChain([serviceAuth, jwtResolver]);
```
## Chained-resolver pattern with `api-auth-jwt`
The intended deployment topology pairs this adapter with
`@suaveplan/api-auth-jwt` for user sessions. Static-token callers match
first; everything else flows to JWT. The chain's `first-match-wins`
semantics means unknown tokens never throw — they simply move on.
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt"; // future sibling
const chain = new SessionResolverChain([
createBearerStaticResolver({
tokens: [/* service callers */],
}),
createJwtSessionResolver({
issuer: "https://idp.example",
audience: "suaveplan",
}),
]);
```
## Worker-proxy surface
Use the worker-proxy variant when the host process should not hold the
real token table — e.g. an isolated auth worker serving multiple host
workers. The host imports `createBearerStaticResolverWorkerProxy(port)`;
the worker imports `serveBearerStaticResolverOnPort(port, options)`. The
proxy transparently passes `runAuthSessionConformance`, so plugin code
treats it identically to the direct surface.
```ts
// host.ts
const proxy = createBearerStaticResolverWorkerProxy(port);
const chain = new SessionResolverChain([proxy, jwtResolver]);
```
```ts
// worker.ts
serveBearerStaticResolverOnPort(port, {
tokens: [/* real table — never crosses to the host */],
});
```
## Optional mutual-TLS gate
Token entries MAY carry `mtlsRequired: true`. When set, the resolver
requires the host listener to surface a verified peer certificate via
`request.tls.peerCertificate`; absent → returns `null` (outcome:
`mtls-required`). The adapter does NOT establish TLS — that is the host
listener's responsibility (Bun TLS, Node `https`, Cloudflare Worker TLS
terminator, etc.).
## Token rotation
This adapter is intentionally static — rotate tokens by reloading host
config. There is no DB-backed lookup, no JWKS, no refresh loop. If
you need rotation without restarts, use `api-auth-better-auth` or a
future `api-auth-token-table` adapter; this one is the simplest possible
thing that resolves a fixed table against constant-time match.
## Errors
The package exposes the `AUTH_BEARER_STATIC.*` code namespace via
`AUTH_BEARER_STATIC_CODES`. Only `INVALID_OPTIONS` is thrown
(synchronously, at factory construction); `NO_TOKEN`, `UNKNOWN_TOKEN`,
and `MTLS_REQUIRED` are surfaced via the OTel match counter's `outcome`
label.
## Telemetry
One counter — `api.auth.bearer_static_match_total{outcome=match|no-token|unknown-token|mtls-required}`
— increments exactly once per `resolve()` call.
## Conformance
Both surfaces pass `runAuthSessionConformance` from
`@suaveplan/api-auth`. Spec-conformance tests live alongside the source
under `src/factory/*.spec-conformance.test.ts` and
`src/proxy/*.spec-conformance.test.ts`.

View File

@@ -0,0 +1,82 @@
# Token match — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`findTokenIndex` performs a constant-time lookup of a presented bearer
token against a fixed table, returning the matching entry's index or `-1`
if no entry matches. The function is the security boundary of the package:
every other code path in the resolver assumes the match is timing-safe so
an attacker cannot binary-search the token table by measuring response
latency.
The implementation walks every entry, calling `constantTimeEqual` from
`@suaveplan/crypto-utils` once per row. The match-tracking variable is
written only on the first match; subsequent matches are ignored. Crucially
the loop does not break on first match — exiting early would leak the
position of the matching token via wall-clock timing.
## Features
- Wraps `@suaveplan/crypto-utils` `constantTimeEqual` over UTF-8 byte
views.
- Iterates the full table for every call — no early exit, no short-circuit
on `presented === entry.token`.
- Returns the first matching index or `-1`.
- Handles `undefined` entries (defensive guard for sparse arrays).
## Usage: Basic
```ts
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
const table = [
{ token: "T1" },
{ token: "T2" },
];
const index = findTokenIndex("T2", table);
// index === 1
```
## Usage: Advanced
```ts
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
interface TokenEntry {
readonly token: string;
readonly principalId: string;
}
const table: ReadonlyArray<TokenEntry> = [
{ token: "service-cron", principalId: "p1" },
{ token: "service-admin", principalId: "p2" },
];
function lookup(presented: string): TokenEntry | null {
const idx = findTokenIndex(presented, table);
return idx === -1 ? null : (table[idx] ?? null);
}
```
## API Reference
- `findTokenIndex(presented: string, table: ReadonlyArray<{ token: string }>): number`
— returns the index of the first entry whose `token` matches
`presented`, or `-1` if no entry matches.
## Implementation Notes
`constantTimeEqual` from `@suaveplan/crypto-utils` walks every byte of the
two `Uint8Array` views and accumulates differences into an integer via
bitwise OR; the function returns whether the accumulator stayed zero. The
length-mismatch fast path is documented as not secret (token lengths are
not secret in this adapter's threat model — the table is operator-supplied
config). The `TextEncoder` round-trip ensures we compare bytes, not
surrogate-pair code units; otherwise an attacker could compare a multi-
byte presented token against a single-byte entry and learn from the
length-mismatch shortcut whether their candidate had the right length.

View File

@@ -0,0 +1,94 @@
# Error codes — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace
that every public failure path in this package surfaces. Codes are produced
by `defineCodes` from `@suaveplan/error` so the registry yields a frozen
record of fully-qualified strings (`AUTH_BEARER_STATIC.NO_TOKEN`,
`AUTH_BEARER_STATIC.UNKNOWN_TOKEN`, `AUTH_BEARER_STATIC.MTLS_REQUIRED`,
`AUTH_BEARER_STATIC.INVALID_OPTIONS`) along with a `BearerStaticAuthError`
subclass that lifts each code into a real `Error` instance.
This module is the contract a caller branches on. Catching a generic
`SuaveplanError` and reading `.code` is the supported failure-handling
pattern; the concrete subclass exists to make `instanceof` narrow the code
union for downstream code that needs the stronger typing.
## Features
- Frozen four-entry code registry keyed by reason name.
- `BearerStaticAuthError` extends `SuaveplanError<BearerStaticAuthErrorCode>`
— `instanceof` narrows `.code`.
- Stable namespace string (`AUTH_BEARER_STATIC`) safe to log and persist.
- Every code exercised by at least one production code path (registry-vs-
source conformance is asserted by the spec-conformance test).
## Usage: Basic
```ts
import {
AUTH_BEARER_STATIC_CODES,
BearerStaticAuthError,
} from "@suaveplan/api-auth-bearer-static/error-codes";
try {
// factory call that may throw on bad options
throw new BearerStaticAuthError(
AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS,
"tokens table is empty"
);
} catch (e) {
if (e instanceof BearerStaticAuthError) {
// e.code narrows to BearerStaticAuthErrorCode
console.error(e.code, e.message);
}
}
```
## Usage: Advanced
```ts
import {
AUTH_BEARER_STATIC_CODES,
BearerStaticAuthError,
} from "@suaveplan/api-auth-bearer-static/error-codes";
function isInvalidOptions(err: unknown): err is BearerStaticAuthError {
return (
err instanceof BearerStaticAuthError &&
err.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
);
}
// Treat invalid options as a fatal config error; bubble everything else.
try {
// build resolver
} catch (e) {
if (isInvalidOptions(e)) {
process.exit(78); // EX_CONFIG
}
throw e;
}
```
## API Reference
- `AUTH_BEARER_STATIC_CODES` — `Readonly<Record<Reason,
"AUTH_BEARER_STATIC.<Reason>">>`. Frozen at module load.
- `BearerStaticAuthErrorCode` — string-literal union of every value.
- `class BearerStaticAuthError extends SuaveplanError<BearerStaticAuthErrorCode>`
— constructor `(code, messageKey, options?)` matches the
`SuaveplanError` shape.
## Implementation Notes
The registry is constructed with `defineCodes("AUTH_BEARER_STATIC", [...])`.
`defineCodes` returns a `Readonly` record whose key set is the reason names
and whose values are the fully-qualified `"AUTH_BEARER_STATIC.<reason>"`
strings — referenced via `AUTH_BEARER_STATIC_CODES.NO_TOKEN` rather than the
raw string literal so a typo at the call site fails type-check.

View File

@@ -0,0 +1,106 @@
# Direct factory — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createBearerStaticResolver` is the direct-surface entry point of the
package. It accepts validated `BearerStaticResolverOptions`, builds a
`SessionResolver` over the fixed token table, and returns a resolver
implementation suitable for placing inside a `SessionResolverChain` from
`@suaveplan/api-auth`. The resolver matches the documented chain
semantics: an unknown token, missing header, or tripped mTLS gate all
return `null` so the chain advances to the next entry.
The resolver is stateless beyond the disposed flag — there is no DB
connection, no JWKS cache, no refresh loop. Disposal flips the flag and
subsequent calls return `null` rather than throwing.
## Features
- Stateless per-request resolution; safe under concurrent fanout.
- Constant-time token match via `findTokenIndex`.
- Optional per-entry mTLS gate via `checkMtlsGate`.
- Configurable `headerName` and prefix (`Bearer` by default).
- Case-insensitive scheme match per RFC 7235.
- OTel counter emission for every of the four outcomes.
## Usage: Basic
```ts
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
const resolver = createBearerStaticResolver({
tokens: [
{
token: "service-cron-2026",
principal: {
id: "svc_cron",
scopes: ["jobs:run"],
claims: { kind: "service" },
},
},
],
});
const req = new Request("https://api/x", {
headers: { Authorization: "Bearer service-cron-2026" },
});
const principal = await resolver.resolve(req);
// principal.id === "svc_cron"
```
## Usage: Advanced
```ts
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createJwtResolver } from "@suaveplan/api-auth-jwt"; // hypothetical sibling
// Chained — service tokens checked first, JWT users fall through after.
const chain = new SessionResolverChain([
createBearerStaticResolver({
tokens: [
{
token: "admin-2026",
principal: {
id: "svc_admin",
scopes: ["admin:*"],
claims: {},
},
mtlsRequired: true,
},
],
headerName: "X-Service-Auth",
prefix: "Token",
}),
createJwtResolver({ issuer: "https://idp/..." }),
]);
const result = await chain.resolve(request);
```
## API Reference
- `createBearerStaticResolver(options: BearerStaticResolverOptions): SessionResolver`
Returned resolver:
- `resolve(request: Request): Promise<Principal | null>`
- `dispose(): Promise<void>` — idempotent.
## Implementation Notes
`extractToken` accepts the header value, trims, splits on the first space,
and lower-cases the scheme for the prefix comparison. Empty credentials
after the prefix are rejected. The implementation reads the configured
header by exact name (the Web `Headers` API normalises case on lookup, so
`Authorization` and `authorization` both work). When the table contains
the same token twice with conflicting principals, `findTokenIndex` returns
the first index — operator misconfiguration the schema cannot prevent.
TypeScript cannot narrow `table[idx]` to non-undefined even though
`findTokenIndex` only ever returns `-1` or a valid in-range index; a type
assertion encodes that contract at the call site without adding a runtime
guard the test would have to exercise.

View File

@@ -0,0 +1,79 @@
# mTLS gate — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The mTLS gate enforces an optional second factor on individual token table
entries. When operators mark an entry with `mtlsRequired: true`, the
resolver MUST refuse to surface the entry's Principal unless the host
listener attached a verified peer certificate to the inbound request via
`request.tls.peerCertificate`. This guards the highest-privilege service
tokens (e.g. internal admin API) against credential leakage — the bearer
token alone is necessary but no longer sufficient.
The adapter does NOT establish TLS itself. The host listener (Bun's
TLS-enabled server, Node's `https`, a Cloudflare-Worker TLS terminator
ahead of the worker, etc.) is responsible for completing the TLS handshake
and surfacing the verified subject as `request.tls.peerCertificate`. This
module just inspects what was attached.
## Features
- Pure function — no I/O, no state.
- Defensive — accepts any `Request`-shaped input even when the `tls` field
was not attached (returns `undefined`).
- Treats `peerCertificate: null` identically to absent.
- `mtlsRequired === false` short-circuits without inspecting the request.
## Usage: Basic
```ts
import { checkMtlsGate } from "@suaveplan/api-auth-bearer-static/mtls";
const request = new Request("https://example/test");
const allowed = checkMtlsGate(request, true);
// allowed === false — no peerCertificate attached
```
## Usage: Advanced
```ts
import {
checkMtlsGate,
extractPeerCertificate,
} from "@suaveplan/api-auth-bearer-static/mtls";
// Host listener pattern — wrap the inbound request before handing off to
// the auth chain.
const withTls = Object.assign(request, {
tls: { peerCertificate: parsedX509Subject },
});
const cert = extractPeerCertificate(withTls);
// cert === parsedX509Subject
const allowed = checkMtlsGate(withTls, true);
// allowed === true
```
## API Reference
- `interface RequestTlsInfo { peerCertificate?: unknown }`
- `extractPeerCertificate(request: Request): unknown | undefined`
- `checkMtlsGate(request: Request, mtlsRequired: boolean): boolean`
## Implementation Notes
The `Request` Web type has no standardised `tls` field. Both Node's
undici-based `Request` and Bun's polyfill ignore unknown properties on
construction, so the host pattern is to attach `tls` as a plain property
after the Request is built (or to wrap it in a Proxy / subclass). The
extractor reads through that property with a single defensive type cast
and returns `undefined` for any shape that does not satisfy the
`{ peerCertificate: <truthy> }` invariant. The opaque certificate itself
is not parsed here — that is the host's job, and a future
`api-auth-mtls` adapter (out of scope for Stage 4) will own subject
extraction.

View File

@@ -0,0 +1,96 @@
# Options — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The options module owns the public construction surface of every resolver
in this package. It defines the `BearerStaticResolverOptions` interface
(plus its row type `BearerStaticTokenEntry`), the Zod schema that mirrors
the interface, and a thin parser that turns operator-supplied input into a
validated value or throws `AUTH_BEARER_STATIC.INVALID_OPTIONS`.
Validation runs once at factory construction. Hot-path resolve calls never
re-validate — by then the table is structurally trusted. Token rotation is
intentionally out of scope: operators rotate by reloading host config,
which re-runs the factory and re-runs validation.
## Features
- `tokens` is required and non-empty (`.min(1)`).
- Every entry validates token (non-empty string), `principal` shape (id +
scopes + claims, optional tenantId), and optional `mtlsRequired` flag.
- `headerName` and `prefix` are optional with documented defaults
(`Authorization` / `Bearer`).
- Failure surfaces a typed `BearerStaticAuthError` carrying the original
`ZodError` as `cause` — full diagnostic trail is preserved.
## Usage: Basic
```ts
import { parseBearerStaticResolverOptions } from "@suaveplan/api-auth-bearer-static/options";
const opts = parseBearerStaticResolverOptions({
tokens: [
{
token: "service-cron-2026",
principal: {
id: "svc_cron",
scopes: ["jobs:run"],
claims: { kind: "service" },
},
},
],
});
```
## Usage: Advanced
```ts
import {
parseBearerStaticResolverOptions,
bearerStaticResolverOptionsSchema,
} from "@suaveplan/api-auth-bearer-static/options";
import { AUTH_BEARER_STATIC_CODES } from "@suaveplan/api-auth-bearer-static/error-codes";
// Validate without constructing — useful for config UIs that show errors
// inline before committing changes.
const result = bearerStaticResolverOptionsSchema.safeParse(userInput);
if (!result.success) {
renderInlineErrors(result.error);
}
// Or rely on the throwing parser when integrated into a startup pipeline:
try {
const opts = parseBearerStaticResolverOptions(userInput);
return opts;
} catch (e) {
if (
e &&
typeof e === "object" &&
"code" in e &&
e.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
) {
crashWithConfigExitCode();
}
throw e;
}
```
## API Reference
- `interface BearerStaticTokenEntry { token: string; principal: Principal; mtlsRequired?: boolean }`
- `interface BearerStaticResolverOptions { tokens: ReadonlyArray<BearerStaticTokenEntry>; headerName?: string; prefix?: string }`
- `bearerStaticResolverOptionsSchema: z.ZodType<BearerStaticResolverOptions>`
- `parseBearerStaticResolverOptions(raw: unknown): BearerStaticResolverOptions`
## Implementation Notes
The schema's `principal` block deliberately accepts strings rather than
`PrincipalId` / `Scope` brands. Brands are erased at runtime and the host
trusts operator-supplied config to be well-formed; rejecting strings that
look fine at runtime would be developer-hostile noise. Downstream code
treats the returned shape as a `Principal` because the structural fields
match.

View File

@@ -0,0 +1,110 @@
# Worker-proxy factory — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The worker-proxy surface is the second of the two Stage-4 surfaces this
adapter ships (`§5.2a`). It lets the host install the bearer-static
resolver in the main process while the actual token table lives inside an
isolated worker. The host's `SessionResolverChain` calls
`resolve(request)` exactly as for the direct surface; under the hood, the
proxy marshals the request across a `MessagePort` using
`@suaveplan/api-worker-rpc`, the worker's resolver runs, and the
`Principal | null` decision is returned to the host.
The proxy intentionally keeps the token table out of the host's address
space — useful when the host is sandboxed and the worker holds privileged
credentials, or when operators want a single auth daemon serving multiple
host workers.
## Features
- Bidirectional RPC over a `MessagePort` (or any `PortLike`) — no
process / address-space assumptions.
- Idempotent `dispose()` — coalesces multiple calls and closes the
underlying RPC client exactly once.
- Header set, request method, URL, and (optional) `peerCertificate` cross
the boundary; opaque body and abort signal are out of scope (resolution
does not need them).
- Server-side mount via `serveBearerStaticResolverOnPort` — owns the real
resolver and the token table.
- Passes the same `runAuthSessionConformance` harness as the direct
surface.
## Usage: Basic
```ts
import {
createBearerStaticResolverWorkerProxy,
serveBearerStaticResolverOnPort,
} from "@suaveplan/api-auth-bearer-static/proxy";
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
const { clientPort, serverPort } = createInProcessPortPair();
// Worker side — owns the real table.
const server = serveBearerStaticResolverOnPort(serverPort, {
tokens: [
{
token: "T1",
principal: { id: "svc", scopes: [], claims: {} },
},
],
});
// Host side — sees only a proxy.
const proxy = createBearerStaticResolverWorkerProxy(clientPort);
const request = new Request("https://api/x", {
headers: { Authorization: "Bearer T1" },
});
const principal = await proxy.resolve(request);
await proxy.dispose();
server.close("done");
```
## Usage: Advanced
```ts
import { Worker, MessageChannel } from "node:worker_threads";
import {
createBearerStaticResolverWorkerProxy,
} from "@suaveplan/api-auth-bearer-static/proxy";
// Real Node worker boundary. The worker script imports
// `serveBearerStaticResolverOnPort` and calls it against its own port.
const { port1, port2 } = new MessageChannel();
const worker = new Worker("./auth-worker.mjs", { transferList: [port2] });
const resolver = createBearerStaticResolverWorkerProxy(port1 as never);
// Place in chain alongside direct adapters — host treats both uniformly.
import { SessionResolverChain } from "@suaveplan/api-auth";
const chain = new SessionResolverChain([resolver]);
```
## API Reference
- `serveBearerStaticResolverOnPort(port: PortLike, options: BearerStaticResolverOptions): { close(reason?: string): void }`
— mount the server-side handlers.
- `createBearerStaticResolverWorkerProxy(port: PortLike): SessionResolver`
— host-side proxy.
## Implementation Notes
The marshalled envelope is `{url, method, headers, peerCertificate?}` —
the minimal subset of `Request` that the bearer-static decision actually
needs. `peerCertificate` is structured-clone-friendly because the host
attaches whatever shape the listener surfaced; tests pass plain objects.
The proxy never sends the request body, because token resolution does not
consult it. Errors thrown by the server-side resolver (e.g. an unexpected
runtime fault) propagate as the standard `WORKER_RPC` error envelope and
surface in the host as a thrown `WorkerRpcError` — the host's resolver
chain treats this as a fail-closed `RESOLVER_THREW` per the documented
`api-auth` chain semantics. `dispose()` calls the remote `dispose` first
to drain the server-side state, then closes the local client so the
underlying port can be garbage-collected.

View File

@@ -0,0 +1,71 @@
# Security scheme contribution — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module builds the OpenAPI 3.1 `components.securitySchemes`
contribution the adapter publishes to the host's `AuthAdapter` registry.
The generated entry declares an `http` / `bearer` scheme so OpenAPI tooling
(Stoplight, ReadMe, Postman import, etc.) and the host's emitter render
the canonical "Authorize" affordance.
The contribution is content-only: it does NOT register routes, mount
middleware, or affect runtime behaviour. It exists solely so the OpenAPI
document the host emits is correct and discoverable.
## Features
- OAS 3.1 compliant — emits the `http` discriminator with `scheme: bearer`.
- `name` defaults to `bearerStaticAuth` and is overridable at construction.
- Optional `bearerFormat` and `description` fields pass through verbatim.
- Validated against the `OpenApiSecuritySchemeSchema` Zod schema published
by `@suaveplan/api-auth/security-scheme` — if this module ever drifts,
the conformance tests in `api-auth` fail.
## Usage: Basic
```ts
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
const contrib = securitySchemeContribution();
// { name: "bearerStaticAuth",
// scheme: { type: "http", scheme: "bearer" } }
```
## Usage: Advanced
```ts
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
import { validateSecurityScheme } from "@suaveplan/api-auth";
// Custom name + bearerFormat for an admin-only deployment that publishes
// two static-token adapters (cron + admin) side by side under distinct
// scheme keys.
const adminScheme = securitySchemeContribution({
name: "bearerStaticAdminAuth",
description: "Static admin token. mTLS-gated. Rotate via host config reload.",
bearerFormat: "opaque",
});
// Defensive — confirm the contribution still passes the api-auth schema.
validateSecurityScheme(adminScheme);
```
## API Reference
- `interface SecuritySchemeContributionOptions { name?: string; description?: string; bearerFormat?: string }`
- `securitySchemeContribution(options?: SecuritySchemeContributionOptions): SecuritySchemeContribution`
— pure factory; the result is structurally a
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
## Implementation Notes
`bearerFormat` is purely informational per OAS — values like `JWT`,
`opaque`, or vendor-specific strings appear in some doc tooling but never
affect runtime validation. The contribution intentionally omits the
optional fields when undefined rather than emitting `undefined` values,
which would round-trip through `JSON.stringify` as missing keys but
trigger noisy diffs in machine-comparison tooling.

View File

@@ -0,0 +1,76 @@
# OTel instruments — `@suaveplan/api-auth-bearer-static`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The telemetry module owns the single OpenTelemetry counter this package
emits — `api.auth.bearer_static_match_total` — partitioned by the
documented `outcome` attribute. Every call to a resolver's `resolve()`
method increments this counter exactly once with one of four label values.
The instrument is bound lazily against `metrics.getMeter(...)` so the
package is safe to import on the cold path before the host installs a
`MeterProvider`. Resetting the cached bundle (via
`resetBearerStaticInstruments`) is required in test setup to bind against
a freshly-installed in-memory exporter.
## Features
- Single counter — `api.auth.bearer_static_match_total`.
- Four `outcome` label values: `match`, `no-token`, `unknown-token`,
`mtls-required`.
- Lazy meter resolution — module load does not trigger an OTel side
effect.
- Idempotent reset hook for test isolation.
## Usage: Basic
```ts
import { recordMatchOutcome } from "@suaveplan/api-auth-bearer-static/src/telemetry";
// Inside a resolver's resolve() body:
recordMatchOutcome("unknown-token");
```
## Usage: Advanced
```ts
import {
getBearerStaticInstruments,
resetBearerStaticInstruments,
} from "@suaveplan/api-auth-bearer-static/src/telemetry";
// In a vitest setup file that has installed an in-memory MeterProvider
// upstream:
beforeEach(() => {
resetBearerStaticInstruments();
});
it("emits the outcome attribute we expect", async () => {
const inst = getBearerStaticInstruments();
inst.matchTotal.add(1, { outcome: "match" });
// ...then flush + assert on the exporter
});
```
## API Reference
- `interface BearerStaticInstruments { matchTotal: Counter }`
- `type BearerStaticMatchOutcome = "match" | "no-token" | "unknown-token" | "mtls-required"`
- `getBearerStaticInstruments(): BearerStaticInstruments`
- `resetBearerStaticInstruments(): void`
- `recordMatchOutcome(outcome: BearerStaticMatchOutcome): void`
## Implementation Notes
The instrument name uses dotted notation per OpenTelemetry semantic
conventions; the attribute key is `outcome` rather than the host-wide
`auth.outcome` since this counter is adapter-scoped and dashboards aggregate
across adapters by metric name, not attribute. The `description` field is
human-readable only — OTel collectors do not interpret it. The cached bundle
exists per process and is intentionally global; resolvers do not own
instruments because every concrete resolver in a chain would otherwise
double-publish the same counter under the same scope.

View File

@@ -0,0 +1,172 @@
# @suaveplan/api-auth-better-auth — Better-Auth adapter for the @suaveplan/api foundation. Ships two independently-wireable factories implementing the auth.session and auth.provider capability contracts against a Better-Auth instance backed by its own Postgres connection. Maps Better-Auth user+session onto the @suaveplan/api Principal contract, mounts canonical PROVIDER_ROUTE_SURFACE routes (translating Better-Auth's /sign-up etc. idiom), exposes a GitHub OAuth flow via Better-Auth's genericOAuth plugin (so the mock OAuth IdP can be wired end-to-end), guards route-prefix collisions against @suaveplan/api-host's reserved-prefix contract, and emits OTel sessions/oauth/mapping instruments. The ./testing subpath re-exports both conformance suites bound to Postgres-backed factories.
Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two
**independently-wireable** capability surfaces — `createBetterAuthSession`
(implements `auth.session` from `@suaveplan/api-auth`) and
`createBetterAuthProvider` (implements `auth.provider` from
`@suaveplan/api-auth-provider`). Operators MAY wire either side in
isolation OR both together; sharing one options bag between the two
factories shares the underlying Better-Auth instance + DB pool.
> **Two capability surfaces, wire independently.** The session adapter
> reads cookies + bearer tokens; the provider adapter mounts the canonical
> `PROVIDER_ROUTE_SURFACE` (9 routes) under a configurable prefix. They
> share no required code path — wiring only one side does NOT install the
> other side's surface.
<!-- begin:auto-docs -->
## Documentation tree
- [db](./src/db/database.md) — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
- [database](./src/db/database.md) — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
- [error-codes](./src/error-codes/error-codes.md) — Stable, tracked error-code registry for `@suaveplan/api-auth-better-auth`. Every fault path inside the adapter…
- [error-codes](./src/error-codes/error-codes.md) — Stable, tracked error-code registry for `@suaveplan/api-auth-better-auth`. Every fault path inside the adapter…
- [independence](./src/independence/session-without-provider.md) — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
- [session-without-provider](./src/independence/session-without-provider.md) — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
- [instruments](./src/instruments/instruments.md) — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
- [instruments](./src/instruments/instruments.md) — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
- [default-factory](./src/oauth/default-factory.md) — Default `betterAuthFactory` used when an operator does not supply one via `BetterAuthOptions.betterAuthFactory`. Dynami…
- [github](./src/oauth/github.md) — Project Suaveplan's {@link BetterAuthOauthProvider} entries onto the `genericOAuth` plugin config Better-Auth consumes.…
- [options](./src/options/options.md) — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both `createBetterAuthSession` and `createBette…
- [options](./src/options/options.md) — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both `createBetterAuthSession` and `createBette…
- [principal](./src/principal/map-user-to-principal.md) — Default Better-Auth `(user, session)` → `Principal` mapping consumed by both `createBetterAuthSession` and `createBette…
- [map-user-to-principal](./src/principal/map-user-to-principal.md) — Default Better-Auth `(user, session)` → `Principal` mapping consumed by both `createBetterAuthSession` and `createBette…
- [create-provider](./src/provider/create-provider.md) — Factory for the `auth.provider` half of the Stage-6 split. Returns an `AuthProviderAdapter` (per `@suaveplan/api-auth-p…
- [route-translation](./src/provider/route-translation.md) — Translation table mapping every entry in `PROVIDER_ROUTE_SURFACE` (the canonical 9-route foundation surface) to the mat…
- [route-prefix](./src/route-prefix/route-prefix.md) — Resolve an operator-supplied `routePrefix` against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
- [route-prefix](./src/route-prefix/route-prefix.md) — Resolve an operator-supplied `routePrefix` against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
- [session](./src/session/create-session.md) — Factory for the `auth.session` half of the Stage-6 split. Returns a `SessionResolver` that asks Better-Auth's `auth.api…
- [create-session](./src/session/create-session.md) — Factory for the `auth.session` half of the Stage-6 split. Returns a `SessionResolver` that asks Better-Auth's `auth.api…
- [testing](./src/testing/testing.md) — `@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages import when they want to assert their own…
- [testing](./src/testing/testing.md) — `@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages import when they want to assert their own…
<!-- end:auto-docs -->
## Modules
- **`error-codes/`** — 10-code `AUTH_BA.*` registry + `BetterAuthError`.
- **`options/`** — Zod-validated `BetterAuthOptions` boundary + canonical
defaults.
- **`db/`** — `LazyBetterAuth` (lazy + cached + concurrency-safe) +
`buildBetterAuthFactoryArgs` (outer-options → Better-Auth-args
projection).
- **`principal/`** — default Better-Auth `(user, session)` → `Principal`
mapping; replaceable via `BetterAuthOptions.principalMapping`.
- **`session/`** — `createBetterAuthSession(opts)` factory implementing
the `SessionResolver` contract.
- **`provider/`** — `createBetterAuthProvider(opts)` factory implementing
the `AuthProviderAdapter` contract; mounts canonical routes that rewrite
onto Better-Auth's native sub-paths.
- **`route-prefix/`** — `assertRoutePrefixAvailable` collision check
against `@suaveplan/api-host/reserved-prefix`.
- **`oauth/`** — `genericOAuth` plugin config builder + default
`betterAuth(...)` factory that dynamically loads the plugin when OAuth
providers are configured. The GitHub OAuth flow is the canonical
reference; the implementation works against the real GitHub IdP in
production and the mock IdP from
`@suaveplan/testing-integration-oauth-mock` in tests because we use
`genericOAuth` (which accepts the full URL triple) rather than the
hard-coded `github` social-provider.
- **`instruments/`** — OTel counters + histograms per the spec's metrics
contract.
- **`independence/`** — invariant pin: session-only / provider-only wiring
exposes nothing from the other side.
- **`testing/`** — `./testing` subpath re-exporting both conformance
harnesses bound to Better-Auth factories.
## Usage
```ts
import { Pool } from "pg";
import {
createBetterAuthProvider,
createBetterAuthSession,
} from "@suaveplan/api-auth-better-auth";
const pool = new Pool({ connectionString: process.env.BETTER_AUTH_DB_URL! });
const opts = {
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
routePrefix: "/auth",
oauth: {
providers: {
github: {
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
},
},
},
};
// Wire both sides — they share one Better-Auth instance via per-options WeakMap:
host.registerAuth({
resolver: createBetterAuthSession(opts),
csrf: true,
});
host.registerAuthProvider(createBetterAuthProvider(opts));
```
## Error codes
| Code | Surface | When thrown |
| --- | --- | --- |
| `AUTH_BA.INVALID_SESSION` | session | unexpected `getSession` failure |
| `AUTH_BA.DB_UNREACHABLE` | db | `betterAuth({...})` construction threw |
| `AUTH_BA.OAUTH_PROVIDER_REJECTED` | oauth | provider returned 403 |
| `AUTH_BA.OAUTH_STATE_MISMATCH` | oauth | state cookie did not match |
| `AUTH_BA.USER_NOT_FOUND` | mapping | custom mapper signalled missing user |
| `AUTH_BA.EMAIL_NOT_VERIFIED` | session | session present but email unverified |
| `AUTH_BA.RATE_LIMITED` | provider | Better-Auth rate-limiter blocked |
| `AUTH_BA.INVALID_REDIRECT` | oauth | redirect URI outside allow-list |
| `AUTH_BA.PRINCIPAL_MAPPING_FAILED` | mapping | mapper threw |
| `AUTH_BA.ROUTE_PREFIX_COLLISION` | mount | prefix collides with host-reserved |
## Conformance
Conformance tests live at:
- `src/session/create-session.spec-conformance.test.ts` — drives
`runAuthSessionConformance` against a Postgres-shaped stubbed
`BetterAuthLike` (the contract asserts the abstract `SessionResolver`
invariants; DB backing is orthogonal).
- `src/provider/create-provider.spec-conformance.test.ts` — drives
`runAuthProviderConformance` against `createBetterAuthProvider` with the
same stubbing topology.
- `tests/integration/postgres-better-auth.test.ts` — real Postgres
container + real Better-Auth + register/login/resolve. Gracefully skips
with a logged reason when Docker / `pg` / schema bootstrap is unavailable
in the runner environment.
## Architecture
```
host config
│
▼
┌─────────────────────────────┐
│ BetterAuthOptions (Zod) │ parseBetterAuthOptions(...)
└──────────────┬──────────────┘
│
per-opts WeakMap
│
▼
┌──────────────┐
│ LazyBetterAuth│ ── betterAuth({ ...args, plugins: [genericOAuth(...)] })
└─────┬────────┘
│ shared (single connection pool)
┌───────┴────────┐
▼ ▼
createBetterAuthSession createBetterAuthProvider
(SessionResolver) (AuthProviderAdapter)
│ │
│ ├── mounts /register, /login, /logout, /refresh,
│ │ /oauth/:provider/start, /oauth/:provider/callback,
│ │ /verify-email/:token, /forgot-password, /reset-password
▼ ▼
Principal auth.handler(rewrittenRequest)
```

View File

@@ -0,0 +1,83 @@
# `database`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Lazy holder for the underlying Better-Auth instance plus a helper that
projects {@link BetterAuthOptions} onto the loosely-typed argument bag
`betterAuth(...)` accepts. The lazy pattern matters because Better-Auth's
constructor opens its own DB connection pool — we MUST NOT open it twice
when both the session and provider factories are wired against the same
options, and we MUST NOT do it eagerly at module import time because that
would make `import "@suaveplan/api-auth-better-auth"` block on a network
round-trip.
## Features
- `LazyBetterAuth.get()` builds the Better-Auth instance on first call,
caches it, and serves concurrent callers from the same in-flight promise.
- `LazyBetterAuth.reset()` drops the cache (tests only).
- Factory failures wrapped in `BetterAuthError(AUTH_BA.DB_UNREACHABLE)` with
the underlying error as `cause`.
- In-flight slot is cleared on failure so a transient outage doesn't
permanently poison the cache.
- `buildBetterAuthFactoryArgs(opts)` maps our outer options onto
Better-Auth's shape so the wrapper isn't sprinkled with shape translation.
## Usage: Basic
```ts
import { LazyBetterAuth } from "@suaveplan/api-auth-better-auth";
import { betterAuth } from "better-auth";
const lazy = new LazyBetterAuth(opts, (args) => betterAuth(args));
const auth = await lazy.get();
const response = await auth.handler(request);
```
## Usage: Advanced
```ts
import { LazyBetterAuth, buildBetterAuthFactoryArgs } from "@suaveplan/api-auth-better-auth";
import { betterAuth } from "better-auth";
// Tests can swap in a stubbed factory that records the args + returns a
// stubbed BetterAuthLike. The wrapper's behaviour is exercised without
// standing up a real database.
const captured: Record<string, unknown>[] = [];
const lazy = new LazyBetterAuth(opts, (args) => {
captured.push(args);
return { handler: async () => new Response("ok"), api: { getSession: async () => null } };
});
// Inspect the args the wrapper would have sent to `betterAuth(...)`:
expect(buildBetterAuthFactoryArgs(opts)).toMatchObject({
database: opts.database,
baseURL: opts.baseUrl,
});
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `LazyBetterAuth` | class | One instance per options bag; lazy + cached |
| `buildBetterAuthFactoryArgs(opts)` | `(BetterAuthOptions) => Record<string, unknown>` | Outer-options → Better-Auth-args projection |
## Implementation Notes
The `inflight` slot is the linchpin of the concurrency guarantee — the
first caller to enter `get()` writes a single promise; every subsequent
caller observes that same promise via the second `if`. On rejection the
`finally` clears `inflight` so the next call can retry; on resolution
`this.cached` is set, and the `inflight` clear is harmless. The
session-vs-advanced split in `buildBetterAuthFactoryArgs` mirrors
Better-Auth 1.4's option topology — `session.cookieName` is configured under
`session`, but `sameSite` / `secure` live under
`advanced.defaultCookieAttributes`. Operators wanting the full Better-Auth
option surface (passkeys, magic links, organisation plugin) supply their own
`betterAuthFactory` and call `betterAuth({ ...buildBetterAuthFactoryArgs(opts), plugins: [...] })`
themselves.

View File

@@ -0,0 +1,88 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable, OpenSpec-tracked error-code registry for `@suaveplan/api-auth-better-auth`.
Every fault path inside the adapter — session resolution, database access,
OAuth handshake, principal mapping, route-prefix collision detection — terminates
in a {@link BetterAuthError} carrying one of the ten codes defined here. Codes
are namespaced under `AUTH_BA.*` via the package's `defineCodes` helper so the
full literal value never collides with sibling adapters and consumers can branch
on a single discriminated value.
## Features
- 10 namespaced codes covering session, database, OAuth, mapping and mount-time
failure surfaces.
- `BetterAuthErrorCode` literal-union derived from the code bag — narrows
exhaustively in `switch`.
- `BetterAuthError` class extending `SuaveplanError` so `instanceof` and
structural matching (`err.betterAuthCode`) both work.
## Usage: Basic
```ts
import {
AUTH_BA_CODES,
BetterAuthError,
} from "@suaveplan/api-auth-better-auth/error-codes";
if (!session) {
throw new BetterAuthError(
AUTH_BA_CODES.INVALID_SESSION,
"session resolver returned no principal"
);
}
```
## Usage: Advanced
```ts
import {
AUTH_BA_CODES,
BetterAuthError,
type BetterAuthErrorCode,
} from "@suaveplan/api-auth-better-auth/error-codes";
function describe(code: BetterAuthErrorCode): string {
switch (code) {
case AUTH_BA_CODES.OAUTH_STATE_MISMATCH:
return "Tampered or stale state parameter — restart the flow";
case AUTH_BA_CODES.OAUTH_PROVIDER_REJECTED:
return "The upstream IdP rejected our token request";
default:
return "Unhandled Better-Auth adapter failure";
}
}
try {
await router.handle(request);
} catch (err) {
if (err instanceof BetterAuthError) {
logger.warn(describe(err.betterAuthCode), { code: err.code, cause: err.cause });
}
throw err;
}
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `AUTH_BA_CODES` | `Readonly<Record<Name, "AUTH_BA.${Name}">>` | Frozen bag of every documented code |
| `BetterAuthErrorCode` | string-literal union | The exact set of values `code` may take |
| `BetterAuthError` | class extending `SuaveplanError` | Throwable carrying a `BetterAuthErrorCode` |
## Implementation Notes
`defineCodes("AUTH_BA", [...])` from `@suaveplan/error` is the only way to mint
codes — it freezes the bag and produces the string-literal type. Adding a new
code requires (1) an OpenSpec change against the `api-auth-better-auth`
capability so the spec delta lists the new code, (2) editing the tuple here,
and (3) editing every consumer `switch` block. The redundant
`betterAuthCode` field mirrors `code` so transport boundaries that drop the
prototype chain (worker, IPC) can still match structurally.

View File

@@ -0,0 +1,77 @@
# `session-without-provider`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pin the design invariant from The spec change: the Better-Auth
adapter ships two factories — `createBetterAuthSession` and
`createBetterAuthProvider` — and operators MUST be able to wire either side
without dragging in the other. Wiring only the session adapter (keeping
account-lifecycle elsewhere — e.g. Clerk or a custom rolled provider) is the
"hosted IdP for accounts, Better-Auth for session reads against a shared
cookie" topology the design doc §2.3 calls out. Wiring only the provider
adapter (no Better-Auth session resolution) is rarer but legal — the host
fills the `auth.session` slot with whatever other resolver is configured.
## Features
- Sibling test file pins the invariant by:
- Asserting `createBetterAuthSession(opts)` returns a `SessionResolver`
with no `routes` / `pathPrefix` field.
- Asserting `createBetterAuthProvider(opts)` returns an
`AuthProviderAdapter` with no `resolve` / `dispose` field.
- Walking the recorded route list when `routes(...)` IS invoked to verify
every canonical name lands.
- `betterAuthFactories` constant re-exports both factories as a single bag
so consumer documentation can name the pair without repeating imports.
## Usage: Basic
```ts
import { betterAuthFactories } from "@suaveplan/api-auth-better-auth";
const resolver = betterAuthFactories.session(opts);
const provider = betterAuthFactories.provider(opts);
```
## Usage: Advanced
```ts
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
import { ClerkSessionResolver } from "third-party/clerk-suaveplan-adapter";
// Topology: Clerk owns sign-up / sign-in routes; Better-Auth reads the
// shared session cookie via createBetterAuthSession against a shadow
// Better-Auth DB that mirrors the Clerk user store. Provider side NOT wired.
host.registerAuth({
resolver: new SessionResolverChain({
resolvers: [
createBetterAuthSession({ database: pool, baseUrl, secret }),
new ClerkSessionResolver({ clerkApiKey: process.env.CLERK_KEY! }),
],
}),
});
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `betterAuthFactories` | `Readonly<{ session, provider }>` | Convenience bag pairing both factories |
## Implementation Notes
The module is intentionally near-empty — the invariant lives in the test
file. The thin re-export bag is the only runtime contribution because the
OpenSpec scaffolding rule requires every implementation file to have a
sibling `.md` AND a paired test file. The independence is enforced
structurally by TypeScript anyway — `SessionResolver` and
`AuthProviderAdapter` have disjoint shapes — but the test pin guards against
a refactor that accidentally adds an `if-not-mounted-mount-the-other-side`
side effect inside either factory. The OpenSpec spec delta lists this file
as the verification for the "session-only / provider-only wiring exposes
nothing from the other side" scenario, so the test names match the spec
exactly.

View File

@@ -0,0 +1,83 @@
# `instruments`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter.
Built once per process against the current global `MeterProvider` and cached.
Tests call {@link resetBetterAuthInstruments} after installing a fresh
`MeterProvider` so assertions only see the instruments emitted by the unit
under test.
## Features
- Four canonical instruments matching the spec's OTel surface contract:
`auth.better_auth.sessions_resolved_total`,
`auth.better_auth.session_resolve_duration_ms`,
`auth.better_auth.oauth_flows_total`,
`auth.better_auth.principal_mapping_errors_total`.
- Singleton-per-process pattern so consumers can call
`getBetterAuthInstruments()` from anywhere without worrying about meter
proliferation.
- Reset helper for test isolation.
## Usage: Basic
```ts
import { getBetterAuthInstruments } from "@suaveplan/api-auth-better-auth/instruments";
const ins = getBetterAuthInstruments();
ins.sessionsResolvedTotal.add(1, { outcome: "resolved" });
ins.sessionResolveDurationMs.record(elapsedMs, { outcome: "resolved" });
```
## Usage: Advanced
```ts
import {
getBetterAuthInstruments,
resetBetterAuthInstruments,
} from "@suaveplan/api-auth-better-auth/instruments";
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
// In test setup
const provider = new MeterProvider({ /* reader, ... */ });
metrics.setGlobalMeterProvider(provider);
resetBetterAuthInstruments(); // force the cache to rebind to the new provider
// In test
const ins = getBetterAuthInstruments();
ins.oauthFlowsTotal.add(1, { provider: "github", outcome: "state_mismatch" });
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `BetterAuthInstruments` | interface | Bundle of `Counter` / `Histogram` instances |
| `getBetterAuthInstruments()` | `() => BetterAuthInstruments` | Lazy cached accessor |
| `resetBetterAuthInstruments()` | `() => void` | Drop the cache (tests only) |
Instrument labels:
| Instrument | Labels | Semantics |
| --- | --- | --- |
| `auth.better_auth.sessions_resolved_total` | `outcome ∈ "resolved" \| "null" \| "error"` | Session-resolver call count |
| `auth.better_auth.session_resolve_duration_ms` | `outcome` (same) | Duration histogram |
| `auth.better_auth.oauth_flows_total` | `provider`, `outcome ∈ "success" \| "state_mismatch" \| "provider_rejected"` | OAuth flow conclusion count |
| `auth.better_auth.principal_mapping_errors_total` | none | Mapper-threw count |
## Implementation Notes
The meter is acquired via `metrics.getMeter("@suaveplan/api-auth-better-auth")`
to match the OpenTelemetry naming convention (instrument scope = package
name). Caching is critical — calling `meter.createCounter(name, ...)` more
than once for the same `name` produces a stable instrument under the OTel
spec, but the SDK MAY emit a warning, and re-binding the cached objects on
every `add()` call would generate measurable overhead. The reset helper
exists purely for tests because the global `MeterProvider` IS the cache key
that decides whether the cached instruments still point at the live provider.

View File

@@ -0,0 +1,78 @@
# `default-factory`
## Purpose
Default `betterAuthFactory` used when an operator does not supply one via
`BetterAuthOptions.betterAuthFactory`. Dynamically imports `better-auth`
AND (when OAuth providers are configured) the `genericOAuth` plugin, then
calls `betterAuth({...})` with the assembled args. The dynamic imports
mean module-load of `@suaveplan/api-auth-better-auth` never blocks on
`better-auth` — operators that ship a stubbed factory (unit tests, custom
adapter shells) avoid the upstream package altogether.
## Features
- Dynamic `import("better-auth")` keeps cold-start fast for stub-only
consumers.
- Dynamic `import("better-auth/plugins/generic-oauth")` only fires when the
options actually configure OAuth providers.
- The Suaveplan-private `__suaveplan_generic_oauth` sentinel key from
`buildBetterAuthFactoryArgs` is stripped before forwarding so the upstream
package never sees our internal fields.
- Pre-existing `plugins` arrays the operator may pass through their own
factory are preserved (the genericOAuth plugin is appended, not assigned).
## Usage: Basic
```ts
// Production wiring — pass options without a betterAuthFactory and the
// default fires implicitly:
const provider = createBetterAuthProvider({
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
});
```
## Usage: Advanced
```ts
import { defaultBetterAuthFactory } from "@suaveplan/api-auth-better-auth";
// Operator-supplied factory that wraps the default to inject the
// `organization` Better-Auth plugin on top of whatever the wrapper itself
// already configures:
const customFactory = async (args: Record<string, unknown>) => {
const auth = await defaultBetterAuthFactory(args);
return auth;
};
const provider = createBetterAuthProvider({
database: pool,
baseUrl,
secret,
betterAuthFactory: customFactory,
});
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `defaultBetterAuthFactory(args)` | `(Record<string, unknown>) => Promise<BetterAuthLike>` | Dynamic loader + plugin assembler |
## Implementation Notes
The sentinel-key pattern (`__suaveplan_generic_oauth`) is the trade-off
between (a) keeping `buildBetterAuthFactoryArgs` synchronous and pure, and
(b) actually loading the upstream plugin module. The args bag is built
synchronously so callers and tests can inspect it; the plugin module is
loaded lazily inside this factory. Stripping the sentinel keeps the
boundary clean — Better-Auth's option validator complains about unknown
top-level keys.
The `plugins` array preservation matters because operators MAY supply a
custom factory that adds Better-Auth's own plugins (organisation, passkeys,
2FA, magic-link) on top of the OAuth config the wrapper itself injects. By
appending we ensure a stub operator factory + the wrapper's genericOAuth
config coexist correctly.

View File

@@ -0,0 +1,81 @@
# `github`
## Purpose
Project Suaveplan's {@link BetterAuthOauthProvider} entries onto the
`genericOAuth` plugin config Better-Auth consumes. We use `genericOAuth`
(not the bundled `github` social-provider) for a deliberate reason: the
social-provider hard-codes GitHub's production token endpoint URL, which
cannot be redirected onto a mock IdP — `@suaveplan/testing-integration-oauth-mock`'s
`createMockOauthIdp({ userShape: "github" })` exposes a real-HTTP server
shaped like GitHub, but the social-provider would still hit GitHub.com.
`genericOAuth` accepts the full URL triple (`authorizationUrl`, `tokenUrl`,
`userInfoUrl`) so the same wrapper code drives both production GitHub and
the mock fixture — the only difference is the URL set the operator
configures.
## Features
- Default scope sets: `read:user user:email` for `providerId: "github"`,
`openid email profile` for any other provider.
- Operator-supplied `scopes` always wins.
- `providerName` plumbed onto Better-Auth's `name` field when set; omitted
otherwise.
- Returns `undefined` from `buildGenericOauthPluginArgs` when no providers
are configured so the caller skips wiring the plugin entirely.
## Usage: Basic
```ts
import { buildGenericOauthPluginArgs } from "@suaveplan/api-auth-better-auth";
const pluginArgs = buildGenericOauthPluginArgs({
github: {
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
},
});
```
## Usage: Advanced
```ts
import { buildGenericOauthProviderConfig } from "@suaveplan/api-auth-better-auth";
import { createMockOauthIdp } from "@suaveplan/testing-integration-oauth-mock";
// Integration test: same provider shape, mock URLs.
const idp = await createMockOauthIdp({ userShape: "github" });
const cfg = buildGenericOauthProviderConfig({
providerId: "github",
clientId: "test-cid",
clientSecret: "test-csec",
authorizationUrl: `${idp.url}/authorize`,
tokenUrl: `${idp.url}/token`,
userInfoUrl: `${idp.url}/userinfo`,
});
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `buildGenericOauthProviderConfig(p)` | `(BetterAuthOauthProvider) => Record<string, unknown>` | Single provider → genericOAuth config entry |
| `buildGenericOauthPluginArgs(providers)` | `(Record<string, BetterAuthOauthProvider> \| undefined) => Record<string, unknown> \| undefined` | Full bag → `{ config: [...] }` |
## Implementation Notes
The default GitHub scopes mirror the upstream `github` social-provider's
defaults — `read:user` for the `/user` endpoint, `user:email` for
`/user/emails` (Better-Auth's `genericOAuth` plugin only hits the
configured `userInfoUrl`, but real GitHub still expects the broader scope
set when the operator later switches to the native provider). The
OIDC-compliant default (`openid email profile`) covers Google, Okta, Keycloak
and most other discovery-doc-flavoured IdPs and was the safest "I don't
know which provider, give me an email and a stable id" fallback. The
`providerId` field is the discriminant the upstream plugin matches against
the URL params — `:provider` in `/oauth/:provider/start` MUST match an
entry's `providerId` for the route to resolve.

View File

@@ -0,0 +1,105 @@
# `options`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod-validated boundary for the Better-Auth adapter's configuration bag. Both
`createBetterAuthSession` and `createBetterAuthProvider` accept the same
{@link BetterAuthOptions} shape so a single options object can be threaded
through host configuration even when the operator wires only one side of the
auth split. Validation fires at factory-construction time so misconfiguration
fails fast at boot instead of surfacing as a confusing runtime error on the
first inbound request.
## Features
- Required fields — `database` (opaque pass-through to Better-Auth),
`baseUrl` (URL-validated), `secret` (>= 16 chars).
- Optional fields — `routePrefix` (`/auth` by default, must start with `/`),
`oauth.providers` (per-provider URL-validated client config),
`session` (cookie tuning), `principalMapping` (pluggable user → Principal).
- Test hook — `betterAuthFactory` lets unit tests stub the underlying
`betterAuth(...)` call without standing up a real database.
## Usage: Basic
```ts
import { parseBetterAuthOptions } from "@suaveplan/api-auth-better-auth/options";
const opts = parseBetterAuthOptions({
database: pool, // Better-Auth-shaped value (pg.Pool, Kysely dialect, …)
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
});
```
## Usage: Advanced
```ts
import { parseBetterAuthOptions } from "@suaveplan/api-auth-better-auth/options";
import type { Principal } from "@suaveplan/api-types/principal";
const opts = parseBetterAuthOptions({
database: pool,
baseUrl: "https://api.example.com",
routePrefix: "/api/auth",
secret: process.env.BETTER_AUTH_SECRET!,
oauth: {
providers: {
github: {
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ["read:user", "user:email"],
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
},
},
},
session: { cookieName: "ba.session", cookieSameSite: "lax", cookieSecure: true },
principalMapping: ({ user }): Principal => ({
id: user.id as never,
scopes: (user.roles ?? []) as never[],
claims: { email: user.email },
}),
});
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `BetterAuthOptionsSchema` | Zod schema | Validates the full options object |
| `parseBetterAuthOptions(raw)` | `(unknown) => BetterAuthOptions` | Parse + throw on failure |
| `DEFAULT_ROUTE_PREFIX` | `"/auth"` | Default mount prefix |
| `DEFAULT_COOKIE_NAME` | `"better-auth.session"` | Default cookie name |
| `BetterAuthOptions` | interface | Outer options shape |
| `BetterAuthOauthProvider` | interface | Per-provider OAuth config |
| `BetterAuthSessionOptions` | interface | Cookie tuning bag |
| `BetterAuthUserView` / `BetterAuthSessionRecord` / `BetterAuthSessionView` | interfaces | Mapper input shape |
| `BetterAuthLike` | interface | Structural slice of `better-auth`'s `Auth` |
| `PrincipalMapping` | function type | `(view) => Principal` |
## Implementation Notes
`database` is intentionally typed as `unknown` and the schema only checks
non-null/non-undefined — Better-Auth itself supports `pg.Pool`, Kysely
`Dialect`, `D1Database`, `DBAdapterInstance` and several other shapes; the
wrapper never inspects this value beyond passing it through to
`betterAuth({ database })`. This keeps the wrapper one-driver-agnostic and
lets operators bring their own connection topology (transaction-pool, single
client, serverless adapter, …) without the wrapper opinionating on it.
`secret` enforces a 16-character floor because Better-Auth uses it for cookie
signing and shorter secrets are trivially brute-forced. The exact constant is
informational — Better-Auth itself would still accept shorter strings — so
the strict check here is a defence-in-depth guard at the wrapper boundary.
`betterAuthFactory` is the only sanctioned way to inject a stubbed
Better-Auth instance into the adapter. Production callers MUST omit it so the
real `betterAuth(...)` import drives the runtime; tests use it to swap in a
synchronous in-memory stub conforming to {@link BetterAuthLike}.

View File

@@ -0,0 +1,77 @@
# `map-user-to-principal`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Default Better-Auth `(user, session)` → `Principal` mapping consumed by both
`createBetterAuthSession` and `createBetterAuthProvider`. Pure function; given
the same view it always returns the same `Principal`. Operators MAY supply a
custom `principalMapping` in `BetterAuthOptions` to project Better-Auth's
user row onto a custom claims topology, but the default is good enough for
most deployments — it drops sensitive fields, brands `id` and `scopes`, and
prefers `session.activeOrganizationId` over `user.tenantId` so the
organisation plugin's tenant scoping wins automatically when wired.
## Features
- `id` ← `user.id` branded as `PrincipalId`.
- `scopes` ← `user.roles` branded as `Scope[]` (empty array when absent).
- `tenantId` ← `session.activeOrganizationId` → `user.tenantId` fallback.
- `claims` ← rest of `user` minus sensitive fields; `session_id` appended.
- Throws `BetterAuthError(AUTH_BA.PRINCIPAL_MAPPING_FAILED)` on missing id.
- Returned `Principal` and its `claims` are frozen.
## Usage: Basic
```ts
import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-auth-better-auth/principal";
const principal = mapBetterAuthUserToPrincipal({
user: { id: "u_1", email: "a@b.c", roles: ["admin"] },
session: { id: "s_1" },
});
// → { id: "u_1", scopes: ["admin"], claims: { id, email, roles, session_id } }
```
## Usage: Advanced
```ts
import type { Principal } from "@suaveplan/api-types/principal";
import type { PrincipalMapping } from "@suaveplan/api-auth-better-auth/options";
const customMapping: PrincipalMapping = ({ user, session }): Principal => ({
id: `tenant_${session.activeOrganizationId}_${user.id}` as never,
scopes: ((user.roles ?? []) as readonly string[]).flatMap((role) =>
role === "admin"
? ["billing:write", "billing:read", "users:write"]
: [role]
) as never[],
claims: { email: user.email, source: "better-auth" },
});
// Hand the custom mapping through BetterAuthOptions:
// { ..., principalMapping: customMapping }
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `mapBetterAuthUserToPrincipal(view)` | `PrincipalMapping` | Default mapping consumed by both factories |
## Implementation Notes
The frozen-object return shape (`Object.freeze` on both the `Principal` and
its `claims` bag) defends against handlers downstream mutating a single
returned principal and observing the mutation on later requests — the same
hardening applied by `@suaveplan/api-auth-static-principal`. The sensitive
field list (`password`, `passwordHash`, `salt`, `twoFactorSecret`,
`magicLinkToken`, `resetPasswordToken`) covers Better-Auth's stock plugin
set; operators who define additional sensitive fields via a custom
Better-Auth plugin SHOULD supply a custom `principalMapping` to drop those
too. The mapping deliberately does NOT translate roles into a stable
scope vocabulary — that's a deployment concern; the default is identity so
the topology stays predictable.

View File

@@ -0,0 +1,110 @@
# `create-provider`
## Purpose
Factory for the `auth.provider` half of The split. Returns an
`AuthProviderAdapter` (per `@suaveplan/api-auth-provider`) that mounts the
canonical `PROVIDER_ROUTE_SURFACE` (9 routes) under `routePrefix` (default
`/auth`). Each handler internally rewrites the incoming `Request` onto the
Better-Auth-native sub-path via `ROUTE_TRANSLATION` and delegates to
`auth.handler(...)`. This indirection means downstream consumers of the
provider see the canonical foundation surface — never Better-Auth's idioms —
while Better-Auth still owns the precise wire response (cookies, 302
redirects, error envelopes).
## Features
- `id: "better-auth"`; `pathPrefix` validated as `AuthPathPrefix`.
- All 9 canonical route names registered:
- `POST /register` → `POST /sign-up/email`
- `POST /login` → `POST /sign-in/email`
- `POST /logout` → `POST /sign-out`
- `POST /refresh` → `GET /get-session`
- `GET /oauth/:provider/start` → `POST /oauth2/sign-in/:provider`
- `GET /oauth/:provider/callback` → `GET /oauth2/callback/:provider`
- `GET /verify-email/:token` → `GET /verify-email`
- `POST /forgot-password` → `POST /forget-password`
- `POST /reset-password` → `POST /reset-password`
- `routePrefix` collision-checked against
`@suaveplan/api-host/reserved-prefix` — refused with
`AUTH_BA.ROUTE_PREFIX_COLLISION` on collision.
- Better-Auth instance materialised during `lifecycle.install` so DB
unreachable errors fail before traffic.
- OAuth flow conclusions emit `oauth_flows_total{provider,outcome}`.
- Per-options `WeakMap` so the same Better-Auth instance is shared with
`createBetterAuthSession(opts)` when wired with the same options object.
## Usage: Basic
```ts
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
const provider = createBetterAuthProvider({
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
});
// Mount via the host's plugin lifecycle:
host.registerAuthProvider(provider);
```
## Usage: Advanced
```ts
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
// Operators MUST share the SAME options bag between the two factories to
// share the Better-Auth instance (and thus the DB pool):
const opts = {
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
routePrefix: "/api/auth",
oauth: {
providers: {
github: {
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ["read:user", "user:email"],
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
},
},
},
};
host.registerAuth({
resolver: createBetterAuthSession(opts),
csrf: true,
});
host.registerAuthProvider(createBetterAuthProvider(opts));
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `createBetterAuthProvider(opts)` | `(BetterAuthOptions) => AuthProviderAdapter` | Build the provider |
| `ROUTE_TRANSLATION` | `Record<ProviderRouteName, NativeRoute>` | Re-exported for callers wanting to introspect the mapping |
| `buildNativeUrl(args)` | function | Re-exported for advanced wrappers |
## Implementation Notes
The provider's `routes(router, ctx)` mounts every canonical entry from
`PROVIDER_ROUTE_SURFACE` with `auth: null` because the routes are mounted
INSIDE the auth provider's own prefix and the routes themselves are the
authentication flow — they MUST be reachable unauthenticated. The handler's
inner rewrite reads `(ctx as { params })` because the abstract
`RequestContext` doesn't declare `params`, but the request pipeline driver
(`@suaveplan/api-host/request-pipeline-driver.ts`) attaches them at runtime.
The cast is documented inline. The OAuth outcome heuristic maps status codes
to labels — 2xx/3xx → `success`, 403 → `provider_rejected`, anything else
→ `state_mismatch`. The semantic split between "state_mismatch" and
"provider_rejected" is intentional even though a single 400 covers both
wire-wise: the conformance suite distinguishes the two via the underlying
mock IdP's `primeError` API. The per-options `WeakMap` is keyed by object
identity, not deep equality — sharing requires literal same-object reuse.

View File

@@ -0,0 +1,75 @@
# `route-translation`
## Purpose
Translation table mapping every entry in `PROVIDER_ROUTE_SURFACE` (the
canonical 9-route foundation surface) to the matching Better-Auth-native
sub-path. The wrapper presents the canonical names outward while Better-Auth
still drives the heavy lifting at its idiomatic paths
(`/sign-up/email`, `/sign-in/email`, `/oauth2/sign-in/:providerId`, ...).
Without this translation a host built on the foundation contracts would
need to know Better-Auth-specific URLs — which would defeat the purpose of
the abstract `auth.provider` capability.
## Features
- Single immutable map keyed by `ProviderRouteName`.
- Carries both `nativePath` and `nativeMethod` so the wrapper can rewrite
the request method too (some translations flip GET → POST).
- `buildNativeUrl` helper rewrites the canonical request URL onto
Better-Auth's base URL, resolving `:provider` / `:token` params and
preserving the query string.
## Usage: Basic
```ts
import { ROUTE_TRANSLATION, buildNativeUrl } from "@suaveplan/api-auth-better-auth";
const target = ROUTE_TRANSLATION.login;
// → { nativePath: "/sign-in/email", nativeMethod: "POST" }
const url = buildNativeUrl({
baseUrl: "https://api.example.com",
nativePath: target.nativePath,
params: {},
search: "redirect=/home",
});
// → URL { https://api.example.com/sign-in/email?redirect=/home }
```
## Usage: Advanced
```ts
import { buildNativeUrl, ROUTE_TRANSLATION } from "@suaveplan/api-auth-better-auth";
// OAuth callback — the canonical `/oauth/:provider/callback?code=xxx&state=yyy`
// is rewritten to Better-Auth's `/oauth2/callback/:provider?...` surface.
const target = ROUTE_TRANSLATION["oauth.callback"];
const native = buildNativeUrl({
baseUrl: "https://api.example.com",
nativePath: target.nativePath,
params: { provider: "github" },
search: "code=xxx&state=yyy",
});
// → URL { https://api.example.com/oauth2/callback/github?code=xxx&state=yyy }
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `NativeRoute` | interface | `{ nativePath, nativeMethod }` |
| `ROUTE_TRANSLATION` | `Readonly<Record<ProviderRouteName, NativeRoute>>` | Canonical → native map |
| `buildNativeUrl(args)` | function | Rewrite the canonical URL onto Better-Auth's base URL |
## Implementation Notes
`Object.freeze` on the outer record + a `Readonly<...>` cast — but not on
the inner entries because each entry is already a frozen literal authored
inline. Decided against `as const` on the outer block so the indexed key
narrowing stays type-friendly under `noPropertyAccessFromIndexSignature`.
`buildNativeUrl` runs `:param` substitution before constructing the URL so
malformed param values can't break out of the path; `encodeURIComponent`
defends against `/` injection in the `:provider` slot. Query string
preservation is critical for OAuth — the entire upstream callback payload
(`code`, `state`, `scope`, …) needs to land on Better-Auth's handler verbatim.

View File

@@ -0,0 +1,79 @@
# `route-prefix`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Resolve an operator-supplied `routePrefix` against the host's reserved-prefix
contract from `@suaveplan/api-host/reserved-prefix`. This guard ensures the
Better-Auth provider cannot accidentally mount its account-lifecycle routes
inside paths the host (or another first-party engine) reserves for itself —
`/_host` (admin sub-router) and `/internal` (livez/readyz/metrics) today,
plus whatever future entries land in `HOST_RESERVED_PREFIXES`. The check is
performed at provider construction time so misconfiguration fails before the
process is accepting traffic.
## Features
- Normalised prefix returned on success (lowercase, leading slash, no
trailing slash) so downstream code can use it without re-normalising.
- Collision detection covers both exact matches (`/internal`) and child
paths (`/internal/livez`).
- Reserved list NOT hard-coded — sourced from `@suaveplan/api-host` so the
wrapper inherits future additions for free.
- Throws `BetterAuthError(AUTH_BA.ROUTE_PREFIX_COLLISION)` with the
normalised collision string in `context.prefix`.
## Usage: Basic
```ts
import { assertRoutePrefixAvailable } from "@suaveplan/api-auth-better-auth";
const prefix = assertRoutePrefixAvailable("/auth"); // → "/auth"
```
## Usage: Advanced
```ts
import { assertRoutePrefixAvailable } from "@suaveplan/api-auth-better-auth";
import { BetterAuthError, AUTH_BA_CODES } from "@suaveplan/api-auth-better-auth/error-codes";
try {
const prefix = assertRoutePrefixAvailable(operatorConfig.routePrefix);
return mountProvider({ prefix });
} catch (err) {
if (
err instanceof BetterAuthError &&
err.code === AUTH_BA_CODES.ROUTE_PREFIX_COLLISION
) {
// The deploy script can report the collision back to the operator
// before traffic is accepted.
reportFatalConfigError({ requested: operatorConfig.routePrefix, code: err.code });
}
throw err;
}
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `assertRoutePrefixAvailable(prefix)` | `(string) => string` | Validates + returns normalised prefix |
The function throws `BetterAuthError(AUTH_BA.ROUTE_PREFIX_COLLISION)` when
the prefix collides with `HOST_RESERVED_PREFIXES` from
`@suaveplan/api-host/reserved-prefix`.
## Implementation Notes
The lazy `codeFactory` callback pattern from `@suaveplan/api-host`'s
`assertPrefixAvailable` API is critical here — the factory is only invoked on
collision, so the `BetterAuthError` allocation (with its stack trace and
context bag) is skipped on the happy path. The reserved-prefix module also
exports `normalisePrefix` separately; this wrapper calls it to compute the
returned canonical form because `assertPrefixAvailable` returns `void`. This
matches the pattern in `@suaveplan/api-plugin-rbac` and other adapter
packages that need both the throw-on-collision behaviour and the normalised
output value at the call site.

View File

@@ -0,0 +1,94 @@
# `create-session`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory for the `auth.session` half of The split. Returns a
`SessionResolver` that asks Better-Auth's `auth.api.getSession({ headers })`
to materialise the current session and then maps the resulting `(user,
session)` tuple onto a `Principal` per `@suaveplan/api-types`. Returns `null`
when Better-Auth reports no session — the host's resolver chain advances to
the next adapter (typically `api-auth-anonymous`).
## Features
- Implements the `SessionResolver` contract from `@suaveplan/api-auth`.
- Pluggable `principalMapping` via `BetterAuthOptions.principalMapping`;
default `mapBetterAuthUserToPrincipal`.
- Lazy + cached Better-Auth instance — shared with
`createBetterAuthProvider` when both factories receive the same options
object (per-`opts` WeakMap).
- OTel instruments emitted on every resolve: `sessions_resolved_total{outcome}`,
`session_resolve_duration_ms{outcome}`,
`principal_mapping_errors_total`.
- Stable failure modes: `INVALID_SESSION`, `DB_UNREACHABLE`,
`PRINCIPAL_MAPPING_FAILED`.
- Disposable — `dispose()` is idempotent and turns subsequent `resolve()`
calls into `null` returns.
## Usage: Basic
```ts
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
const resolver = createBetterAuthSession({
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
});
// Inside the host's resolver chain:
const principal = await resolver.resolve(request);
```
## Usage: Advanced
```ts
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous";
const resolver = new SessionResolverChain({
resolvers: [
createBetterAuthSession(opts),
createAnonymousResolver(),
],
});
// The Better-Auth resolver fires first; if it returns `null`, the
// anonymous tail takes over and the host's `publicRoutes` policy applies.
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `createBetterAuthSession(opts)` | `(BetterAuthOptions) => SessionResolver` | Build the resolver |
The resolver throws `BetterAuthError` with one of:
- `AUTH_BA.INVALID_SESSION` — unexpected upstream error from Better-Auth's
`getSession`.
- `AUTH_BA.DB_UNREACHABLE` — first-resolve build failed (wraps the
underlying connection error as `cause`).
- `AUTH_BA.PRINCIPAL_MAPPING_FAILED` — `principalMapping` threw; a partial
principal is never returned.
## Implementation Notes
`getSession` is consumed via the structural `BetterAuthLike` view declared
in `../options/options.types.ts`. That view is intentionally narrow — only
`handler` and `api.getSession` are referenced — so the unit tests can stub
Better-Auth without touching the real runtime. The per-options WeakMap
(`lazyByOptions`) is the linchpin of the "share an instance between session
and provider" guarantee: passing the same `opts` object to both factories
threads through the same `LazyBetterAuth`. The OTel `finally` block runs on
every code path so the counters reflect every attempt, including failures
that propagate as `BetterAuthError`. `dispose()` resets the cached
Better-Auth instance via `lazy.reset()` and flips a disposed flag that
neutralises any subsequent `resolve()` call. The default Better-Auth factory
dynamically imports `better-auth` so consumers wiring a `betterAuthFactory`
override avoid the cost of loading the upstream package altogether.

View File

@@ -0,0 +1,90 @@
# `testing`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages
import when they want to assert their own wiring layer still meets the
abstract `auth.session` and `auth.provider` contracts on top of this
adapter. It re-exports both conformance harnesses
(`runAuthSessionConformance` from `@suaveplan/api-auth` and
`runAuthProviderConformance` from `@suaveplan/api-auth-provider`) alongside
the two Better-Auth factories from this package, so a single import gets
the consumer everything they need.
## Features
- Re-exports `runAuthSessionConformance` + `runAuthProviderConformance` from
the contract packages so consumers don't have to add those packages to
their own dependency closure.
- Re-exports `createBetterAuthSession` + `createBetterAuthProvider`
so the conformance call sites can drop the factory inline.
- `AuthSessionConformanceFactory` / `AuthProviderConformanceFactory` /
`AuthProviderConformanceOptions` type aliases for explicit typing.
## Usage: Basic
```ts
import {
createBetterAuthProvider,
runAuthProviderConformance,
} from "@suaveplan/api-auth-better-auth/testing";
const results = runAuthProviderConformance({
label: "my-host's Better-Auth wiring",
factory: () => createBetterAuthProvider(myOptions),
});
for (const scenario of results) {
it(`${scenario.category} :: ${scenario.name}`, () => {
expect(scenario.passed).toBe(true);
});
}
```
## Usage: Advanced
```ts
import {
createBetterAuthSession,
runAuthSessionConformance,
type AuthSessionConformanceFactory,
} from "@suaveplan/api-auth-better-auth/testing";
const spec: AuthSessionConformanceFactory = {
label: "Postgres-backed Better-Auth + my custom mapping",
factory: () => createBetterAuthSession(myOptionsWithCustomMapping),
seedNull: () => new Request("https://api.example.com/anon"),
seedPrincipal: () => ({
request: new Request("https://api.example.com/me", {
headers: { cookie: "better-auth.session=valid" },
}),
expected: knownPrincipal,
}),
};
runAuthSessionConformance(spec);
```
## API Reference
| Export | Source | Purpose |
| ------ | ------ | ------- |
| `runAuthSessionConformance` | `@suaveplan/api-auth` | Session contract harness |
| `runAuthProviderConformance` | `@suaveplan/api-auth-provider` | Provider contract harness |
| `createBetterAuthSession` | this package | Session factory under test |
| `createBetterAuthProvider` | this package | Provider factory under test |
| `AuthSessionConformanceFactory` | type | Spec passed to session harness |
| `AuthProviderConformanceFactory` | type | Factory signature for provider harness |
| `AuthProviderConformanceOptions` | type | Options for provider harness |
## Implementation Notes
The subpath is intentionally light — most of the value is just having one
import path consumers can pull from. The conformance harnesses themselves
live in the contract packages because that's where the abstract invariants
are authored; this package's contribution to "passing conformance" is the
factory pair. Consumers test their own wiring layer (custom middleware,
host configuration, plugin order) by invoking the harnesses with their
own options bag and asserting every `ConformanceScenarioResult.passed`.

View File

@@ -0,0 +1,135 @@
# @suaveplan/api-auth-jwt — concrete JWT session resolver for @suaveplan/api-auth. Verifies a JWT presented on the Authorization header (or a configurable cookie) using @suaveplan/jws against either a JWKS endpoint (TTL + negative-cache + in-flight dedup) or a single static JsonWebKey; projects validated claims onto a Principal via an overridable mapper. Returns null on parse/verify failure (chain semantics) and contributes a Bearer JWT OpenAPI security scheme.
Stage-4 concrete `SessionResolver` for the
[`@suaveplan/api`](../api/README.md) plugin system. Verifies a JWT
presented on the `Authorization: Bearer <jwt>` header (or a configured
cookie) against either a remote JWKS endpoint or a single static JWK,
then projects the validated claims onto the foundation's `Principal`
contract via an overridable mapper.
This is the adapter you pair with hosted identity providers (Clerk,
Auth0, Cognito, Stytch, custom Keycloak deployments) or with any
self-hosted JWT-issuing provider. It is **session-resolver only** —
login / register / refresh routes are the responsibility of an `auth`
provider, not this package.
<!-- begin:auto-docs -->
## Documentation tree
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
<!-- end:auto-docs -->
## Surface
- [`./resolver`](src/resolver/resolver.md) — `createJwtSessionResolver(opts)`.
- [`./jwks-cache`](src/jwks-cache/jwks-cache.md) — `JwksCache` with TTL,
negative cache, in-flight dedup, `kid`-miss refresh.
- [`./claims-mapper`](src/claims-mapper/claims-mapper.md) —
`defaultClaimsToPrincipal` + `runClaimsMapper` helpers.
- [`./security-scheme`](src/security-scheme/security-scheme.md) —
`JWT_BEARER_SCHEME` and `createJwtBearerSecurityScheme(name)` for
OpenAPI emission.
- [`./error-codes`](src/error-codes/error-codes.md) — `AUTH_JWT_*` codes
+ `AuthJwtError` class.
- [`./options`](src/options/options.md) — Zod-validated options
boundary.
## Quick start
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
const resolver = createJwtSessionResolver({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
// Use in an api-host AuthAdapter:
const adapter = {
resolver,
securityScheme: JWT_BEARER_SCHEME,
};
```
## Pairing with hosted IdPs
The package is designed to be paired with an external identity provider
that already issues JWTs. The recipe is the same for every provider —
plug in the provider's JWKS URL, issuer, and audience, then add a custom
`claimsToPrincipal` when the provider uses non-standard claim names.
### Clerk
```ts
createJwtSessionResolver({
jwksUrl: "https://your-instance.clerk.accounts.dev/.well-known/jwks.json",
issuer: "https://your-instance.clerk.accounts.dev",
audience: "your-frontend-api",
algorithms: ["RS256"],
claimsToPrincipal: (claims) => {
const sub = claims["sub"];
if (typeof sub !== "string") return null;
return {
id: sub as never,
scopes: [] as never[],
tenantId: typeof claims["org_id"] === "string"
? (claims["org_id"] as never)
: undefined,
claims,
};
},
});
```
### Auth0
```ts
createJwtSessionResolver({
jwksUrl: "https://your-tenant.auth0.com/.well-known/jwks.json",
issuer: "https://your-tenant.auth0.com/",
audience: "https://api.example.com",
algorithms: ["RS256"],
});
```
### Cognito
```ts
createJwtSessionResolver({
jwksUrl: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX/.well-known/jwks.json",
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX",
audience: "your-cognito-app-client-id",
algorithms: ["RS256"],
});
```
## Semantics
`resolve()` returns `null` on any per-request soft failure (missing
token, malformed token, bad signature, wrong issuer, wrong audience,
expired, not-yet-valid, no matching JWKS key). That collapse lets the
upstream `SessionResolverChain` fall through to the next adapter
(anonymous, static-bearer, etc.). It throws only on infrastructure
failures — `AUTH_JWT.JWKS_FETCH_FAILED` when the JWKS endpoint is
unreachable, `AUTH_JWT.INVALID_KEY_FORMAT` for a malformed static key,
`AUTH_JWT.INVALID_CLAIMS_MAPPER` when the caller's mapper throws or
returns a non-Principal value.
## License
MIT — see the repository.

View File

@@ -0,0 +1,84 @@
# `claims-mapper` — `Claims → Principal` projection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Default and runtime helpers for converting a verified JWT payload
(`Claims`) into a `Principal` from `@suaveplan/api-types`. The default
projection follows OAuth 2 / OIDC conventions: `sub` for subject,
space-delimited `scope` (or array-form `scopes`) for authorisation
scopes, and `tenant_id` / `tid` for the tenant. Callers can override
the projection entirely to read non-standard claims (e.g. Clerk's
`org_id`) or to enforce additional invariants — return `null` to defer
to the next resolver in the chain.
## Features
- `defaultClaimsToPrincipal` — drop-in OAuth 2 / OIDC projection.
- `runClaimsMapper` — wraps a caller-supplied mapper; catches throws,
rejects non-Principal returns, surfaces both as
`AUTH_JWT.INVALID_CLAIMS_MAPPER`.
- Returns `null` when the token has no `sub` — that is the documented
"not a user identity" path and lets the chain move on.
- Brands `PrincipalId`, `Scope`, and `TenantId` via type assertion at
the trust boundary; downstream consumers see opaque branded strings.
- Passes the full claims record through as `principal.claims` so
downstream tenant resolvers / authorisation rules can read raw values
without a second token parse.
## Usage: Basic
```ts
import { defaultClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
const principal = defaultClaimsToPrincipal({
sub: "user_123",
scope: "billing:read profile:read",
tenant_id: "acme",
});
// → { id: "user_123", scopes: ["billing:read", "profile:read"], tenantId: "acme", claims: {...} }
```
## Usage: Advanced
Override the projection to read Clerk-style `org_id` and enforce a
non-empty scopes list:
```ts
import type { ClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
const clerkMapper: ClaimsToPrincipal = (claims) => {
const sub = claims["sub"];
const scope = claims["scope"];
const org = claims["org_id"];
if (typeof sub !== "string" || typeof scope !== "string") return null;
return {
id: sub as never,
scopes: scope.split(" ") as never[],
tenantId: typeof org === "string" ? (org as never) : undefined,
claims,
};
};
```
## API Reference
- `ClaimsToPrincipal` — `(claims) => Principal | null`.
- `defaultClaimsToPrincipal: ClaimsToPrincipal` — the built-in projection.
- `runClaimsMapper(mapper, claims): Principal | null` — invoke a mapper
with structured error reporting.
## Implementation Notes
The default mapper deliberately tolerates either string-form (`"a b c"`)
or array-form (`["a", "b", "c"]`) scopes — RFC 6749 §3.3 uses the
former, while several IdPs (Auth0, Cognito) emit the latter. Empty
strings are filtered out so a stray double-space does not produce a
zero-length scope token. Tenant extraction prefers `tenant_id` (the
Suaveplan-canonical name) over `tid` (the Microsoft / OIDC name) but
accepts both. A mapper that throws is treated as an operator bug, not a
caller error — surfacing it as a `null` would silently mask broken
configuration.

View File

@@ -0,0 +1,76 @@
# `error-codes` — `AUTH_JWT_*` stable codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the
`AUTH_JWT.*` namespace via `defineCodes` from `@suaveplan/error` and a
matching `AuthJwtError` subclass of `SuaveplanError`. The registry lists
only the codes the package throws — namely the infrastructure failures an
operator must act on. Per-request soft failures (bad token, expired
signature, wrong audience, etc.) deliberately do not get codes; they
collapse to `null` so that the upstream `SessionResolverChain` can move on
to the next resolver, which is the documented chain semantic.
## Features
- `AUTH_JWT_CODES.JWKS_FETCH_FAILED` — JWKS endpoint unreachable or
returned a non-2xx after the configured retries, and there is no cached
set old enough to use. Operators see this in logs / metrics; callers
see a 401.
- `AUTH_JWT_CODES.INVALID_KEY_FORMAT` — `options.staticKey` failed JWK
schema validation at construction time.
- `AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER` — `options.claimsToPrincipal`
threw or returned a value that is not a `Principal` or `null`.
- `AuthJwtError` carries the matching `AuthJwtErrorCode`, integrates with
the `SuaveplanError` cause-chain, redaction, and OTel span-event
adapters from `@suaveplan/error`.
## Usage: Basic
```ts
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
try {
// ... resolver internals ...
} catch (cause) {
throw new AuthJwtError(
AUTH_JWT_CODES.JWKS_FETCH_FAILED,
"JWKS endpoint unreachable",
{ cause }
);
}
```
## Usage: Advanced
Branch on the code at the boundary to map infrastructure failures to
operator alerts rather than user-facing 401s:
```ts
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
function isOperatorActionable(err: unknown): err is AuthJwtError {
if (!(err instanceof AuthJwtError)) return false;
return err.code === AUTH_JWT_CODES.JWKS_FETCH_FAILED;
}
```
## API Reference
- `AUTH_JWT_CODES` — frozen record produced by `defineCodes("AUTH_JWT", [...])`.
- `AuthJwtErrorCode` — union of the three string literals.
- `AuthJwtError extends SuaveplanError<AuthJwtErrorCode>` — `jwtCode`
mirrors `code` for ergonomic `instanceof` narrowing.
## Implementation Notes
The three-code registry is intentionally small. Adding "soft" failure
codes here would tempt consumers to branch on them, defeating the chain
semantic that `null` means "not my caller". If a richer breakdown is
needed downstream, emit it on the failure counter (`reason` attribute)
rather than as a separate `AUTH_JWT.*` code. Registry membership is the
public contract; only add codes through an OpenSpec change.

View File

@@ -0,0 +1,91 @@
# `jwks-cache` — TTL-bounded JWKS resolution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS
document from a remote endpoint, holds it under a positive TTL,
negative-caches transient failures, dedupes concurrent fetches into a
single network request, and supports forced refresh on a `kid` miss
(the standard "the IdP rotated keys" recovery path). Throws
`AUTH_JWT.JWKS_FETCH_FAILED` only when neither a fresh fetch nor a stale
cached document is available — under partial outages we prefer
stale-but-valid keys to a 401 storm.
## Features
- Positive TTL (default 10 minutes) with monotonic clock injection.
- Negative cache window (default 30 seconds) on fetch failure — within
the window callers see the stale document if any, otherwise the
error.
- In-flight dedup: concurrent `getKeyForKid` callers share a single
network round-trip.
- `forceRefresh()` for operator-triggered rotation.
- `kid` miss against a cached document triggers exactly one refresh.
- JSON shape validation (`{ keys: object[] }`) at the boundary.
## Usage: Basic
```ts
import { JwksCache } from "@suaveplan/api-auth-jwt/jwks-cache";
const cache = new JwksCache({
url: "https://idp.example/.well-known/jwks.json",
ttlMs: 10 * 60 * 1000,
negativeMs: 30 * 1000,
clock: { nowMs: () => Date.now() },
fetchImpl: fetch,
});
const jwk = await cache.getKeyForKid("kid-2024-01");
```
## Usage: Advanced
Inject a fake clock and a controllable fetch impl for tests of the
expiry and dedup behaviours:
```ts
let now = 0;
const calls: number[] = [];
const cache = new JwksCache({
url: "https://idp.example/.well-known/jwks.json",
ttlMs: 1000,
negativeMs: 100,
clock: { nowMs: () => now },
fetchImpl: async () => {
calls.push(now);
return new Response(JSON.stringify({ keys: [{ kid: "a", kty: "RSA", n: "...", e: "AQAB" }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
await cache.getKeyForKid("a");
now = 500;
await cache.getKeyForKid("a"); // cache hit, no second fetch
```
## API Reference
- `class JwksCache` — constructor takes `{ url, ttlMs, negativeMs, clock,
fetchImpl }`.
- `getKeyForKid(kid?: string): Promise<Record<string, unknown> | undefined>`
— fetch-as-needed, returns `undefined` when no key matches.
- `forceRefresh(): Promise<JwksDocument>` — clear cache and refetch.
- `JwksDocument` — `{ keys: ReadonlyArray<Record<string, unknown>> }`.
## Implementation Notes
When the token carries no `kid`, the cache only returns a key when the
JWKS contains exactly one entry — ambiguous lookups must be a 401, not
a guess. The negative cache window is short by design (30s default);
longer windows accumulate authentication outages, shorter windows
hammer the IdP. The in-flight dedup uses a single `Promise` field
rather than a queue — the second concurrent caller awaits the same
promise as the first. The "kid miss after a just-fetched document"
guard prevents infinite refresh loops when the IdP legitimately does
not have a matching key.

View File

@@ -0,0 +1,82 @@
# `options` — `JwtSessionResolverOptions` boundary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod-validated boundary for `createJwtSessionResolver` and the
companion `validateOptions` helper. The boundary enforces every static
contract the runtime depends on: exactly one of `jwksUrl` or `staticKey`,
non-empty `algorithms` allowlist, mandatory `issuer` and `audience`, a
positive cache TTL, and a non-negative negative-cache window. Anything
the schema cannot encode (injectable clock, fetch impl, claims mapper
function) is part of the wider `JwtSessionResolverOptions` TypeScript
type but is not Zod-validated — those are dev-time contracts.
## Features
- Strict-mode object schema rejects unknown keys at construction.
- Mutual exclusion check for `jwksUrl` / `staticKey` — both supplied or
neither supplied throws `AUTH_JWT.INVALID_KEY_FORMAT`.
- Sensible defaults: header name `authorization`, JWKS TTL 10 min,
negative cache 30s, clock tolerance 0.
- Accepts `issuer` and `audience` as either a single string or a
non-empty list of strings — JWS verify enforces membership.
## Usage: Basic
```ts
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
const opts = validateOptions({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
```
## Usage: Advanced
Custom header, cookie fallback, tighter cache TTL, and a custom claims
mapper:
```ts
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
const opts = validateOptions({
staticKey: { kty: "oct", k: "..." },
issuer: ["https://idp1.example", "https://idp2.example"],
audience: "https://api.example",
algorithms: ["HS256"],
headerName: "x-api-token",
cookieName: "sp_session",
clockToleranceSec: 5,
jwksCacheTtlMs: 60_000,
jwksNegativeCacheMs: 5_000,
});
```
## API Reference
- `JwtSessionResolverOptionsSchema` — Zod object schema.
- `JwtSessionResolverOptions` — public surface (includes non-serialisable
fields like `clock`, `fetchImpl`, `claimsToPrincipal`).
- `ValidatedJwtSessionResolverOptions` — inferred shape after defaults
are applied.
- `validateOptions(raw)` — synchronous validator; throws `AuthJwtError`
with code `AUTH_JWT.INVALID_KEY_FORMAT` on any boundary failure.
## Implementation Notes
The schema is intentionally `.strict()` so a typo in a key name fails
loudly at construction rather than silently activating defaults. `iss`
and `aud` are both required because token-level `iss` / `aud` confusion
is a documented attack vector — every Stage-4 deployment is expected to
pin both. `algorithms` is also mandatory and non-empty — `@suaveplan/jws`
already refuses to verify without an allowlist, but we re-enforce it at
the API boundary so the error surface is consistent. The mutual
exclusion check after parsing matches what the spec calls out: a single
key source is the only way to reason about JWKS cache invalidation.

View File

@@ -0,0 +1,108 @@
# `resolver` — `createJwtSessionResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stage-4 concrete `SessionResolver` factory for the API plugin
foundation. Reads a JWT from the request (Authorization header or
configurable cookie), verifies it via `@suaveplan/jws` against either a
JWKS endpoint (cached and refreshed on `kid` miss) or a single static
JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy,
and projects the validated claims onto a `Principal` via an overridable
mapper. The factory is the only public entry point — internals
(`JwksCache`, claims mapper, error registry) are surface-exported for
testability but most consumers use `createJwtSessionResolver(opts)` alone.
## Features
- `Authorization: Bearer <jwt>` header extraction with case-insensitive
scheme match; configurable header name accepts a raw token form for
non-standard headers (e.g. `x-api-token: <jwt>`).
- Cookie fallback when `cookieName` is supplied; header still wins when
both are present.
- JWKS resolution via `JwksCache` with positive TTL, negative cache,
in-flight dedup, and `kid`-miss refresh.
- Static-key path for HS-family symmetric tokens or single-key
deployments.
- Mandatory `iss` and `aud` enforcement (single string or list); `exp`
and `nbf` enforced when present with configurable `clockToleranceSec`.
- Per-request soft failures collapse to `null` so the upstream
`SessionResolverChain` can fall through to the next adapter; operator
failures (JWKS unreachable, malformed mapper, malformed static key)
surface as `AuthJwtError` with the documented codes.
- Idempotent `dispose()` flips an internal flag so subsequent
`resolve()` calls return `null` rather than emitting late traffic on
a torn-down resolver.
## Usage: Basic
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
const req = new Request("https://api.example/users", {
headers: { authorization: "Bearer eyJ..." },
});
const principal = await resolver.resolve(req);
```
## Usage: Advanced
Static HS256 key, cookie fallback, custom claims mapper, and an
injectable clock for deterministic temporal-claim tests:
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
staticKey: { kty: "oct", k: "<base64url-secret>" },
issuer: ["https://idp1.example", "https://idp2.example"],
audience: "https://api.example",
algorithms: ["HS256"],
cookieName: "sp_session",
clockToleranceSec: 5,
clock: { nowMs: () => 1_700_000_000_000 },
claimsToPrincipal: (claims) => {
const sub = claims["sub"];
if (typeof sub !== "string") return null;
return {
id: sub as never,
scopes: [] as never[],
tenantId: typeof claims["org_id"] === "string"
? (claims["org_id"] as never)
: undefined,
claims,
};
},
});
```
## API Reference
- `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver`
— returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` +
idempotent `dispose`).
## Implementation Notes
The resolver deliberately separates "this is not my caller" (chain
falls through, returns `null`) from "the host is mis-configured"
(throws `AuthJwtError`). Verification failures inside `@suaveplan/jws`
return `null` so a stray Bearer header from an unrelated IdP cannot
401-storm the API — the chain's next adapter (anonymous, static key,
etc.) still gets its turn. The temporal-claim check uses a single
clock reading per request to avoid TOCTOU between the `exp` and `nbf`
arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather
than push it into `@suaveplan/jws` because those are JWT-claim
concerns, not JWS-protected-header concerns — the JWS package
deliberately handles only RFC 7515 (signature) and leaves RFC 7519
(payload claims) to its consumers.

View File

@@ -0,0 +1,66 @@
# `security-scheme` — OpenAPI Bearer JWT contribution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Builds the `SecuritySchemeContribution` this adapter contributes to the
generated OpenAPI document. The shape is the canonical OAS 3.1
`http` / `bearer` scheme with `bearerFormat: "JWT"`. Stage-8
`@suaveplan/api-openapi` consumes the contribution and inlines it under
`components.securitySchemes[name]`, then references it from operation
`security` blocks for non-public routes.
## Features
- Canonical Bearer-JWT scheme — no surprise extensions, no proprietary
fields.
- Configurable contribution name (`bearerAuth` default) so multiple JWT
resolvers can coexist (e.g. user-side `bearerAuth` + service-side
`serviceBearerAuth`).
- `JWT_BEARER_SCHEME` constant for callers that only ever expose one
JWT resolver.
- Human-readable `description` field that links the scheme back to the
adapter so docs readers understand where verification happens.
## Usage: Basic
```ts
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
const adapter = {
resolver: createJwtSessionResolver(...).resolver,
securityScheme: JWT_BEARER_SCHEME,
};
```
## Usage: Advanced
Rename the contribution for a dual-resolver host:
```ts
import { createJwtBearerSecurityScheme } from "@suaveplan/api-auth-jwt/security-scheme";
const userScheme = createJwtBearerSecurityScheme("userAuth");
const serviceScheme = createJwtBearerSecurityScheme("serviceAuth");
```
## API Reference
- `createJwtBearerSecurityScheme(name = "bearerAuth")` →
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
- `JWT_BEARER_SCHEME` — pre-built `bearerAuth`-named contribution.
## Implementation Notes
The `http` / `bearer` form is the only OAS 3.1 scheme that documents
"this endpoint accepts a Bearer JWT" without forcing the OpenAPI
emitter to inline OAuth-flow metadata that this adapter does not own.
The `bearerFormat` field is documentation-only per the spec, but
populating it as `"JWT"` is the convention every OAS reader (Swagger
UI, Redoc, Stoplight) recognises. Tenant or scope expectations are
expressed by individual route handlers, not the scheme itself —
keeping the scheme orthogonal to the route catalogue keeps it stable
across plugin upgrades.

View File

@@ -0,0 +1,68 @@
# @suaveplan/api-auth-provider - Abstract auth-provider contract for the Suaveplan API plugin system. Defines AuthProviderAdapter, the canonical provider route surface, path-ownership registry, collision detector, the auth.provider CapabilityId, the AUTH_PROVIDER error-code registry, and the provider conformance harness. $2; concrete providers (better-auth, rolled) ship in Stages 6 and 8.
Abstract account-lifecycle contract for the Suaveplan API plugin system
(contracts). This package is OPTIONAL: hosted-IdP deployments
(Clerk, Auth0, Cognito) install only the session resolver from
`@suaveplan/api-auth`; self-hosted deployments install exactly one provider
implementation (`api-auth-better-auth` , `api-auth-rolled` in
Stage 8) which structurally implements `AuthProviderAdapter` from this
package.
<!-- begin:auto-docs -->
## Documentation tree
- [capability-id](./src/capability-id/capability-id.md) — The `capability-id` module pins the runtime literal for the `"auth.provider"` capability this package contributes to th…
- [capability-id](./src/capability-id/capability-id.md) — The `capability-id` module pins the runtime literal for the `"auth.provider"` capability this package contributes to th…
- [collision-detector](./src/collision-detector/collision-detector.md) — `collision-detector` is the install-time guard that prevents a plugin from accidentally squatting on the auth-provider'…
- [collision-detector](./src/collision-detector/collision-detector.md) — `collision-detector` is the install-time guard that prevents a plugin from accidentally squatting on the auth-provider'…
- [error-codes](./src/error-codes/error-codes.md) — This module centralises the four stable error codes the auth-provider contract emits — `PATH_COLLISION`, `MULTIPLE_PROV…
- [error-codes](./src/error-codes/error-codes.md) — This module centralises the four stable error codes the auth-provider contract emits — `PATH_COLLISION`, `MULTIPLE_PROV…
- [path-ownership](./src/path-ownership/path-ownership.md) — `path-ownership` is the runtime registry the host consults to enforce the "exactly one auth provider per deployment" in…
- [path-ownership](./src/path-ownership/path-ownership.md) — `path-ownership` is the runtime registry the host consults to enforce the "exactly one auth provider per deployment" in…
- [provider](./src/provider/provider.md) — The `provider` module is the type-level + thin-runtime heart of `@suaveplan/api-auth-provider`. It declares the abstrac…
- [provider](./src/provider/provider.md) — The `provider` module is the type-level + thin-runtime heart of `@suaveplan/api-auth-provider`. It declares the abstrac…
- [route-surface](./src/route-surface/route-surface.md) — `route-surface` declares the closed set of paths an auth provider OWNS relative to its `pathPrefix`. The set is the uni…
- [route-surface](./src/route-surface/route-surface.md) — `route-surface` declares the closed set of paths an auth provider OWNS relative to its `pathPrefix`. The set is the uni…
- [testing](./src/testing/conformance.md) — `runAuthProviderConformance` is the runner-agnostic harness every concrete `AuthProviderAdapter` MUST clear. It exercis…
- [conformance](./src/testing/conformance.md) — `runAuthProviderConformance` is the runner-agnostic harness every concrete `AuthProviderAdapter` MUST clear. It exercis…
<!-- end:auto-docs -->
## What ships here
- `AuthProviderAdapter` contract — `id`, `pathPrefix`,
`routes(router, ctx)`, four-hook lifecycle.
- `AuthPathPrefix` brand + Zod-validated `authPathPrefix` constructor.
- `PROVIDER_ROUTE_SURFACE` — the canonical 9-entry route table providers
project from.
- `PathOwnership` runtime registry — enforces "exactly one provider"
install-time invariant.
- `detectAuthPathCollisions` — install-time guard that plugin routes do not
squat on the provider's owned prefix.
- `AUTH_PROVIDER_CAPABILITY_ID` constant — `"auth.provider"`.
- `AUTH_PROVIDER_CODES` — the four-code error registry plus
`AuthProviderError` and the `authProviderError` factory.
- `runAuthProviderConformance` — runner-agnostic harness every concrete
provider clears.
## Module index
- [provider](src/provider/provider.md)
- [route-surface](src/route-surface/route-surface.md)
- [path-ownership](src/path-ownership/path-ownership.md)
- [collision-detector](src/collision-detector/collision-detector.md)
- [capability-id](src/capability-id/capability-id.md)
- [error-codes](src/error-codes/error-codes.md)
- [testing/conformance](src/testing/conformance.md)
## Dependencies
- `@suaveplan/api-types` (workspace) — `AuthProviderAdapter` consumes
`Router`, `PluginContext`, `CapabilityId`, `CapabilityRequirements`.
- `@suaveplan/error` (workspace) — `SuaveplanError` + `defineCodes`.
- `zod` (catalog) — `AuthPathPrefixSchema`.
- `tslib` (catalog) — emitter helpers.
## Stage / status
Concrete providers ship in Stages 6 / 8.

View File

@@ -0,0 +1,78 @@
# `capability-id` — `"auth.provider"` capability contribution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `capability-id` module pins the runtime literal for the
`"auth.provider"` capability this package contributes to the host's
capability registry. The closed `CapabilityId` union in
`@suaveplan/api-types` already includes `"auth.provider"` (alongside
`"auth.session"`); this module simply exposes a typed constant and predicate
so concrete providers, the host's capability resolver, and downstream
plugin manifests can author against a single value rather than a free-form
string. Centralising the literal gives us one place to grep if the id ever
needs to evolve via an OpenSpec change.
## Features
- `AUTH_PROVIDER_CAPABILITY_ID` constant pinned to `"auth.provider"` via a
`satisfies` clause that fails the build if the literal drifts out of the
`CapabilityId` union.
- `isAuthProviderCapabilityId(value)` runtime type-guard for capability
descriptors that flow through `unknown`.
- `AuthProviderCapabilityId` type alias — `Extract<CapabilityId, "auth.provider">` —
so consumers can name the type without re-spelling the literal.
## Usage: Basic
```ts
import { AUTH_PROVIDER_CAPABILITY_ID } from "@suaveplan/api-auth-provider/capability-id";
definePlugin({
requires: [AUTH_PROVIDER_CAPABILITY_ID] as const,
start: async (ctx) => {
// ctx.capabilities["auth.provider"] is now in scope.
},
});
```
## Usage: Advanced
The host's capability resolver inspects untyped descriptors and narrows them
to the typed slot via the predicate:
```ts
import {
AUTH_PROVIDER_CAPABILITY_ID,
isAuthProviderCapabilityId,
} from "@suaveplan/api-auth-provider/capability-id";
function bindCapability(descriptor: { id: string }) {
if (isAuthProviderCapabilityId(descriptor.id)) {
// descriptor.id is now typed as "auth.provider".
registerSlot(AUTH_PROVIDER_CAPABILITY_ID, descriptor);
}
}
```
## API Reference
- `const AUTH_PROVIDER_CAPABILITY_ID: AuthProviderCapabilityId` — frozen
literal.
- `function isAuthProviderCapabilityId(value: string): value is AuthProviderCapabilityId`
- `type AuthProviderCapabilityId = Extract<CapabilityId, "auth.provider">`
## Implementation Notes
The `satisfies` clause is the load-bearing line of the module — if the
`CapabilityId` union ever loses `"auth.provider"`, the build fails here
rather than downstream. The predicate uses an equality test against the
constant rather than the literal to keep the two in lockstep — if a future
change renames the id, the constant and predicate move together. There is
intentionally no companion type-augmentation block here; the union in
`@suaveplan/api-types` is closed and pre-includes every capability the host
knows about, so adding a capability is a co-located OpenSpec change to that
package and not a downstream module-augmentation pattern.

View File

@@ -0,0 +1,88 @@
# `collision-detector` — install-time auth-path collision check
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`collision-detector` is the install-time guard that prevents a plugin from
accidentally squatting on the auth-provider's owned path prefix. It walks
every plugin route, segment-aligns its path against the provider's
`pathPrefix`, and emits `AUTH_PROVIDER.PATH_COLLISION` entries for every
overlap that is not on the plugin's `publicRoutes` allowlist or marked
`isPublic` on the per-route descriptor. Returning a report instead of
throwing keeps callers in control of how to surface aggregated failures —
typically the host turns a non-empty report into a single error listing
every offender.
## Features
- Segment-aware containment — `/auth` matches `/auth` and `/auth/login` but
NOT `/authoritative`.
- Configurable prefix — a deployment that mounts the provider under
`/myauth` shifts the collision domain accordingly.
- Per-route `isPublic` opt-out for plugin routes that augment the auth
surface (e.g. a custom `/auth/healthz`).
- Plugin-level `publicRoutes` allowlist — identical effect, declared once
per plugin manifest.
- Aggregating reporter — every offender is returned in one frozen report.
- Trailing-slash normalisation — `/auth` and `/auth/` are treated as equal.
## Usage: Basic
```ts
import { detectAuthPathCollisions } from "@suaveplan/api-auth-provider/collision-detector";
const report = detectAuthPathCollisions({ provider, plugins });
if (report.hasCollisions) {
throw new Error(`Auth-path collisions: ${report.entries.length}`);
}
```
## Usage: Advanced
Aggregating a problem-detail response across every offender:
```ts
import {
detectAuthPathCollisions,
type CollisionReportEntry,
} from "@suaveplan/api-auth-provider/collision-detector";
const report = detectAuthPathCollisions({ provider, plugins });
if (report.hasCollisions) {
const offenders = report.entries.map(
(e: CollisionReportEntry) => ({ plugin: e.details.plugin, path: e.details.path })
);
return new Response(
JSON.stringify({ type: "auth-collision", offenders }),
{ status: 409 }
);
}
```
## API Reference
- `function detectAuthPathCollisions(input: DetectCollisionsInput): CollisionReport`
- `function isInsidePrefix(routePath: string, prefix: string): boolean`
- `function normalisePrefix(prefix: string): string`
- `interface DetectCollisionsInput { provider; plugins }`
- `interface PluginDescriptor { name; routes; publicRoutes? }`
- `interface PluginRouteDescriptor { path; method?; isPublic? }`
- `interface CollisionReport { entries; hasCollisions }`
- `interface CollisionReportEntry { code; message; details }`
## Implementation Notes
The detector returns a non-throwing report so the host can decide whether a
collision is fatal or merely loggable — empirically a deployment running an
older plugin against a newer provider needs the choice. Segment-aware
matching is essential: a naïve `startsWith` would falsely flag
`/authoritative` (or worse, `/auth-cache`) when the provider owns `/auth`.
The root prefix `"/"` is short-circuited to "owns nothing" so a deployment
that mistakenly configures the auth provider at the root does not blow up
the whole route table; the path-prefix validator catches that mis-configuration
at construction time. Two opt-out paths (per-route `isPublic` and plugin
`publicRoutes`) coexist because manifests historically use one or the other
and supporting both keeps migration tractable.

View File

@@ -0,0 +1,91 @@
# `error-codes` — `AUTH_PROVIDER` error-code registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module centralises the four stable error codes the auth-provider
contract emits — `PATH_COLLISION`, `MULTIPLE_PROVIDERS_REGISTERED`,
`PROVIDER_LIFECYCLE_FAILED`, `OAUTH_CALLBACK_INVALID_STATE` — and pairs each
with HTTP / severity / retry metadata so downstream consumers (problem-detail
mappers, log routers, retry wrappers) classify failures without
string-matching messages. The registry is built with `defineCodes` from
`@suaveplan/error`, which preserves the literal-typed union through the
public surface so exhaustive `switch (err.code)` blocks report unhandled
reasons at type-check time.
## Features
- `AUTH_PROVIDER_CODES` — frozen `{ REASON: "AUTH_PROVIDER.REASON" }` record.
- `AuthProviderErrorCode` union of fully-qualified codes.
- `AuthProviderErrorKey` union of short keys (`"PATH_COLLISION"`).
- `AUTH_PROVIDER_ERROR_METADATA` — per-code HTTP status / severity / retryable
flag / human description.
- `AuthProviderError` — concrete `SuaveplanError` subclass with a public
constructor.
- `authProviderError(key, opts)` — factory that defaults `messageKey` to
`auth_provider.errors.<lower_case_key>`.
- `getAuthProviderCodeMetadata(code)` — lookup helper used by adapters.
## Usage: Basic
```ts
import { authProviderError } from "@suaveplan/api-auth-provider/error-codes";
throw authProviderError("OAUTH_CALLBACK_INVALID_STATE", {
context: { state: requestState },
});
```
## Usage: Advanced
A problem-detail mapper turns any thrown `AuthProviderError` into an HTTP
response with the correct status, retry hint, and severity-driven log level.
```ts
import {
AUTH_PROVIDER_ERROR_METADATA,
AuthProviderError,
} from "@suaveplan/api-auth-provider/error-codes";
function mapToProblem(err: AuthProviderError): Response {
const meta = AUTH_PROVIDER_ERROR_METADATA[err.code];
return new Response(
JSON.stringify({
type: `https://errors.suaveplan/${err.code}`,
title: err.message,
status: meta.httpStatus,
retryable: meta.retryable,
}),
{ status: meta.httpStatus }
);
}
```
## API Reference
- `const AUTH_PROVIDER_CODES`
- `type AuthProviderErrorCode`
- `type AuthProviderErrorKey`
- `interface AuthProviderCodeMetadata`
- `const AUTH_PROVIDER_ERROR_METADATA: Readonly<Record<AuthProviderErrorCode, AuthProviderCodeMetadata>>`
- `class AuthProviderError extends SuaveplanError<AuthProviderErrorCode>` —
public constructor `(code, messageKey, opts?)`.
- `function authProviderError(key, opts?): AuthProviderError`
- `function getAuthProviderCodeMetadata(code: string): AuthProviderCodeMetadata | undefined`
## Implementation Notes
`SuaveplanError`'s base constructor is `protected`; the subclass exposes a
public constructor so callers outside this package — including the
conformance harness — can build typed errors without going through the
factory. The factory is preferred for everyday use because it derives the
i18n message key from the short reason key, keeping the call site dense. The
metadata record is frozen with `Object.freeze` (not just `as const`) so a
runtime caller cannot mutate the table; `satisfies` keeps the table
exhaustive at type-check time. `PATH_COLLISION` maps to HTTP 409 because the
host refuses the install rather than overwriting an existing route; an
adapter that needs a different status should override the mapper rather than
re-shape the registry.

View File

@@ -0,0 +1,83 @@
# `path-ownership` — single-provider registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`path-ownership` is the runtime registry the host consults to enforce the
"exactly one auth provider per deployment" invariant from design §2.3. A
provider claims its `pathPrefix` at install time; any subsequent registration
with a different provider id throws `MULTIPLE_PROVIDERS_REGISTERED`. The
registry is deliberately not a module-global singleton — each host instance
constructs its own `PathOwnership`, which keeps multi-tenant test runners
and hot-reload scenarios deterministic.
## Features
- `PathOwnership` class with `register`, `release`, `current`, `hasProvider`.
- Injectable `clock` — defaults to `Date.now`, used for the `claimedAt`
timestamp on the recorded claim.
- Idempotent registration — re-registering the same instance, or a different
instance with the same provider id, returns the existing claim.
- Strict rejection of a different provider id with
`AUTH_PROVIDER.MULTIPLE_PROVIDERS_REGISTERED`, including a structured
`context` payload (`incomingProviderId`, `incumbentProviderId`,
`prefix`) suitable for diagnostic logs.
- `release()` drops the current claim — used by host shutdown and the
blue-green hot-reload sequence to swap providers atomically.
## Usage: Basic
```ts
import { PathOwnership } from "@suaveplan/api-auth-provider/path-ownership";
const ownership = new PathOwnership();
ownership.register(myProvider);
```
## Usage: Advanced
Tests inject a deterministic clock so the recorded `claimedAt` is stable
across runs:
```ts
import { PathOwnership } from "@suaveplan/api-auth-provider/path-ownership";
const ownership = new PathOwnership({ clock: () => 1_000_000 });
const claim = ownership.register(myProvider);
// claim.claimedAt === 1_000_000
// Hot-reload: release before swapping in the new provider.
ownership.release();
ownership.register(replacementProvider);
```
## API Reference
- `class PathOwnership` — runtime registry.
- `constructor(options?: PathOwnershipOptions)`
- `register(provider): PathOwnershipClaim`
- `release(): void`
- `current(): ProviderClaim | null`
- `hasProvider(): boolean`
- `interface PathOwnershipOptions { clock? }`
- `type OwnershipClock = () => number`
- `interface PathOwnershipClaim { providerId; prefix; claimedAt }`
- `interface ProviderClaim<Provider> { provider; claim }`
## Implementation Notes
The registry stores both the `AuthProviderAdapter` instance and a flat
`PathOwnershipClaim` record. Two layers exist because consumers fall into
two camps: hosts that need the live provider for lifecycle dispatch, and
diagnostic tooling that only needs the metadata. Idempotency is decided by
identity first, id second — so a host that constructs a fresh provider
instance per hot-reload still cannot accidentally register two providers
with the same id. The `MULTIPLE_PROVIDERS_REGISTERED` error carries enough
context for operators to diagnose double-installation in a misconfigured
multi-process deployment without consulting the host logs. The class uses a
private field rather than a module-level variable so multiple registries can
coexist in the same process — essential for runner-agnostic test suites and
multi-tenant in-process deployments.

View File

@@ -0,0 +1,92 @@
# `provider` — `AuthProviderAdapter` contract + path-prefix validation
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `provider` module is the type-level + thin-runtime heart of
`@suaveplan/api-auth-provider`. It declares the abstract `AuthProviderAdapter`
interface every concrete account-lifecycle provider (Better-Auth, the rolled
reference, future Cognito/Auth0 bridges) structurally implements, and ships
the Zod-validated `authPathPrefix` constructor that asserts a deployment's
configured prefix is a legal segment-aligned mount point. By splitting the
contract from any concrete provider we keep this package optional: a
hosted-IdP deployment installs the session resolver (`api-auth`) only and
never pulls in a provider implementation.
## Features
- `AuthProviderAdapter<Caps>` interface with `id`, configurable `pathPrefix`,
`routes(router, ctx)` registration callback, and the four-hook
`AuthProviderLifecycle` quartet (`install`/`start`/`stop`/`uninstall`).
- `AuthPathPrefix` opaque brand — protects against accidental string
substitution at the install boundary.
- `AuthPathPrefixSchema` — a Zod schema enforcing leading slash, no trailing
slash (except `/`), no path parameters, no whitespace.
- `authPathPrefix(value)` constructor — returns the branded value or throws
`AUTH_PROVIDER.PROVIDER_LIFECYCLE_FAILED` with a structured `reason`.
- `isAuthPathPrefix(value)` predicate — non-throwing variant for tests.
- `DEFAULT_AUTH_PATH_PREFIX` constant (`"/auth"`).
## Usage: Basic
```ts
import {
authPathPrefix,
DEFAULT_AUTH_PATH_PREFIX,
type AuthProviderAdapter,
} from "@suaveplan/api-auth-provider/provider";
const myProvider: AuthProviderAdapter = {
id: "rolled",
pathPrefix: authPathPrefix(DEFAULT_AUTH_PATH_PREFIX),
routes(router) {
router.post("/login", { auth: null }, async () => new Response());
},
lifecycle: {
install: async () => {},
start: async () => {},
stop: async () => {},
uninstall: async () => {},
},
};
```
## Usage: Advanced
A deployment that mounts its provider under a non-standard prefix MUST run
the input through `authPathPrefix` so the value is validated before the host
records the claim in the path-ownership registry.
```ts
import { authPathPrefix } from "@suaveplan/api-auth-provider/provider";
const prefix = authPathPrefix(process.env.AUTH_PREFIX ?? "/auth/v2");
// Throws AUTH_PROVIDER.PROVIDER_LIFECYCLE_FAILED on input like
// "/auth/v2/" or "/auth/:tenant".
```
## API Reference
- `interface AuthProviderAdapter<Caps>` — provider contract.
- `interface AuthProviderLifecycle<Caps>` — install/start/stop/uninstall.
- `type AuthPathPrefix` — branded prefix string.
- `const AuthPathPrefixSchema: ZodType` — validation schema.
- `function authPathPrefix(value: string): AuthPathPrefix` — throwing
constructor.
- `function isAuthPathPrefix(value: string): boolean` — predicate.
- `const DEFAULT_AUTH_PATH_PREFIX = "/auth"` — host default.
## Implementation Notes
The brand is purely a TypeScript construct (`unique symbol`) — at runtime
`authPathPrefix("/auth")` returns the string unchanged. The schema's order of
refinements is significant: the leading-slash check fires first so callers
see the most actionable error message. Whitespace is rejected before the
parameter check because shell-supplied configuration is the typical source of
both classes of error. The constructor surfaces failures as
`PROVIDER_LIFECYCLE_FAILED` (not a hypothetical `INVALID_PREFIX`) because the
install lifecycle is the only legitimate call site — keeping the code small
makes the error registry tighter and the metadata table denser.

View File

@@ -0,0 +1,87 @@
# `route-surface` — canonical auth-provider route surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`route-surface` declares the closed set of paths an auth provider OWNS
relative to its `pathPrefix`. The set is the union of the routes any
realistic concrete provider exposes — register, login, logout, refresh, two
OAuth-flow endpoints, email verification, and the forgot/reset-password
pair. Providers MAY implement a subset; the canonical list is the bar the
collision detector measures against and the manifest the OpenAPI emitter
projects when stitching a unified document at build time.
## Features
- `PROVIDER_ROUTE_SURFACE` — frozen array of `ProviderRouteEntry` records,
every entry carrying `path`, `method`, stable `name`, and OpenAPI
`summary`.
- `ProviderRouteName` literal union — exhaustive narrowing in switch blocks.
- `lookupProviderRoute(name)` — name → entry lookup.
- `resolveProviderRoutePath(prefix, entry)` — mount under a given prefix
(handles `"/"` as a no-op).
- `listProviderRouteNames()` — declaration-order name iteration; used by the
conformance harness to verify every canonical name has a scenario.
## Usage: Basic
```ts
import {
PROVIDER_ROUTE_SURFACE,
resolveProviderRoutePath,
} from "@suaveplan/api-auth-provider/route-surface";
for (const entry of PROVIDER_ROUTE_SURFACE) {
console.log(entry.method, resolveProviderRoutePath("/auth", entry));
}
```
## Usage: Advanced
The OpenAPI emitter walks the surface to generate the global `paths` block
for the unified document, projecting `summary` directly into the OpenAPI
operation and using `name` as `operationId` after kebab → camelCase
conversion.
```ts
import {
PROVIDER_ROUTE_SURFACE,
resolveProviderRoutePath,
} from "@suaveplan/api-auth-provider/route-surface";
const openapiPaths: Record<string, unknown> = {};
for (const entry of PROVIDER_ROUTE_SURFACE) {
const path = resolveProviderRoutePath(provider.pathPrefix, entry);
openapiPaths[path] = {
[entry.method.toLowerCase()]: {
operationId: entry.name.replace(/[-.]/g, "_"),
summary: entry.summary,
tags: ["auth"],
},
};
}
```
## API Reference
- `interface ProviderRouteEntry { path; method; name; summary }`
- `type ProviderRouteName` — closed union of 9 names.
- `const PROVIDER_ROUTE_SURFACE: ReadonlyArray<ProviderRouteEntry>`
- `function lookupProviderRoute(name: ProviderRouteName): ProviderRouteEntry | undefined`
- `function resolveProviderRoutePath(prefix: string, entry: ProviderRouteEntry): string`
- `function listProviderRouteNames(): ReadonlyArray<ProviderRouteName>`
## Implementation Notes
The surface is frozen at module-load with two layers of `Object.freeze` —
once around the outer array and once around each entry — to keep downstream
hosts from mutating the table at runtime. `resolveProviderRoutePath` treats
`"/"` as the empty-prefix case so a deployment that mounts the provider at
the host root produces `"/login"` rather than `"//login"`. Adding a new
canonical route requires an OpenSpec change because the conformance harness
and the OpenAPI projector both treat the surface as authoritative; touching
either side without the other is the bug class this list-as-data design
guards against.

View File

@@ -0,0 +1,89 @@
# `testing/conformance` — provider conformance harness
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`runAuthProviderConformance` is the runner-agnostic harness every concrete
`AuthProviderAdapter` MUST clear. It exercises the four critical contract
surfaces — path ownership, canonical route surface, lifecycle quartet, and
collision-detector behaviour — against a fresh provider instance per
scenario. Concrete provider packages call the harness inside a `describe()`
block in their own test file and turn each returned `ConformanceScenarioResult`
into an `it()` assertion. The harness deliberately does not import a test
runner; that responsibility stays at the call site so providers compatible
with `vitest`, `bun:test`, or any other runner can all share the same
behaviour bar.
## Features
- Four-category coverage — `path-ownership`, `route-surface`, `lifecycle`,
`collision`.
- Fresh-provider-per-scenario semantics — the factory is invoked once per
scenario so cross-scenario state cannot leak.
- Records every category and scenario name in the returned array so callers
can verify (in their own tests) that every category is exercised.
- Returns a `passed` boolean per scenario plus an optional `detail` string
for debug context.
- Frozen return value — the caller cannot accidentally mutate the report
while iterating.
## Usage: Basic
```ts
import { describe, expect, it } from "@suaveplan/testing/runner";
import { runAuthProviderConformance } from "@suaveplan/api-auth-provider/testing";
describe("better-auth conformance", () => {
const results = runAuthProviderConformance({
label: "better-auth",
factory: () => buildBetterAuthProvider(),
});
for (const r of results) {
it(`[${r.category}] ${r.name}`, { timeout: 5000 }, () => {
expect(r.passed, r.detail).toBe(true);
});
}
});
```
## Usage: Advanced
A provider whose factory is async (e.g. needs to bring up a Mailpit
testcontainer for verify-email) wraps the synchronous factory the harness
expects and runs setup outside it:
```ts
import { runAuthProviderConformance } from "@suaveplan/api-auth-provider/testing";
const env = await startMailpit();
const results = runAuthProviderConformance({
label: "better-auth-with-mailpit",
factory: () => buildBetterAuthProvider({ smtp: env.connection }),
});
await env.dispose();
```
## API Reference
- `function runAuthProviderConformance(options): ReadonlyArray<ConformanceScenarioResult>`
- `interface AuthProviderConformanceOptions { label; factory }`
- `type AuthProviderConformanceFactory = () => AuthProviderAdapter`
- `type AuthProviderConformanceCategory =
"path-ownership" | "route-surface" | "lifecycle" | "collision"`
- `interface ConformanceScenarioResult { category; name; passed; detail? }`
## Implementation Notes
The harness deliberately drives a synchronous factory because the
information it asserts is structural — `id`, `pathPrefix`, lifecycle quartet
presence, route surface enumeration — and forcing async setup into every
provider creates fixture-management noise where none is needed. Providers
that need real I/O (e.g. database-backed user creation) layer their own
scenarios on top. Each category drives a fresh provider instance, so a
scenario that crashes one provider does not leave global state poisoned for
the next. The `lifecycle` category checks for the four-function quartet by
`typeof === "function"`; a provider whose `stop` hook is missing or
non-callable fails the check loud rather than at runtime.

View File

@@ -0,0 +1,83 @@
# @suaveplan/api-auth-rolled — rolled-our-own auth provider. Implements both `auth.session` and `auth.provider` capability contracts. Session tokens are RS256-signed JWTs (via @suaveplan/crypto-rsa + @suaveplan/jws) with rotating `kid` for non-disruptive key rotation. Passwords are hashed via Argon2id (@suaveplan/crypto-hash), gated by @suaveplan/password-strength and @suaveplan/hibp at registration. Provider mounts the canonical 9-route PROVIDER_ROUTE_SURFACE under a configurable path prefix. DB access via injected @suaveplan/types `DatabaseLike` — shares the host's main connection per design §2.3. Projects DB rows onto the @suaveplan/api-types `Principal` shape. Contributes cookie + bearer OpenAPI security schemes.
Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues
RS256-signed JWTs with rotating `kid` slots, verifies tokens through the
rotator's active + retired-non-revoked key set, projects DB rows onto
the `@suaveplan/api-types` `Principal` contract, and contributes cookie
+ bearer OpenAPI security schemes.
This milestone ships the **session-resolver half** of the eventual dual
contract (`auth.session`). The matching `AuthProviderAdapter`
(account-lifecycle routes, password gating, MFA, OAuth) is deferred to
a successor change `api-auth-rolled-provider`.
<!-- begin:auto-docs -->
## Documentation tree
- [composition](./src/composition/composition.md) — Top-level factory `createRolledAuthAdapter` that wires the four sub-modules (`token-rotator`, `session`, `principal-pro…
- [composition](./src/composition/composition.md) — Top-level factory `createRolledAuthAdapter` that wires the four sub-modules (`token-rotator`, `session`, `principal-pro…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is namespaced under `AUTH_ROLLED.*` so cross-pa…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is namespaced under `AUTH_ROLLED.*` so cross-pa…
- [options](./src/options/options.md) — Defines the configuration shape accepted by `createRolledAuthAdapter` and the Zod schema that validates it. Operators w…
- [options](./src/options/options.md) — Defines the configuration shape accepted by `createRolledAuthAdapter` and the Zod schema that validates it. Operators w…
- [principal-projection](./src/principal-projection/principal-projection.md) — Projects the package's internal `UserRow` storage shape onto the abstract `Principal` contract published by `@suaveplan…
- [principal-projection](./src/principal-projection/principal-projection.md) — Projects the package's internal `UserRow` storage shape onto the abstract `Principal` contract published by `@suaveplan…
- [security-schemes](./src/security-schemes/security-schemes.md) — Contributes OpenAPI 3.1 `securitySchemes` entries describing both transports the rolled session resolver accepts — `Aut…
- [security-schemes](./src/security-schemes/security-schemes.md) — Contributes OpenAPI 3.1 `securitySchemes` entries describing both transports the rolled session resolver accepts — `Aut…
- [session](./src/session/session.md) — The rolling-JWT session resolver — the keystone of the rolled auth adapter. Builds a `SessionResolver` (per `@suaveplan…
- [session](./src/session/session.md) — The rolling-JWT session resolver — the keystone of the rolled auth adapter. Builds a `SessionResolver` (per `@suaveplan…
- [token-rotator](./src/token-rotator/token-rotator.md) — Owns the rotating-keyset state for the rolled session-token resolver. The active key signs new JWTs; retired-but-non-re…
- [token-rotator](./src/token-rotator/token-rotator.md) — Owns the rotating-keyset state for the rolled session-token resolver. The active key signs new JWTs; retired-but-non-re…
<!-- end:auto-docs -->
## Why
This package gives operators a
session-auth path that owns zero third-party runtime dependencies.
## Install
```bash
bun add @suaveplan/api-auth-rolled
```
## Modules
| Module | What it owns |
|---|---|
| `composition/` | `createRolledAuthAdapter` — the public factory. |
| `error-codes/` | The 4-code `AUTH_ROLLED.*` registry. |
| `options/` | Zod-validated options + named defaults. |
| `principal-projection/` | `UserRow` → `Principal` projection. |
| `security-schemes/` | Cookie + bearer OpenAPI contributions. |
| `session/` | RS256 sign/verify, cookie+bearer extraction, `Set-Cookie`. |
| `token-rotator/` | Rotating-key state machine. |
## Quick start
```ts
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
const adapter = createRolledAuthAdapter({
store: hostStore,
keys: [{ kid: "k1", privateJwk, publicJwk, active: true }],
issuer: "https://api.example.com",
audience: "example-front",
});
host.registerAuthSession(adapter);
```
Rotation cron:
```ts
setInterval(async () => {
if (!adapter.rotator.decide().shouldRotate) return;
adapter.rotator.setKeys(await kms.nextKeyset());
}, 60_000);
```
## License
MIT

View File

@@ -0,0 +1,85 @@
# `composition`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Top-level factory `createRolledAuthAdapter` that wires the four
sub-modules (`token-rotator`, `session`, `principal-projection`,
`security-schemes`) into a single `AuthAdapter` ready for the host to
register. Keeping the composition explicit (rather than letting each
sub-module own its own host wiring) means the dependency graph is one
arrow: options flow in, an adapter object flows out, no hidden state.
The returned value extends the `AuthAdapter` contract (`resolver` +
`securityScheme`) with three additional surfaces the future provider
half (`api-auth-rolled-provider`) will consume: `issueToken`,
`verifyToken`, and the rotator handle so operators can wire
`setKeys(...)` from an out-of-band rotation cron.
## Features
- Single entry point — `createRolledAuthAdapter(rawOptions)`.
- Options validated via the package's Zod schema before any wiring runs.
- Defaults filled at the composition layer (cookie name, rotation
period, session TTL) so individual sub-modules stay focused on their
one concern.
- Returned adapter is structurally a valid `AuthAdapter` so any host
that accepts the contract will accept the rolled adapter.
## Usage: Basic
```ts
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
const adapter = createRolledAuthAdapter({
store,
keys: [
{ kid: "k1", privateJwk, publicJwk, active: true },
],
issuer: "https://api.example.com",
audience: "example-front",
});
host.registerAuthSession(adapter);
```
## Usage: Advanced
```ts
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
const adapter = createRolledAuthAdapter({
store,
keys: initialKeys,
issuer: "https://api.example.com",
audience: "example-front",
cookieName: "acme.session",
sessionTtlSec: 1800,
rotationPeriodMs: 12 * 60 * 60 * 1000,
});
// Operator rotation cron — fetch new key from KMS, swap atomically.
setInterval(async () => {
if (!adapter.rotator.decide().shouldRotate) return;
const next = await kms.nextKeyset();
adapter.rotator.setKeys(next);
}, 60_000);
```
## API Reference
- `createRolledAuthAdapter(rawOptions: unknown): RolledAuthAdapter`
- `RolledAuthAdapter extends AuthAdapter`: adds `id: "rolled"`, `rotator`,
`issueToken(userId)`, `verifyToken(token)`.
## Implementation Notes
The adapter intentionally keeps `id` as the literal `"rolled"` so a host
running both Better-Auth and rolled can disambiguate by adapter id. The
`securityScheme` slot points at the bearer contribution (the canonical
single-scheme shape) but the `.schemes` map on the contribution exposes
both the cookie and bearer entries — emitters aware of dual-transport
adapters can register both names without a separate adapter property.

View File

@@ -0,0 +1,78 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is
namespaced under `AUTH_ROLLED.*` so cross-package consumers (the host's audit
log, the OpenAPI emitter, the conformance harness) can identify the package
that raised the failure without string-matching on free-form messages.
The milestone ships the session-resolver half only; the registry
is correspondingly minimal — four operator-actionable infrastructure codes,
all of which are thrown synchronously from production code paths and
covered by the conformance test. Per-request soft failures (missing token,
bad signature, expired) collapse to `null` per `SessionResolver` chain
semantics and are therefore NOT codes. Account-lifecycle codes
(`USER_NOT_FOUND`, `INVALID_CREDENTIALS`, `PASSWORD_BREACHED`, …) belong to
the deferred provider half and will be added by a successor change.
## Features
- Tagged with `defineCodes("AUTH_ROLLED", [...])` from `@suaveplan/error`,
so all codes share a single namespace prefix.
- `AuthRolledError` extends `SuaveplanError` so `instanceof AuthRolledError`
narrows `.code` to `AuthRolledErrorCode`.
- Four codes: `SIGNING_KEY_NOT_CONFIGURED` (options validation),
`KEY_ROTATION_FAILED` (`TokenRotator.setKeys`),
`DATABASE_REQUIRED` (missing `store`), `PRINCIPAL_PROJECTION_FAILED`
(malformed user row).
## Usage: Basic
```ts
import { AUTH_ROLLED_CODES, AuthRolledError } from "@suaveplan/api-auth-rolled";
throw new AuthRolledError(
AUTH_ROLLED_CODES.DATABASE_REQUIRED,
"host did not inject a store"
);
```
## Usage: Advanced
```ts
import { AUTH_ROLLED_CODES, AuthRolledError } from "@suaveplan/api-auth-rolled";
try {
/* ... */
} catch (err) {
if (err instanceof AuthRolledError) {
// .rolledCode is typed as AuthRolledErrorCode — no narrowing cast.
if (err.rolledCode === AUTH_ROLLED_CODES.KEY_ROTATION_FAILED) {
log.fatal({ err }, "operator key-rotation contract violated");
}
}
throw err;
}
```
## API Reference
- `AUTH_ROLLED_CODES` — frozen `Record<Name, "AUTH_ROLLED.NAME">`.
- `AuthRolledErrorCode` — string-literal union of every code value.
- `AuthRolledError extends SuaveplanError<AuthRolledErrorCode>` — has a
`rolledCode: AuthRolledErrorCode` convenience accessor.
## Implementation Notes
The set of 20 codes is fixed by the OpenSpec spec delta. The conformance
test enumerates `AUTH_ROLLED_CODES` and asserts every code has a named
production trigger; if a code is added without a trigger, the test fails
loudly. Cross-package codes (e.g. `HIBP.*` propagated from
`@suaveplan/hibp` when the breach lookup fails) are NOT re-encoded — they
surface to the caller intact through `SuaveplanError`'s `cause` chain so
operators can attribute failures to the originating layer.

View File

@@ -0,0 +1,90 @@
# `options`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the configuration shape accepted by `createRolledAuthAdapter` and
the Zod schema that validates it. Operators wire this from `host.json` or
build it programmatically; everything the rest of the package needs flows
from a single validated `RolledAuthOptions` value, which keeps invariants
(active-key count, prefix shape, TTL positivity) in one place.
The schema deliberately validates injected functions and the `store`
adapter as `unknown` and then refines them later — Zod cannot verify
function-argument shapes statically, and TypeScript already does that work
at the call boundary.
## Features
- Zod schema `RolledAuthOptionsSchema` covering every operator-facing
knob.
- A `superRefine` cross-field invariant that exactly one key is both
`active: true` and non-revoked — keeps sign-time semantics ambiguity-free.
- Stable named defaults (`DEFAULT_COOKIE_NAME`, `DEFAULT_ROUTE_PREFIX`,
TTLs, rotation period, minimum strength score) so consumers can
reference the defaults without re-reading the source.
- Two distinct error codes for failures: `DATABASE_REQUIRED` when `store`
is missing (operator hooked up nothing), `SIGNING_KEY_NOT_CONFIGURED`
for every other schema failure (Zod issues attached as `context`).
## Usage: Basic
```ts
import { parseRolledAuthOptions } from "@suaveplan/api-auth-rolled";
const opts = parseRolledAuthOptions({
store: myStore,
keys: [{ kid: "k1", privateJwk, publicJwk, active: true }],
issuer: "https://api.example.com",
audience: "example-frontend",
});
```
## Usage: Advanced
```ts
import {
DEFAULT_ROTATION_PERIOD_MS,
parseRolledAuthOptions,
} from "@suaveplan/api-auth-rolled";
const opts = parseRolledAuthOptions({
store,
keys,
issuer: "https://api.example.com",
audience: "example-frontend",
routePrefix: "/api/auth",
cookieName: "acme.session",
cookieSameSite: "strict",
cookieSecure: true,
sessionTtlSec: 1800,
refreshTtlSec: 7 * 24 * 60 * 60,
rotationPeriodMs: DEFAULT_ROTATION_PERIOD_MS / 2,
minPasswordStrength: 4,
strength: { async score(pw) { return 4; } },
breach: { async isBreached() { return false; } },
});
```
## API Reference
- `parseRolledAuthOptions(raw): RolledAuthOptions` — throws
`AuthRolledError` on bad input.
- `RolledAuthOptionsSchema` — the Zod schema, re-exported for advanced
consumers that want to layer their own refinements.
- `DEFAULT_*` constants — every operator-facing default exposed as a
named export.
## Implementation Notes
The schema is `strictObject` so unknown keys produce a Zod error rather
than silently passing through. The `keys[]` `active` invariant is enforced
in `superRefine` rather than as a `transform` so the invariant message
appears in the validation issue list instead of as a thrown exception
mid-parse. Cross-field invariants that cross the option/store boundary
(e.g. "every kid actually resolves to a JWK") are checked later inside the
session/token-rotator modules so the options layer remains synchronous and
pure.

View File

@@ -0,0 +1,73 @@
# `principal-projection`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Projects the package's internal `UserRow` storage shape onto the abstract
`Principal` contract published by `@suaveplan/api-types`. Keeping the
projection in one place ensures that no storage-specific columns leak
into the principal seen by downstream plugins, and that the four
documented principal fields (`id`, `scopes`, `tenantId?`, `claims`) are
the only surface area authors can rely on.
The projection is deliberately strict: structurally-invalid rows raise
`AUTH_ROLLED.PRINCIPAL_PROJECTION_FAILED`, surfacing storage-driver bugs
loudly rather than letting malformed data flow into route handlers.
## Features
- One-shot pure function — no DB calls, no async, no allocations beyond
the new array of branded `Scope`s.
- Validates every column the projection touches (`id` non-empty string,
`scopes` array of non-empty strings, `claims` plain object).
- Omits `tenantId` from the output `Principal` when the storage row's
value is `null`, matching the optional shape in `Principal`.
- Brands strings as `PrincipalId` / `Scope` / `TenantId` at the boundary
so type narrowing carries through to consumers.
## Usage: Basic
```ts
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
const principal = projectUserRowToPrincipal(row);
console.log(principal.id, principal.scopes);
```
## Usage: Advanced
```ts
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
function resolveSession(row: UserRow): Principal {
try {
return projectUserRowToPrincipal(row);
} catch (err) {
// Storage adapter returned a malformed row — log + treat as an
// anonymous request rather than surfacing the bug to the client.
log.error({ err }, "principal projection failed");
throw err;
}
}
```
## API Reference
- `projectUserRowToPrincipal(row: UserRow): Principal` — pure projection.
Throws `AuthRolledError(PRINCIPAL_PROJECTION_FAILED)` on any structural
invariant violation.
## Implementation Notes
The projection allocates a fresh `scopes` array rather than spreading the
row's array — this is intentional so that downstream handlers cannot
mutate the source row through the principal reference. `claims` is a
shallow read-only reference so deep-cloning every claim object on every
request stays optional (consumers wanting deep immutability can wrap with
`Object.freeze`). The `tenantId` branding mirrors the contract published
by `@suaveplan/api-tenant-resolver-*` adapters — keeping the brand at the
projection boundary means downstream code that takes a `TenantId` never
needs an unsafe cast.

View File

@@ -0,0 +1,73 @@
# `security-schemes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Contributes OpenAPI 3.1 `securitySchemes` entries describing both
transports the rolled session resolver accepts — `Authorization: Bearer
<token>` and a `Cookie` carrying the same JWT. Keeping the two entries
co-located means a single source of truth describes what the resolver
accepts on the wire; the `api-openapi` emitter (Stage 8.6) walks the
adapter's `securityScheme` slot AND its `.schemes` map so both are
surfaced in the generated document.
## Features
- Bearer entry: OpenAPI 3.1 `http` / `bearer` form with `bearerFormat:
"JWT"` — the canonical Bearer-JWT shape consumers expect.
- Cookie entry: `apiKey` / `in: cookie` form whose `name` is the
caller-configured cookie name, so the doc matches the adapter's
runtime behaviour exactly.
- `createRolledSecuritySchemes({ cookieName })` returns a single value
that satisfies the `SecuritySchemeContribution` contract (its `name`
and `scheme` point at the bearer entry) AND exposes both contributions
on `.schemes` — the emitter can pull either or both.
## Usage: Basic
```ts
import { createRolledSecuritySchemes } from "@suaveplan/api-auth-rolled";
const contribution = createRolledSecuritySchemes({
cookieName: "suaveplan.session",
});
```
## Usage: Advanced
```ts
import {
createRolledBearerSecurityScheme,
createRolledCookieSecurityScheme,
} from "@suaveplan/api-auth-rolled";
// Multi-resolver host — name the contributions to disambiguate.
const userBearer = createRolledBearerSecurityScheme("userBearer");
const serviceBearer = createRolledBearerSecurityScheme("serviceBearer");
const cookie = createRolledCookieSecurityScheme({
name: "userCookie",
cookieName: "acme.session",
});
```
## API Reference
- `createRolledBearerSecurityScheme(name?: string): SecuritySchemeContribution`
- `createRolledCookieSecurityScheme({ name?, cookieName }): SecuritySchemeContribution`
- `createRolledSecuritySchemes({ cookieName }): RolledSecurityContribution`
— convenience bundle.
## Implementation Notes
The combined `RolledSecurityContribution` is structurally a
`SecuritySchemeContribution` (so old emitters that only read `name` +
`scheme` keep working) but adds a `.schemes` map for emitters aware of
the dual-transport shape. This keeps the contribution drop-in for both
single-transport and dual-transport emitters without forcing every
caller to choose. The cookie scheme's `apiKey/in:cookie` representation
matches OpenAPI 3.1 §4.8.27.4 — there is no first-class cookie-JWT
shape in OAS 3.1 so `apiKey` with the configured name is the canonical
form.

View File

@@ -0,0 +1,100 @@
# `session`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The rolling-JWT session resolver — the keystone of the rolled auth
adapter. Builds a `SessionResolver` (per `@suaveplan/api-auth`) that
parses-and-verifies a JWT extracted from either the `Authorization:
Bearer` header or a configurable cookie, runs the JWT through the
`TokenRotator` to find the verifying JWK by `kid`, projects the matching
user row onto a `Principal`, and returns `null` on every soft failure so
the host's resolver chain can move on to the next adapter.
The same module exposes `issueToken(userId)` so the future
account-lifecycle provider (deferred to `api-auth-rolled-provider`) can
share a single signing path with the resolver. Keeping issue and verify
co-located means the JWT shape and the rotator wiring stay in lock-step.
## Features
- RS256 signing via `@suaveplan/jws` `compactSign` with the rotator's
active JWK; verification via `compactVerify` against the rotator's
retired-or-active JWK matched by `kid`.
- Bearer + cookie extraction in one pass — bearer wins when both are
present so service clients can override a stale cookie.
- Strict JWT-claim validation: `iss`/`aud` exact match, `exp` future,
`iat` not before `users.createdAt`.
- Soft failures (missing token, bad signature, expired, unknown kid)
collapse to `null`; hard failures (storage adapter throws) propagate
as `AuthRolledError`.
- `buildSetCookie` returns a deterministic `Set-Cookie` value with
`HttpOnly`, `Path=/`, `Max-Age`, `SameSite`, `Secure` — used by the
future provider half but published here so the cookie shape lives next
to the cookie parser.
## Usage: Basic
```ts
import { createRolledSessionResolver, TokenRotator } from "@suaveplan/api-auth-rolled";
const rotator = new TokenRotator({ keys, rotationPeriodMs, clock });
const resolver = createRolledSessionResolver({
options,
rotator,
clock,
cookieName: "suaveplan.session",
sessionTtlSec: 3600,
});
const principal = await resolver.resolve(req);
```
## Usage: Advanced
```ts
import { createRolledSessionResolver } from "@suaveplan/api-auth-rolled";
const resolver = createRolledSessionResolver({ /* ... */ });
// Provider half (future) signs new tokens with the same key set as the
// resolver — sharing one path means the rotator's view of the active
// kid is the only source of truth.
const issued = await resolver.issueToken("u_1");
const setCookie = buildSetCookie({
name: "suaveplan.session",
value: issued.token,
maxAgeSec: issued.expiresAt - issued.issuedAt,
sameSite: "lax",
secure: true,
});
```
## API Reference
- `createRolledSessionResolver({ options, rotator, clock, cookieName, sessionTtlSec }) → SessionResolver & { issueToken, verifyToken }`
- `buildSetCookie({ name, value, maxAgeSec, sameSite, secure }) → string`
- `tokenSizeOf(token: string): number` — byte length, used by the
conformance harness to assert tokens stay under the 8 KiB cookie limit.
## Implementation Notes
The resolver intentionally returns `null` on every parse/verify failure
rather than throwing — `SessionResolver` chain semantics treat thrown
errors as operator-actionable infrastructure failures, so soft failures
must collapse to `null` to let the chain advance to the next adapter
(typically `api-auth-anonymous`). The two cases where the resolver does
throw are `AuthRolledError(PRINCIPAL_PROJECTION_FAILED)` raised by the
projection step (storage-driver bug) and whatever error the operator's
`store.findUserById` raises (DB connection lost). The session-resolver
chain handles both by surfacing 500 to the client and routing to the
host's audit log.
The header parse is done manually rather than via `compactDecode` so a
malformed header (e.g. missing `kid`) bypasses the rotator lookup
entirely. The rotator can verify with retired-but-non-revoked keys, so
rotating the active key never invalidates a session mid-lifetime —
operators can rotate without coordinating with active users.

View File

@@ -0,0 +1,87 @@
# `token-rotator`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the rotating-keyset state for the rolled session-token resolver. The
active key signs new JWTs; retired-but-non-revoked keys still verify
in-flight tokens so a rotation event never invalidates a session
mid-lifetime. The rotator does NOT generate new key material — that is an
operator-controlled action (HSM, KMS, mounted file) that submits the
replacement set through `setKeys`. This split is deliberate: keeping key
generation out of the adapter means the adapter never holds long-lived
private key material outside of the operator's chosen storage layer.
## Features
- Snapshots the keyset atomically (`snapshot()`) — the session module
reads the snapshot, signs with it, and never observes a half-rotated
state.
- Decides when to rotate (`decide()`) based on the active key's age vs
the configured `rotationPeriodMs`. Decision is advisory — the host
schedules the actual rotation cron.
- `setKeys(...)` validates the replacement keyset (exactly one active +
non-revoked) before swapping; throws `KEY_ROTATION_FAILED` if the new
set would leave signing ambiguous.
- `publicJwkFor(kid)` resolves the verifying JWK for any non-revoked kid
— including retired keys — so verification of in-flight tokens works
through the rotation window.
## Usage: Basic
```ts
import { TokenRotator } from "@suaveplan/api-auth-rolled";
const rotator = new TokenRotator({
keys,
rotationPeriodMs: 24 * 60 * 60 * 1000,
clock: () => Date.now(),
});
const snap = rotator.snapshot();
const activeJwk = snap.active.privateJwk;
```
## Usage: Advanced
```ts
import { TokenRotator } from "@suaveplan/api-auth-rolled";
const rotator = new TokenRotator({
keys: initialKeys,
rotationPeriodMs: 24 * 60 * 60 * 1000,
clock,
});
// Periodic rotator cron:
setInterval(async () => {
const decision = rotator.decide();
if (!decision.shouldRotate) return;
const next = await fetchKeysFromKms(); // operator-specific
rotator.setKeys(next);
}, 60 * 1000);
```
## API Reference
- `new TokenRotator({ keys, rotationPeriodMs, clock })` — construct.
- `snapshot(): KeySetSnapshot` — atomic read.
- `decide(): RotationDecision` — should we rotate now?
- `setKeys(next: readonly SigningKeyEntry[]): void` — swap keys.
- `publicJwkFor(kid): Record<string, unknown> | undefined` — verify-side
lookup.
## Implementation Notes
The rotator keeps its `activeSince` timestamp in instance state rather
than reading `keys[i].activatedAt`, so the in-memory rotation timer is
independent of any timestamps the operator stamps on the JWK records. This
means a fresh keyset injected via `setKeys` starts a brand-new rotation
window regardless of when the underlying key material was generated — the
operator's intent ("treat this as the new active key as of now") is the
authoritative signal. The exactly-one-active invariant is enforced both
in `parseRolledAuthOptions` and in `setKeys` so there is no path through
which the rotator can observe an invalid keyset.

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.

View File

@@ -0,0 +1,109 @@
# @suaveplan/api-auth - Abstract session-resolver contract for the Suaveplan API plugin system. Defines the SessionResolver interface (Request → Principal | null), SessionResolverChain (ordered, first-match-wins, fail-closed-on-throw), SecuritySchemeContribution (OpenAPI emission metadata), CsrfPolicy default rules, the composite AuthAdapter, the AUTH_CODES registry, and a runAuthSessionConformance harness reused by every concrete resolver in Stages 4/6/8.
**$2** for the Suaveplan API plugin system. Defines
the abstract session-resolver shape (`Request → Principal | null`) used by the
host on every request, plus the first-match-wins runner, the OpenAPI
security-scheme contribution, the default CSRF policy, the composite
`AuthAdapter`, and the conformance harness every concrete resolver
implementation passes.
This package is **session resolution only** — account lifecycle (login,
register, oauth callback, password reset) lives in the separate
`@suaveplan/api-auth-provider` package per design doc §2.3.
<!-- begin:auto-docs -->
## Documentation tree
- [capability-id](./src/capability-id/capability-id.md) — Pins this package's `CapabilityId` contribution as a stand-alone literal type plus runtime constant. The `CapabilityId`…
- [capability-id](./src/capability-id/capability-id.md) — Pins this package's `CapabilityId` contribution as a stand-alone literal type plus runtime constant. The `CapabilityId`…
- [chain](./src/chain/chain.md) — Ordered, first-match-wins runtime over an array of `SessionResolver`s. The chain is the integration point at design-doc…
- [chain](./src/chain/chain.md) — Ordered, first-match-wins runtime over an array of `SessionResolver`s. The chain is the integration point at design-doc…
- [csrf-policy](./src/csrf-policy/csrf-policy.md) — Exposes a single pure predicate, `CsrfPolicy.requiresCsrf(scheme)`, that defaults CSRF protection ON for any cookie-bas…
- [csrf-policy](./src/csrf-policy/csrf-policy.md) — Exposes a single pure predicate, `CsrfPolicy.requiresCsrf(scheme)`, that defaults CSRF protection ON for any cookie-bas…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, namespaced error-code registry for the `@suaveplan/api-auth` package and the concrete `AuthError` c…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, namespaced error-code registry for the `@suaveplan/api-auth` package and the concrete `AuthError` c…
- [resolver](./src/resolver/resolver.md) — `SessionResolver` is the abstract per-request session-resolver shape that every concrete adapter in Stages 4/6/8 (`api-…
- [resolver](./src/resolver/resolver.md) — `SessionResolver` is the abstract per-request session-resolver shape that every concrete adapter in Stages 4/6/8 (`api-…
- [security-scheme](./src/security-scheme/security-scheme.md) — Defines the shape every concrete `SessionResolver` MUST contribute so the `api-openapi` package can emit the right `com…
- [security-scheme](./src/security-scheme/security-scheme.md) — Defines the shape every concrete `SessionResolver` MUST contribute so the `api-openapi` package can emit the right `com…
- [session-adapter](./src/session-adapter/session-adapter.md) — `AuthAdapter` is the composite shape every concrete session-auth adapter exports — pairing the per-request `SessionReso…
- [session-adapter](./src/session-adapter/session-adapter.md) — `AuthAdapter` is the composite shape every concrete session-auth adapter exports — pairing the per-request `SessionReso…
- [testing](./src/testing/conformance.md) — A reusable Vitest `describe(...)` block every concrete `SessionResolver` must pass before publishing. The harness cover…
- [conformance](./src/testing/conformance.md) — A reusable Vitest `describe(...)` block every concrete `SessionResolver` must pass before publishing. The harness cover…
<!-- end:auto-docs -->
## What's inside
| Module | Subpath | Purpose |
|-------------------------------|----------------------------------------|---------------------------------------------------------------|
| `SessionResolver` interface | `@suaveplan/api-auth/resolver` | Abstract `resolve(req) → Principal \| null` + `dispose`. |
| `SessionResolverChain` runner | `@suaveplan/api-auth/chain` | Ordered first-match-wins; fail-closed-on-throw by default. |
| Security-scheme contract | `@suaveplan/api-auth/security-scheme` | Zod-validated OpenAPI security-scheme metadata. |
| CSRF policy default | `@suaveplan/api-auth/csrf-policy` | Cookie schemes → ON; bearer / oauth2 / OIDC → OFF. |
| Composite `AuthAdapter` | `@suaveplan/api-auth/session-adapter` | `{ resolver, securityScheme, csrf? }` registration shape. |
| `auth.session` capability id | `@suaveplan/api-auth/capability-id` | Re-export of `"auth.session"` with type-level assertion. |
| `AUTH_CODES` registry | `@suaveplan/api-auth/error-codes` | Frozen `AUTH.*` error-code registry + `AuthError` class. |
| Conformance harness | `@suaveplan/api-auth/testing` | `runAuthSessionConformance(spec)` for concrete resolvers. |
## Co-located documentation
Each module ships a sibling `.md`:
- [`src/resolver/resolver.md`](src/resolver/resolver.md)
- [`src/chain/chain.md`](src/chain/chain.md)
- [`src/security-scheme/security-scheme.md`](src/security-scheme/security-scheme.md)
- [`src/csrf-policy/csrf-policy.md`](src/csrf-policy/csrf-policy.md)
- [`src/session-adapter/session-adapter.md`](src/session-adapter/session-adapter.md)
- [`src/capability-id/capability-id.md`](src/capability-id/capability-id.md)
- [`src/error-codes/error-codes.md`](src/error-codes/error-codes.md)
- [`src/testing/conformance.md`](src/testing/conformance.md)
## Quick example
```ts
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
import { validateAuthAdapter } from "@suaveplan/api-auth/session-adapter";
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
declare const jwt: SessionResolver;
declare const staticBearer: SessionResolver;
const jwtAdapter = validateAuthAdapter({
resolver: jwt,
securityScheme: {
name: "bearerAuth",
scheme: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
},
});
const bearerAdapter = validateAuthAdapter({
resolver: staticBearer,
securityScheme: {
name: "staticBearer",
scheme: { type: "http", scheme: "bearer" },
},
});
const chain = new SessionResolverChain([
jwtAdapter.resolver,
bearerAdapter.resolver,
]);
const principal = await chain.resolve(new Request("https://example.test"));
```
## Versioning
1.0`. Graduates to `1.0.0` when the Stage 1
contract bundle (`api-types`, `api-auth`, `api-tenant-resolver`,
`api-secrets`, `api-db-adapter`) all reach 1.0.
## Dependencies
| Dep | Why |
|---------------------------|------------------------------------------------------|
| `@suaveplan/api-types` | `Principal`, `CapabilityId`. |
| `@suaveplan/error` | `SuaveplanError`, `defineCodes`. |
| `@suaveplan/types` | Cross-package contracts (logger, clock, etc.). |
| `tslib` | TypeScript runtime helpers (catalog). |
| `zod` | Boundary validation for security-scheme contributions. |

View File

@@ -0,0 +1,85 @@
# `capability-id` — `"auth.session"` contribution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pins this package's `CapabilityId` contribution as a stand-alone literal type
plus runtime constant. The `CapabilityId` union in
`@suaveplan/api-types/capabilities` already enumerates `"auth.session"`
directly; this module ties the api-auth package to that identifier so any
future rename surfaces here first as a build break.
Exporting the runtime literal as `AUTH_SESSION_CAPABILITY_ID` lets plugin
manifests declare `requires: [AUTH_SESSION_CAPABILITY_ID] as const` without
hard-coding the string at the manifest site — refactor-safe and centrally
documented.
## Features
- Compile-time `const _assignable: CapabilityId = AUTH_SESSION_CAPABILITY_ID`
asserts membership at build time. If `api-types` ever drops the token, this
package fails to type-check rather than silently regressing.
- Type and value are exported together so the consumer can pick the form that
fits the call site without re-stringifying.
## Usage: Basic
```ts
import { AUTH_SESSION_CAPABILITY_ID } from "@suaveplan/api-auth/capability-id";
const requires = [AUTH_SESSION_CAPABILITY_ID] as const;
```
## Usage: Advanced
Use the literal type to constrain a manifest builder helper:
```ts
import {
AUTH_SESSION_CAPABILITY_ID,
type AuthSessionCapabilityId,
} from "@suaveplan/api-auth/capability-id";
import type { CapabilityId } from "@suaveplan/api-types/capabilities";
function manifestRequires<const T extends readonly CapabilityId[]>(req: T): T {
return req;
}
const requires = manifestRequires([
AUTH_SESSION_CAPABILITY_ID,
"logger",
] satisfies readonly (AuthSessionCapabilityId | "logger")[]);
```
## API Reference
| Symbol | Kind | Description |
|---------------------------------|------|----------------------------------------------|
| `AuthSessionCapabilityId` | type | The literal `"auth.session"`. |
| `AUTH_SESSION_CAPABILITY_ID` | const| `"auth.session"` runtime value. |
## Implementation Notes
- The `void _assignable` keeps the assignment from being tree-shaken; without
it Rollup MAY drop the line and the build-time assertion silently regresses
to no-op.
- This module is import-time-only — no behaviour at runtime beyond the
string literal binding.
- The literal is exported as both a `type` and a runtime `const` deliberately:
consumers that build manifests imperatively want the runtime value; consumers
that constrain compile-time-only structures (a `definePlugin` builder, for
example) want the literal type. Re-stringifying `"auth.session"` at the
consumer side would defeat the purpose of having the package central.
- The pairing with `@suaveplan/api-types/capabilities` is deliberately
one-directional. `api-types` is the source of truth for the closed union;
this package only re-asserts membership and binds the runtime value. If a
future change wanted to flip the relationship — have api-auth contribute via
declaration merging instead — that would require relaxing the `CapabilityId`
union in api-types to be `open` (i.e. `string & { brand }`), which is a
larger contract change documented as out-of-scope here.
- The runtime constant uses a type annotation rather than `as const`: the
annotation makes the assignable check above explicit, and `as const` would
widen any future enum-style rewrite.

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.

View File

@@ -0,0 +1,90 @@
# `csrf-policy` — `CsrfPolicy.requiresCsrf`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exposes a single pure predicate, `CsrfPolicy.requiresCsrf(scheme)`, that
defaults CSRF protection ON for any cookie-based scheme and OFF for any scheme
where the caller must actively attach the credential. This default codifies the
design-doc §6 rule "Cookie-based auth adapters trigger CSRF verification" so
every concrete adapter inherits the right behaviour without restating it.
The package exposes only the default; the host (`api-host`) applies per-route
`csrf: false` overrides on top. Keeping the default and the override separate
means a route-level opt-out is always auditable as an explicit decision in the
manifest, never a side-effect of which adapter happened to be wired.
## Features
- Pure function — no side effects, no module-load cost.
- Frozen container object so test code can `expect(CsrfPolicy)` reliably and
consumers cannot monkey-patch the predicate.
- Total over the `OpenApiSecurityScheme` discriminated union — no `default`
branch means an added scheme type at the schema layer will surface a
type-level reminder to revisit the policy.
## Usage: Basic
```ts
import { CsrfPolicy } from "@suaveplan/api-auth/csrf-policy";
const needsCsrf = CsrfPolicy.requiresCsrf({
type: "apiKey",
in: "cookie",
name: "sp_session",
}); // → true
```
## Usage: Advanced
Drive a host-side CSRF middleware off the policy, with a per-route override
applied at the call site:
```ts
import { CsrfPolicy } from "@suaveplan/api-auth/csrf-policy";
import type { OpenApiSecurityScheme } from "@suaveplan/api-auth/security-scheme";
interface RouteCsrfDecision {
readonly required: boolean;
readonly reason: "scheme-default" | "route-override";
}
function decideCsrf(
scheme: OpenApiSecurityScheme,
routeOverride: boolean | undefined,
): RouteCsrfDecision {
if (routeOverride !== undefined) {
return { required: routeOverride, reason: "route-override" };
}
return {
required: CsrfPolicy.requiresCsrf(scheme),
reason: "scheme-default",
};
}
const decision = decideCsrf(
{ type: "http", scheme: "bearer" },
undefined,
);
// → { required: false, reason: "scheme-default" }
```
## API Reference
| Symbol | Kind | Description |
|----------------------------|-------------|----------------------------------------------------------|
| `CsrfPolicy.requiresCsrf` | function | `(scheme) => boolean`. Cookie schemes → `true`. |
## Implementation Notes
- The predicate inspects `type === "apiKey" && in === "cookie"` — apiKey-in-
header is treated identically to `Authorization: Bearer …` for CSRF purposes
because the caller must attach it explicitly.
- `oauth2` and `openIdConnect` default OFF — both are bearer-token-derived in
practice; if a deployment exposes an oauth2 access token via cookie, that's
a custom apiKey-in-cookie scheme, not the `oauth2` scheme.
- `Object.freeze` guards the predicate slot; the predicate itself is a regular
method and TypeScript prevents reassignment via the `Readonly` inference.

View File

@@ -0,0 +1,84 @@
# `error-codes` — `AUTH_CODES` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable, namespaced error-code registry for the `@suaveplan/api-auth`
package and the concrete `AuthError` class. Every code is reachable from a
production throw site somewhere in the package (chain construction, chain
execution, adapter registration, or principal-shape validation). The registry
is frozen via `defineCodes("AUTH", […])` so accidental drift between thrown
codes and the documented set is impossible.
Consumers — including downstream concrete resolvers in Stages 4/6/8 such as
`api-auth-jwt`, `api-auth-anonymous`, and `api-auth-bearer-static` — branch on
the string literal (`"AUTH.RESOLVER_THREW"` etc.) or compare to the constant
(`AUTH_CODES.RESOLVER_THREW`). The literal form is preferred at the API
boundary; the constant is preferred inside the package itself so the linter
flags typos as type errors.
## Features
- Frozen string-literal union of all four codes.
- `AuthError` extends `SuaveplanError<AuthErrorCode>` so `instanceof AuthError`
narrows `code` correctly.
- Cause-chain propagation: the optional `options.cause` field is forwarded to
the base class.
- Public constructor (the base class is `protected`).
## Usage: Basic
```ts
import { AUTH_CODES, AuthError } from "@suaveplan/api-auth/error-codes";
if (chain.length === 0) {
throw new AuthError(AUTH_CODES.CHAIN_EMPTY, "at least one resolver required");
}
```
## Usage: Advanced
Re-throw a resolver error with a cause chain and inspect it downstream:
```ts
import { AUTH_CODES, AuthError } from "@suaveplan/api-auth/error-codes";
try {
await resolver.resolve(req);
} catch (cause) {
throw new AuthError(AUTH_CODES.RESOLVER_THREW, "resolver A threw", {
cause,
context: { resolverIndex: 0 },
});
}
```
```ts
import { AuthError } from "@suaveplan/api-auth/error-codes";
if (err instanceof AuthError && err.code === "AUTH.RESOLVER_THREW") {
logger.warn("upstream session resolver failed", { cause: err.cause });
}
```
## API Reference
| Symbol | Kind | Description |
|----------------|--------|-----------------------------------------------------------------|
| `AUTH_CODES` | const | Frozen lookup `{ RESOLVER_THREW, CHAIN_EMPTY, … }`. |
| `AuthErrorCode`| type | Literal union of the four code strings. |
| `AuthError` | class | Concrete `SuaveplanError` subclass narrowed to `AuthErrorCode`. |
## Implementation Notes
- `defineCodes("AUTH", [...] as const)` produces an object literal with
string-literal value types, which is what gives `AuthErrorCode` its narrow
union. Removing `as const` would widen the type to `string`.
- The class field `authCode` mirrors `code` for ergonomic narrowing on
pre-instantiated error objects (some test runners obscure the inherited
`code` field via Proxy wrappers).
- Code values follow the `AUTH.<UPPER_SNAKE>` template enforced by
`@suaveplan/api-types`' `ErrorCode` template literal type.

View File

@@ -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.

Some files were not shown because too many files have changed in this diff Show More