updated skills

This commit is contained in:
2026-06-09 21:06:27 +01:00
parent db1ce43ee1
commit 427a5bd919
1283 changed files with 24684 additions and 2313 deletions

View File

@@ -15,11 +15,13 @@ description: Python-tier audio fingerprinting service — librosa chromagram + s
**File Structure:** `packages/media/audio/audio-fingerprint/src/{api,client,types}/` (+ Python backend)
**Sibling packages:**
- `@suaveplan/react-audio-fingerprint` — React UI surface (browser tier)
**Patterns:**
*Quick Start (TypeScript client):*
```ts
import { PythonAudioService } from "@suaveplan/audio-fingerprint/client";
const service = new PythonAudioService({ baseUrl: "http://localhost:8000" });
@@ -28,17 +30,20 @@ const match = await service.match(fp);
```
*Quick Start (Python backend):*
```bash
micromamba run -n suaveplan-audio-fingerprint uvicorn src.api.app:app --host 0.0.0.0 --port 8000
```
**Constraints:**
- Dual-runtime: Python FastAPI backend + TypeScript HTTP client
- Python env managed via micromamba (`environment.yml` + `pyproject.toml`)
- Subpath exports: `/client`, `/client/python-audio-service`, `/types`
- High-accuracy backend behind `@suaveplan/react-audio-fingerprint`
**Checklist:**
- [ ] `PythonAudioService.fingerprint()` sends audio and returns a fingerprint
- [ ] `PythonAudioService.match()` returns Jaccard similarity scores
- [ ] FastAPI health endpoint responds at `/health`

View File

@@ -17,6 +17,7 @@ description: Interactive scaffold for new Suaveplan API plugins — generates pl
**File Structure:** `packages/api-stack/api/create-suaveplan-plugin/src/{bin,generator,prompt,templates}/`
**Sibling packages:**
- `@suaveplan/wasm-compiler-javy` — WASM compiler for compiled plugins
- `@suaveplan/testing-integration-postgres` — Postgres fixture for integration tests
- `@suaveplan/testing-integration-surrealdb` — SurrealDB fixture for integration tests
@@ -24,12 +25,14 @@ description: Interactive scaffold for new Suaveplan API plugins — generates pl
**Patterns:**
*Quick Start (CLI):*
```bash
npx create-suaveplan-plugin
# interactive prompts → generates complete plugin skeleton
```
*Programmatic generation:*
```ts
import { generate, validateAnswers } from "create-suaveplan-plugin/generator";
const answers = validateAnswers({ isolationTier: "sandboxed", capabilities: ["rest"], databaseEngine: "sqlite" });
@@ -37,6 +40,7 @@ const files = generate(answers);
```
**Constraints:**
- CLI entrypoint: `create-suaveplan-plugin` bin
- Subpath exports: `/error-codes`, `/prompt`, `/generator`
- Generates `plugin.json`, `definePlugin` body, host test, integration test, vitest configs, migrations, README
@@ -44,6 +48,7 @@ const files = generate(answers);
- No `@suaveplan/*` runtime dependencies other than `@suaveplan/error`
**Checklist:**
- [ ] `scaffold` generates a testable plugin skeleton from interactive prompts
- [ ] `generate` returns in-memory `GeneratedFile` records without writing to disk
- [ ] `validateAnswers` rejects invalid isolation tiers or capability combinations

View File

@@ -0,0 +1,171 @@
---
name: suaveplan-library-navigator
description: Root navigator for all SuavePlan libraries. Pick a category to find the right package.
---
# SuavePlan Library Navigator
This skill helps you navigate the SuavePlan monorepo's 600+ libraries. Start by picking a category below, then drill down through subcategories to find the specific package you need.
## Categories
### api-stack
Full-stack API framework, plugins, middleware, gateway, GraphQL, OpenAPI, sandboxing, and real-time transports.
- **api-adapters** — 8 packages
- **api-audit** — 2 packages
- **api-auth** — 7 packages
- **api-capabilities** — 11 packages
- **api-core** — 4 packages
- **api-db** — 4 packages
- **api-gateway** — 6 packages
- **api-graphql** — 6 packages
- **api-jobs** — 3 packages
- **api-middleware** — 7 packages
- **api-migrations** — 1 package
- **api-openapi** — 5 packages
- **api-plugins** — 35 packages
- **api-realtime** — 5 packages
- **api-sandbox** — 6 packages
- **api-secrets** — 3 packages
- **api-tenant** — 6 packages
- **api-transactions** — 1 package
### commerce
Booking engines, contact management, and audience segmentation for e-commerce.
- **booking** — 1 package
- **contacts** — 5 packages
### communication
Email processing, messaging adapters (Discord, Slack, Telegram, SMS), and webhook clients.
- **emails** — 8 packages
- **messaging** — 7 packages
- **telephone** — 1 package
- **webhook-client** — 1 package
### devtools
Docker tooling, documentation generators, CLI utilities, installers, and MCP servers.
- **docker** — 1 package
- **docs** — 9 packages
- **tooling** — 16 packages
### foundation
Core primitives — binary packing, data structures, serialization, timing, CRDTs, queues, and feature flags.
- **barcode** — 2 packages
- **bitpacker** — 10 packages
- **core** — 8 packages
- **data** — 17 packages
- **etl** — 1 package
- **feature-flags** — 1 package
- **flow** — 18 packages
- **random** — 3 packages
- **timing** — 3 packages
### gaming
Game loops, ECS, physics integration, building systems, and multiplayer server infrastructure.
- **game-engine** — 9 packages
- **gaming** — 35 packages
### geospatial
Geo-planetary math, logistics, tides, solar system simulation, and universe generation.
- **geo** — 7 packages
- **logistics** — 1 package
### identity
Authentication, authorization, OAuth, WebAuthn, permissions, and tenant-aware identity.
- **auth** — 7 packages
### localization
i18n backends, formatters, ICU parsers, locale data, and React integration.
- **i18n** — 19 packages
### media
Audio engines, FX plugins, IR reverbs, image processing, video, fonts, and colour science.
- **audio** — 24 packages
- **audio-fx** — 26 packages
- **audio-ir** — 4 packages
- **fonts** — 4 packages
- **image** — 16 packages
- **video** — 2 packages
### money
Currency conversion, cryptocurrency rates, payment providers, and financial primitives.
- **finance** — 13 packages
- **payment** — 7 packages
### networking
Peer-to-peer networking, drive sync, WebRTC, events, transports, and service discovery.
- **drive** — 14 packages
- **events** — 8 packages
- **network** — 44 packages
### observability
Logging, health checks, OpenTelemetry, audit trails, and browser analytics.
- **monitor** — 5 packages
### persistence
Databases, caching, search engines, and multi-backend storage abstractions.
- **cache** — 5 packages
- **database** — 5 packages
- **search** — 1 package
- **storage** — 14 packages
### security-stack
Cryptography, vaults, SRP, X.509, SLSA, TPM structures, and password management.
- **crypto** — 13 packages
- **security** — 17 packages
### testkit
Testing utilities, DOM helpers, E2E tooling, and integration test containers.
- **testing** — 14 packages
### threejs
React Three Fiber (R3F) extensions — shaders, physics, particles, terrain, and UI.
- **r3f** — 17 packages
### ui
React components, form builders, audio players, auth UIs, and control surfaces.
- **forms** — 5 packages
- **react** — 37 packages
## How to use this navigator
1. Identify the domain of your task (e.g., 'audio processing' → `media`).
2. Open the category `SKILL.md` to see subcategories.
3. Open the subcategory `SKILL.md` to see individual packages and pick the right one.
4. Open the package `SKILL.md` (e.g., `sp-audio-core/SKILL.md`) for deep API guidance.

View File

@@ -0,0 +1,187 @@
---
name: api-stack-navigator
description: Navigator for the api-stack category. Pick a subcategory to find the right library.
---
# Api stack Category Navigator
Full-stack API framework, plugins, middleware, gateway, GraphQL, OpenAPI, sandboxing, and real-time transports.
## Subcategories
### api-adapters
8 packages. See `api-adapters/SKILL.md` for the full package list.
- `@suaveplan/api-adapter-express` — Express adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside an Express host. mountE...
- `@suaveplan/api-adapter-fastify` — Fastify adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside a Fastify host. registe...
- `@suaveplan/api-adapter-hono` — Hono adapter for the Suaveplan Fetch-native ApiRuntime — proves frameworks are guests, not hosts. mountHono(app, runtime...
- `@suaveplan/api-adapter-kit` — Shared parity contract for the @suaveplan/api-adapter-* family — the byte-parity foundation every framework adapter is v...
- `@suaveplan/api-adapter-koa` — Koa adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside a Koa host. mountKoa(runtim...
- ... and 3 more
### api-audit
2 packages. See `api-audit/SKILL.md` for the full package list.
- `@suaveplan/api-audit-file` — File-rotating, HMAC-SHA-256-signed AuditSink for the Fetch-native @suaveplan/api platform. Implements the @suaveplan/api...
- `@suaveplan/api-audit-stdout` — Stdout `AuditSink` for the Fetch-native @suaveplan/api platform. Implements the @suaveplan/api-types `AuditSink` contrac...
### api-auth
7 packages. See `api-auth/SKILL.md` for the full package list.
- `@suaveplan/api-auth` — Authentication-strategy meta-package for the Suaveplan API platform. Each strategy is a factory returning a frozen Sessi...
- `@suaveplan/api-auth-anonymous` — Anonymous (no-authentication) session-resolver strategy for the @suaveplan API auth seam. createAnonymousResolver(option...
- `@suaveplan/api-auth-bearer` — Bearer-token session-resolver strategy for the @suaveplan API auth seam. createBearerResolver(options) returns a frozen ...
- `@suaveplan/api-auth-better-auth` — Better-Auth-backed SessionResolver for the Suaveplan API platform. The heavy authentication integration (kept separate f...
- `@suaveplan/api-auth-codes` — Shared stable error-code registry for the @suaveplan API auth seam. Exports AUTH_CODES (defineCodes("AUTH", ["OPTIONS_IN...
- ... and 2 more
### api-capabilities
11 packages. See `api-capabilities/SKILL.md` for the full package list.
- `@suaveplan/api-capability-database-memory` — Faithful in-memory AdminDatabaseLike reference implementation (MemoryDatabase) over a real minimal SQL engine (SqlStore:...
- `@suaveplan/api-capability-events-bus-memory` — Rich in-memory ExtendedEventBusLike reference implementation (createExtendedEventsBus) for the events.extended capabilit...
- `@suaveplan/api-capability-events-kit` — Tier-2 glue for the events.extended capability of the Suaveplan API capability system: the stable EVENTS_CAP.* error-cod...
- `@suaveplan/api-capability-events-memory` — Faithful in-memory CapabilityEventBusLike reference implementation (MemoryEvents) for the Suaveplan API capability syste...
- `@suaveplan/api-capability-jobs-memory` — Faithful in-memory JobRegistrationHandle reference implementation (MemoryJobs) plus a trigger-on-demand runner for the S...
- ... and 6 more
### api-core
4 packages. See `api-core/SKILL.md` for the full package list.
- `@suaveplan/api` — Framework-free Fetch-native plugin runtime for the Suaveplan API platform. Registers plugins, orders them by dependsOn (...
- `@suaveplan/api-core` — Framework-free Fetch-native HTTP engine for the Suaveplan API platform. Owns the radix route matcher, onion middleware c...
- `@suaveplan/api-schemas` — Shared Zod schemas describing Suaveplan API request/response shapes — single source of truth for server plugins, React d...
- `@suaveplan/api-types` — Types-only contract package for the Suaveplan API plugin foundation. Defines Principal, TenantId, PluginContext, Request...
### api-db
4 packages. See `api-db/SKILL.md` for the full package list.
- `@suaveplan/api-db-kit` — Shared adapter-family kit for the Suaveplan API database capability — the DbError typed error + DB_CODES (12) error voca...
- `@suaveplan/api-db-postgres` — PostgreSQL Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike over the porsager `postgres`...
- `@suaveplan/api-db-sqlite-memory` — SQLite Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike + TenantScopedDatabaseLike over ...
- `@suaveplan/api-db-surreal` — SurrealDB Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike over @suaveplan/surrealdb tha...
### api-gateway
6 packages. See `api-gateway/SKILL.md` for the full package list.
- `@suaveplan/api-gateway` — Transport-agnostic, Fetch-native edge gateway core for composing multiple backend targets behind one entry point. Ships ...
- `@suaveplan/api-gateway-balancer-consistent-hash` — Consistent-hash load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: select(ta...
- `@suaveplan/api-gateway-balancer-random` — Uniform-random load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each selec...
- `@suaveplan/api-gateway-balancer-round-robin` — Round-robin load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each select()...
- `@suaveplan/api-gateway-balancer-weighted` — Weight-proportional load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each ...
- ... and 1 more
### api-graphql
6 packages. See `api-graphql/SKILL.md` for the full package list.
- `@suaveplan/api-graphql` — GraphQL-over-HTTP meta-package for the Fetch-native Suaveplan API platform. Builds an executable schema from a GraphqlRe...
- `@suaveplan/api-graphql-apollo` — Optional Apollo-Server-backed GraphQL engine for the Fetch-native Suaveplan API platform. createApolloRoutes(source, opt...
- `@suaveplan/api-graphql-core` — Cycle-breaking foundation shared by the Fetch-native Suaveplan GraphQL engine and IDE packages. Owns the registry runtim...
- `@suaveplan/api-graphql-ui-altair` — Altair GraphQL IDE renderer for the Fetch-native Suaveplan API platform. createAltairRoutes(opts) returns a single GET R...
- `@suaveplan/api-graphql-ui-graphiql` — GraphiQL IDE renderer for the Fetch-native Suaveplan API platform. createGraphiqlRoutes(opts) returns a single GET Route...
- ... and 1 more
### api-jobs
3 packages. See `api-jobs/SKILL.md` for the full package list.
- `@suaveplan/api-jobs-kit` — Shared contract glue for the @suaveplan jobs.scheduler capability tier: the stable JOBS.* error-code registry + JobsErro...
- `@suaveplan/api-jobs-scheduler` — Real Jobs capability backend for the Suaveplan API platform: SchedulerJobs is a JobRegistrationHandle backed by @suavepl...
- `@suaveplan/api-jobs-scheduler-memory` — In-memory reference implementation of the jobs.scheduler capability (JobSchedulerLike from @suaveplan/api-types). Delega...
### api-middleware
7 packages. See `api-middleware/SKILL.md` for the full package list.
- `@suaveplan/api-middleware` — Shared contracts for the Suaveplan API middleware tier: ApiMiddlewareError base class and API_MIDDLEWARE_CODES registry ...
- `@suaveplan/api-middleware-audit` — Install-and-go audit plugin for the Fetch-native @suaveplan/api platform. createAuditPlugin(options) returns a @suavepla...
- `@suaveplan/api-middleware-body-limit` — Install-and-go request-body-size-limit plugin for the Fetch-native @suaveplan/api platform. createBodyLimitPlugin(option...
- `@suaveplan/api-middleware-cors` — Install-and-go CORS plugin for the Fetch-native @suaveplan/api platform. createCorsPlugin(options) returns a @suaveplan/...
- `@suaveplan/api-middleware-rate-limit` — Install-and-go rate-limit plugin for the Fetch-native @suaveplan/api platform. createRateLimitPlugin(options) returns a ...
- ... and 2 more
### api-migrations
1 package. See `api-migrations/SKILL.md` for the full package list.
- `@suaveplan/api-migrations` — Shared, declarative, plug-and-play migration runner for the Suaveplan API platform. A database-agnostic MigrationRunner ...
### api-openapi
5 packages. See `api-openapi/SKILL.md` for the full package list.
- `@suaveplan/api-openapi` — Fetch-native OpenAPI 3.1 document generation and self-contained doc UIs (Swagger UI, Redoc, Scalar) for the Suaveplan AP...
- `@suaveplan/api-openapi-core` — Cycle-breaking foundation shared by the Fetch-native Suaveplan OpenAPI doc-UI renderer packages and the api-openapi meta...
- `@suaveplan/api-openapi-ui-redoc` — Self-contained Redoc doc-UI shell for the Fetch-native Suaveplan API platform. Exposes redocUiHtml / redocUiRoute / REDO...
- `@suaveplan/api-openapi-ui-scalar` — Self-contained Scalar API Reference doc-UI shell for the Fetch-native Suaveplan API platform. Exposes scalarUiHtml / sca...
- `@suaveplan/api-openapi-ui-swagger` — Self-contained Swagger UI doc-UI shell for the Fetch-native Suaveplan API platform. Exposes swaggerUiHtml / swaggerUiRou...
### api-plugins
35 packages. See `api-plugins/SKILL.md` for the full package list.
- `@suaveplan/api-plugin-admin` — Admin / moderation plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePl...
- `@suaveplan/api-plugin-analytics` — Product-analytics plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlu...
- `@suaveplan/api-plugin-asset-register` — Fixed-asset lifecycle plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit defin...
- `@suaveplan/api-plugin-audit` — Audit-trail plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: b...
- `@suaveplan/api-plugin-auth` — Better-Auth-first authentication plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugi...
- ... and 30 more
### api-realtime
5 packages. See `api-realtime/SKILL.md` for the full package list.
- `@suaveplan/api-realtime` — Unified server-push for the Suaveplan API platform: a Fetch-native SSE route surface, a transport-neutral WebSocket hand...
- `@suaveplan/api-sse` — Abstract Server-Sent Events contracts for @suaveplan/api: SseConnection, SseBroker, SseEvent (Zod), ReplayBuffer with La...
- `@suaveplan/api-sse-bun` — The PRIMARY (Bun-native) Server-Sent Events engine for the Suaveplan API platform: a Bun.serve-backed implementation of ...
- `@suaveplan/api-ws-broker-redis` — Distributed realtime Broker for the Fetch-native Suaveplan API platform: a Redis Pub/Sub implementation of @suaveplan/ap...
- `@suaveplan/api-ws-bun` — PRIMARY Bun-native WebSocket engine for the API platform: a Bun.serve-backed implementation of the @suaveplan/api-realti...
### api-sandbox
6 packages. See `api-sandbox/SKILL.md` for the full package list.
- `@suaveplan/api-sandbox-kit` — Shared contract + conformance foundation for the @suaveplan/api-sandbox-* family. Re-exports the frozen isolation Transp...
- `@suaveplan/api-sandbox-permissions` — Per-call permission-deny gate for the Suaveplan sandbox family. Owns the SANDBOX_CODES / SandboxError registry, the Perm...
- `@suaveplan/api-sandbox-vm` — node:vm SourceTextModule isolation Transport for the Suaveplan sandbox family. createVmSandboxTransport evaluates a gues...
- `@suaveplan/api-sandbox-wasm` — WASM isolation Transport for the Suaveplan sandbox family. createWasmSandboxTransport instantiates a guest WASM module w...
- `@suaveplan/api-sandbox-worker` — node:worker_threads isolation Transport for the Suaveplan sandbox family. createWorkerTransport adapts a MessagePort / W...
- ... and 1 more
### api-secrets
3 packages. See `api-secrets/SKILL.md` for the full package list.
- `@suaveplan/api-secrets-bun` — Bun.secrets (OS keychain) SecretsLike capability adapter for the Fetch-native Suaveplan API platform. A faithful Secrets...
- `@suaveplan/api-secrets-env` — Real env-var Secrets capability for the Fetch-native API platform: a faithful SecretsLike over process.env / Bun.env wit...
- `@suaveplan/api-secrets-vault` — Secrets capability for the Suaveplan API platform: a faithful @suaveplan/api-types SecretsLike backed by @suaveplan/vaul...
### api-tenant
6 packages. See `api-tenant/SKILL.md` for the full package list.
- `@suaveplan/api-tenant` — Multi-tenancy resolution for the Suaveplan API platform. Implements the frozen TenantResolver / TenantResolverChain cont...
- `@suaveplan/api-tenant-codes` — Shared error registry and lookup contract for the @suaveplan/api tenant seam. Exports the TENANT.* stable error-code reg...
- `@suaveplan/api-tenant-header` — Header tenant resolver for the @suaveplan/api platform. HeaderTenantResolver (and the headerResolver functional sugar) i...
- `@suaveplan/api-tenant-jwt` — JWT tenant resolver for the @suaveplan/api platform. JwtTenantResolver (and the jwtResolver functional sugar) implements...
- `@suaveplan/api-tenant-memory` — In-memory tenant provider for the @suaveplan/api platform. MemoryTenantProvider is the seedable reference implementation...
- ... and 1 more
### api-transactions
1 package. See `api-transactions/SKILL.md` for the full package list.
- `@suaveplan/api-transactions` — Universal-tier distributed-transaction coordination for the Suaveplan API platform — consolidates api-saga + api-2pc int...
## Related categories
- [identity](../identity/SKILL.md)
- [persistence](../persistence/SKILL.md)

View File

@@ -0,0 +1,63 @@
---
name: api-stack-api-adapters-navigator
description: Navigator for api-stack/api-adapters. Pick a library to get detailed API guidance.
---
# Api adapters (api-stack)
Packages in `api-stack/api-adapters`.
## Libraries
### @suaveplan/api-adapter-express
Express adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside an Express host. mountExpress(runtime) returns a terminal (req, res, next) handler that forwards Expres...
→ Skill: `sp-api-adapter-express/SKILL.md`
### @suaveplan/api-adapter-fastify
Fastify adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside a Fastify host. registerFastify(instance, runtime, opts?) installs a single onRequest hook (Fastify's e...
→ Skill: `sp-api-adapter-fastify/SKILL.md`
### @suaveplan/api-adapter-hono
Hono adapter for the Suaveplan Fetch-native ApiRuntime — proves frameworks are guests, not hosts. mountHono(app, runtime, opts?) mounts a WinterCG fetch(Request) => Response runtime into a Hono app vi...
→ Skill: `sp-api-adapter-hono/SKILL.md`
### @suaveplan/api-adapter-kit
Shared parity contract for the @suaveplan/api-adapter-* family — the byte-parity foundation every framework adapter is verified against. Ships the runner-agnostic, fully-parameterized parity-conforman...
→ Skill: `sp-api-adapter-kit/SKILL.md`
### @suaveplan/api-adapter-koa
Koa adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside a Koa host. mountKoa(runtime) returns a (ctx, next) middleware that sets ctx.respond = false (so Koa relinq...
→ Skill: `sp-api-adapter-koa/SKILL.md`
### @suaveplan/api-adapter-nestjs
NestJS adapter for the Suaveplan Fetch-native ApiRuntime — mounts the runtime as a guest inside a NestJS host. mountNest(app, runtime, opts?) registers the @suaveplan/api-adapter-express terminal hand...
→ Skill: `sp-api-adapter-nestjs/SKILL.md`
### @suaveplan/api-adapter-node
Node http adapter for the Suaveplan Fetch-native ApiRuntime — the standalone half of the redesign's frameworks-are-guests keystone. toNodeListener(runtime) returns a (req, res) => void listener for ht...
→ Skill: `sp-api-adapter-node/SKILL.md`
### @suaveplan/api-adapter-registry
Name -> adapter resolution for the @suaveplan/api-adapter-* family with EXPLICIT registration and zero import side-effects. createAdapterRegistry() returns a registry whose register(name, mount) recor...
→ Skill: `sp-api-adapter-registry/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,75 @@
---
name: sp-api-adapter-express
description: Express adapter for the Fetch-native ApiRuntime. Mounts the runtime as a terminal guest handler, forwarding raw Node req/res through the shared dispatchNode bridge.
---
# API Adapter Express for Suaveplan
Express adapter for the Fetch-native `ApiRuntime`. `mountExpress(runtime)` returns a terminal `(req, res, next)` handler that forwards Express's Node-derived `req`/`res` to `@suaveplan/api-adapter-node`'s `dispatchNode`, bypassing Express's body parser and response helpers entirely. The handler is terminal (never calls `next` on success), so mount it last. `express` is an optional peer dependency; the adapter types `req`/`res` as Node base objects and never imports `express`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `mountExpress` | Returns a terminal Express handler `(req, res, next) => void` that forwards to `dispatchNode` |
| `ExpressHandler` | Type — `(req: IncomingMessage, res: ServerResponse, next: ExpressNext) => void` |
| `dispatchNode` | Shared Node bridge (from `@suaveplan/api-adapter-node`) — streams request body with `duplex: "half"`, streams response back, fail-closed 500 |
## File Structure
```
api-stack/api-adapters/api-adapter-express/
src/express/
express.ts # mountExpress implementation
express.types.ts # ExpressHandler, ExpressNext types
```
Sibling packages:
- `@suaveplan/api-adapter-node` — shared `dispatchNode` bridge used by all Node-based adapters
- `@suaveplan/api-adapter-kit` — parity conformance harness; this adapter is verified byte-identical against the Node baseline
- `@suaveplan/api-adapter-fastify` / `@suaveplan/api-adapter-koa` — other Node-based framework adapters using the same bridge
- `@suaveplan/api-adapter-hono` — WinterCG-native adapter (no Node bridge needed)
## Patterns
### Quick Start
```ts
import express from "express";
import { createApi } from "@suaveplan/api";
import { mountExpress } from "@suaveplan/api-adapter-express";
const runtime = createApi({ plugins: [], capabilities: [] });
const app = express();
app.use(mountExpress(runtime));
app.listen(3000);
```
### Path-Prefixed Mount
```ts
const app = express();
// Express owns /health and logging middleware...
app.get("/health", (_req, res) => res.json({ ok: true }));
// ...the runtime owns everything under /api
app.use("/api", mountExpress(runtime));
app.listen(8080);
// Express strips "/api" from req.url before the handler runs,
// so the runtime sees the post-strip path.
```
## Constraints
- **Terminal handler**: never calls `next()` on success; mount it last or on a catch-all route.
- **No body-parser coupling**: the runtime re-parses the request from raw socket bytes; Express's body parser is bypassed.
- **Optional peer dependency**: `express` is optional; the adapter only needs Node's `IncomingMessage`/`ServerResponse` types.
- **Byte-identical to Node baseline**: verified by `@suaveplan/api-adapter-kit` parity harness.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-express`.
2. Mount with `app.use(mountExpress(runtime))` — place it last in the middleware chain.
3. For path prefixes, use `app.use("/api", mountExpress(runtime))`; the runtime matches on the post-strip path.
4. Do not combine with Express body-parser middleware on the same route — the runtime handles its own parsing.

View File

@@ -0,0 +1,74 @@
---
name: sp-api-adapter-fastify
description: Fastify adapter for the Fetch-native ApiRuntime. Registers an onRequest hook that hijacks requests and delegates raw Node streams to the shared dispatchNode bridge.
---
# API Adapter Fastify for Suaveplan
Fastify adapter for the Fetch-native `ApiRuntime`. `registerFastify(instance, runtime, opts?)` installs a single `onRequest` hook that hijacks in-scope requests and delegates `request.raw`/`reply.raw` to `@suaveplan/api-adapter-node`'s `dispatchNode`, so the runtime re-parses the request straight from the socket; an optional path prefix lets out-of-scope requests fall through to Fastify. `fastify` is an optional peer dependency typed structurally.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `registerFastify` | Registers an `onRequest` hook on a Fastify instance; returns the same instance for chaining |
| `onRequest hook` | Seizes request at Fastify's earliest stage (before body parsing), calls `reply.hijack()`, delegates to `dispatchNode` |
| `dispatchNode` | Shared Node bridge (from `@suaveplan/api-adapter-node`) — streaming request/response with `duplex: "half"` |
| `FastifyInstanceLike` | Structural type — no hard dependency on Fastify's concrete types |
## File Structure
```
api-stack/api-adapters/api-adapter-fastify/
src/fastify/
fastify.ts # registerFastify implementation
fastify.types.ts # FastifyInstanceLike, FastifyRequestLike, FastifyReplyLike, etc.
```
Sibling packages:
- `@suaveplan/api-adapter-node` — shared `dispatchNode` bridge
- `@suaveplan/api-adapter-kit` — parity conformance harness
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-koa` — other Node-based adapters
- `@suaveplan/api-adapter-hono` — WinterCG-native adapter
## Patterns
### Quick Start
```ts
import Fastify from "fastify";
import { createApi } from "@suaveplan/api";
import { registerFastify } from "@suaveplan/api-adapter-fastify";
const runtime = createApi({ plugins: [], capabilities: [] });
const app = Fastify();
registerFastify(app, runtime);
await app.listen({ port: 3000 });
```
### Path-Prefixed Mount
```ts
const app = Fastify({ logger: true });
// Fastify owns /health; the runtime owns everything under /api
app.get("/health", async () => ({ ok: true }));
registerFastify(app, runtime, { path: "/api" });
await app.listen({ port: 8080 });
// GET /health -> Fastify; GET /api/hello -> runtime (sees /api/hello)
```
## Constraints
- **onRequest timing is essential**: the hook runs before Fastify's body parser drains `request.raw`; any later stage would hang POSTs.
- **reply.hijack() required**: without it, Fastify tries to respond a second time after the runtime has already written the response.
- **Optional peer dependency**: `fastify` is optional; structural typing keeps the adapter framework-agnostic at build time.
- **Byte-identical to Node baseline**: verified by `@suaveplan/api-adapter-kit` parity harness against a real Fastify server.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-fastify`.
2. Register with `registerFastify(app, runtime)` before `app.listen()`.
3. For path scoping, pass `{ path: "/api" }` — requests outside the prefix fall through to Fastify's router.
4. Do not register body-parser plugins that drain `request.raw` before the `onRequest` hook fires.

View File

@@ -0,0 +1,79 @@
---
name: sp-api-adapter-hono
description: Hono adapter for the Fetch-native ApiRuntime. Mounts the WinterCG runtime into Hono via hono.mount() with prefix-strip semantics; no Node bridge needed.
---
# API Adapter Hono for Suaveplan
Hono adapter for the Fetch-native `ApiRuntime`. Because the runtime already speaks the WinterCG `fetch(Request) => Promise<Response>` contract, mounting it into Hono is a one-liner with no translation layer. `mountHono(app, runtime, opts?)` mounts via `hono.mount()` (honoring prefix-strip + trailing-args semantics); `toHonoHandler(runtime)` adapts the runtime to the `(request, ...args) => Response` shape `.mount()` expects. `hono` is an optional peer dependency; this module never imports `node:*`, so it is safe on Bun, Deno, Workers, and Node.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `mountHono` | Mounts the runtime into a Hono app via `hono.mount()` and returns the same app for chaining |
| `toHonoHandler` | Adapts the runtime into the `(request, ...args) => Response` shape Hono's `.mount()` expects |
| `HonoMountable` | Structural type — no hard dependency on Hono's concrete `Hono` type |
| `HonoMountHandler` | Type — `(request: Request, ...args: readonly unknown[]) => Response \| Promise<Response>` |
## File Structure
```
api-stack/api-adapters/api-adapter-hono/
src/hono/
hono.ts # mountHono, toHonoHandler implementation
hono.types.ts # HonoMountable, HonoMountHandler, MountHonoOptions
```
Sibling packages:
- `@suaveplan/api-adapter-node` — Node bridge for Node-based frameworks
- `@suaveplan/api-adapter-kit` — parity conformance harness
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-fastify` / `@suaveplan/api-adapter-koa` — Node-based adapters
## Patterns
### Quick Start
```ts
import { Hono } from "hono";
import { createApi } from "@suaveplan/api";
import { mountHono } from "@suaveplan/api-adapter-hono";
const runtime = createApi({ plugins: [], capabilities: [] });
const app = new Hono();
mountHono(app, runtime, { path: "/api" });
// GET /api/hello reaches the runtime as GET /hello (prefix stripped)
const response = await app.request("/api/hello");
```
### Direct Handler Usage
```ts
import { Hono } from "hono";
import { toHonoHandler } from "@suaveplan/api-adapter-hono";
const handler = toHonoHandler(runtime);
const app = new Hono()
.mount("/v1", handler)
.mount("/v2", handler); // same runtime behind two prefixes
// Hono passes (request, env, executionCtx); the adapter forwards
// only the Request to runtime.fetch and ignores env/executionCtx.
const direct = await handler(new Request("http://host/v1/hello"), {}, {});
```
## Constraints
- **No Node bridge needed**: because both Hono and the runtime speak WinterCG `fetch`, there is no `IncomingMessage` translation layer.
- **Prefix stripped by Hono**: the adapter performs no path rewriting of its own; Hono strips the mount prefix before invoking the handler.
- **Trailing args ignored**: Hono passes `(request, env, executionCtx)` for Workers-style runtimes; the adapter discards everything after the first `Request`.
- **Optional peer dependency**: `hono` is optional; structural typing avoids a hard build-time dependency.
- **Byte-identical to Node baseline**: verified by `@suaveplan/api-adapter-kit` parity harness.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-hono`.
2. Use `mountHono(app, runtime, { path: "/api" })` for prefix-mounted runtime access.
3. Use `toHonoHandler(runtime)` when you need the raw handler for custom composition.
4. Safe to import in Bun, Deno, Workers, and browser-adjacent runtimes — no `node:*` imports.

View File

@@ -0,0 +1,111 @@
---
name: sp-api-adapter-kit
description: Framework-agnostic parity conformance harness for the api-adapter family. Asserts byte-identical responses across adapters via caller-supplied driver closures.
---
# API Adapter Kit for Suaveplan
Framework-agnostic parity-conformance harness for the `@suaveplan/api-adapter-*` family. `runAdapterParityConformance` drives the same requests through a caller-supplied list of `AdapterDriver` closures and asserts each is byte-identical to the first (baseline); `runFrameworkParityConformance` is the two-adapter convenience wrapper. The harness imports NO adapter package — adapters depend on the kit (test-only), never the reverse — so the kit sits beneath every adapter in the dependency graph and the kit → adapter cycle cannot form.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `runAdapterParityConformance` | N-adapter matrix — registers one parity test per (request × non-baseline adapter), asserting status + byte-identical body |
| `runFrameworkParityConformance` | Two-adapter convenience wrapper — Node baseline vs one framework adapter |
| `AdapterDriver` | Caller-supplied closure `{ name, drive }` — owns the framework import and server bootstrap |
| `ParityRequest` | Test case — `{ name, path, method?, body?, headers? }` |
| `ParityResponse` | Reduced response — `{ status, body }` — compared byte-for-byte |
## File Structure
```
api-stack/api-adapters/api-adapter-kit/
src/parity/
parity.ts # runAdapterParityConformance, runFrameworkParityConformance
parity.types.ts # ParityRequest, ParityResponse, AdapterDriver, etc.
```
Sibling packages:
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-fastify` / `@suaveplan/api-adapter-hono` / `@suaveplan/api-adapter-koa` / `@suaveplan/api-adapter-node` — all verified against this harness
- `@suaveplan/api` — the runtime being tested through adapters
## Patterns
### Two-Adapter Parity
```ts
import { describe, expect, it } from "@suaveplan/testing/runner";
import { toNodeListener } from "@suaveplan/api-adapter-node";
import { mountHono } from "@suaveplan/api-adapter-hono";
import { runFrameworkParityConformance } from "@suaveplan/api-adapter-kit";
import { Hono } from "hono";
import { createServer } from "node:http";
const runtime = createApi({ plugins: [], capabilities: [] });
async function driveNode(request) {
const server = createServer(toNodeListener(runtime));
await new Promise((r) => server.listen(0, "127.0.0.1", r));
try {
const { port } = server.address();
const res = await fetch(`http://127.0.0.1:${port}${request.path}`);
return { status: res.status, body: await res.text() };
} finally {
await new Promise((r) => server.close(() => r()));
}
}
describe("parity", () => {
runFrameworkParityConformance({
driveBaseline: driveNode,
framework: "hono",
requests: [{ name: "GET /hello", path: "/hello" }],
test: it,
expect,
driveFramework: async (request) => {
const app = mountHono(new Hono(), runtime, { path: "/api" });
const res = await app.request(`/api${request.path}`);
return { status: res.status, body: await res.text() };
},
});
});
```
### N-Adapter Matrix
```ts
import { runAdapterParityConformance } from "@suaveplan/api-adapter-kit";
runAdapterParityConformance({
adapters: [
{ name: "node", drive: driveNode },
{ name: "hono", drive: driveHono },
{ name: "fastify", drive: driveFastify },
{ name: "express", drive: driveExpress },
],
requests: [
{ name: "GET /hello", path: "/hello" },
{ name: "POST /echo", path: "/echo", method: "POST", body: "hi" },
],
test: it,
expect,
});
```
## Constraints
- **Caller owns adapters**: the harness imports NO framework and NO adapter; you supply each as a `drive` closure.
- **Baseline is adapters[0]**: every other adapter is compared against the first entry in the adapters array.
- **Fail-fast**: throws `RangeError` synchronously if fewer than two adapters are supplied.
- **Runner-agnostic**: accepts `test` and `expect` as parameters; imports no `vitest`/`bun:test` primitives.
- **Real I/O only**: each driver must run its adapter's real server/mount; no mocks.
- **Body bytes only**: compares `status` and body text; per-framework header noise is intentionally ignored.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-kit`.
2. Write a `drive` closure for each adapter that boots the real server and returns `{ status, body }`.
3. Pass `test` and `expect` from your test runner.
4. Use `runFrameworkParityConformance` for the common Node-vs-one-framework case.
5. Use `runAdapterParityConformance` for multi-framework matrices (3+ adapters).

View File

@@ -0,0 +1,76 @@
---
name: sp-api-adapter-koa
description: Koa adapter for the Fetch-native ApiRuntime. Terminal middleware that sets ctx.respond = false and delegates raw Node req/res to the shared dispatchNode bridge.
---
# API Adapter Koa for Suaveplan
Koa adapter for the Fetch-native `ApiRuntime`. `mountKoa(runtime)` returns a `(ctx, next)` middleware that sets `ctx.respond = false` and delegates `ctx.req`/`ctx.res` to `@suaveplan/api-adapter-node`'s `dispatchNode`, so the runtime re-parses each request from the socket bytes and streams its response back without Koa's `ctx.body`/`ctx.status` model touching the payload. The middleware is terminal (never calls `next`), so mount it last in the onion; upstream onion middleware still runs around it. `koa` is an optional peer dependency typed structurally.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `mountKoa` | Returns a terminal Koa middleware `(ctx, next) => Promise<void>` that forwards to `dispatchNode` |
| `KoaContextLike` | Structural type — `{ req: IncomingMessage; res: ServerResponse; respond: boolean }` |
| `dispatchNode` | Shared Node bridge (from `@suaveplan/api-adapter-node`) — streaming request/response with `duplex: "half"` |
## File Structure
```
api-stack/api-adapters/api-adapter-koa/
src/koa/
koa.ts # mountKoa implementation
koa.types.ts # KoaContextLike, KoaMiddleware, KoaNext
```
Sibling packages:
- `@suaveplan/api-adapter-node` — shared `dispatchNode` bridge
- `@suaveplan/api-adapter-kit` — parity conformance harness
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-fastify` — other Node-based adapters
- `@suaveplan/api-adapter-hono` — WinterCG-native adapter
## Patterns
### Quick Start
```ts
import Koa from "koa";
import { createApi } from "@suaveplan/api";
import { mountKoa } from "@suaveplan/api-adapter-koa";
const runtime = createApi({ plugins: [], capabilities: [] });
const app = new Koa();
app.use(mountKoa(runtime));
app.listen(3000);
```
### Upstream Middleware + Terminal Runtime
```ts
const app = new Koa();
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // descends into the runtime mount
ctx.set("x-response-time", `${Date.now() - start}ms`);
});
app.use(mountKoa(runtime)); // terminal: produces the response
app.listen(8080);
```
## Constraints
- **ctx.respond = false is required**: without it, Koa overwrites the runtime's response with a 404 because `ctx.body` was never set.
- **Terminal middleware**: never calls `next`; mount it last in the onion.
- **Upstream middleware still works**: `await next()` timing and teardown logic on the way back out remains correct.
- **Optional peer dependency**: `koa` is optional; structural typing avoids a hard build-time dependency.
- **Byte-identical to Node baseline**: verified by `@suaveplan/api-adapter-kit` parity harness against a real Koa server.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-koa`.
2. Mount with `app.use(mountKoa(runtime))` — place it last in the onion.
3. Upstream middleware that wraps `await next()` will still run correctly around the terminal mount.
4. Do not set `ctx.body` or `ctx.status` after the runtime mount — the runtime owns the response.

View File

@@ -0,0 +1,72 @@
---
name: sp-api-adapter-nestjs
description: NestJS adapter for the Fetch-native ApiRuntime. Registers the Express terminal handler through Nest's app.use(), so the runtime re-parses requests from raw socket bytes.
---
# API Adapter NestJS for Suaveplan
NestJS adapter for the Fetch-native `ApiRuntime`. `mountNest(app, runtime, opts?)` registers `@suaveplan/api-adapter-express`'s terminal handler through Nest's Express-compatible `app.use(...)`, so the runtime re-parses each request from the raw socket bytes — Nest's controllers, pipes, and interceptors never touch the bytes. As thin as Nest allows: a single catch-all middleware. The `@nestjs/*` packages are optional peer dependencies typed structurally.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `mountNest` | Registers a terminal Express-style middleware on the Nest application via `app.use(...)`; returns the same app for chaining |
| `NestApplicationLike` | Structural type — exposes `use(handler)` and `use(path, handler)` overloads without hard-depending on Nest's concrete types |
| `mountExpress` | Reused from `@suaveplan/api-adapter-express` — forwards to the shared `dispatchNode` bridge |
## File Structure
```
api-stack/api-adapters/api-adapter-nestjs/
src/nestjs/
nestjs.ts # mountNest implementation
nestjs.types.ts # NestApplicationLike, RegisterNestOptions
```
Sibling packages:
- `@suaveplan/api-adapter-express` — the Express mount reused under the hood
- `@suaveplan/api-adapter-node` — shared `dispatchNode` bridge
- `@suaveplan/api-adapter-kit` — parity conformance harness
## Patterns
### Quick Start
```ts
import { NestFactory } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { createApi } from "@suaveplan/api";
import { mountNest } from "@suaveplan/api-adapter-nestjs";
@Module({})
class AppModule {}
const runtime = createApi({ plugins: [], capabilities: [] });
const app = await NestFactory.create(AppModule);
mountNest(app, runtime);
await app.listen(3000);
```
### Path-Prefixed Mount
```ts
const app = await NestFactory.create(AppModule);
// Nest controllers serve their own routes; the runtime owns everything under /api
mountNest(app, runtime, { path: "/api" });
await app.listen(8080);
```
## Constraints
- **Reuses Express mount**: because Nest runs on `@nestjs/platform-express`, the adapter registers the same terminal handler as the Express adapter.
- **Optional peer dependencies**: `@nestjs/common`, `@nestjs/core`, and `@nestjs/platform-express` are all optional; structural typing avoids hard build-time dependencies.
- **Controllers never touch the bytes**: the runtime owns routing end-to-end; Nest's pipes, guards, and interceptors are bypassed.
- **Byte-identical to Node baseline**: verified by `@suaveplan/api-adapter-kit` parity harness against a real Nest application.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-nestjs`.
2. Mount with `mountNest(app, runtime)` after `NestFactory.create()`.
3. For path scoping, pass `{ path: "/api" }` — Express strips the prefix before the handler runs.

View File

@@ -0,0 +1,79 @@
---
name: sp-api-adapter-node
description: Node http adapter for the Fetch-native ApiRuntime. Bridges Node's stream-based HTTP to WHATWG Request/Response with streaming bodies and fail-closed error handling.
---
# API Adapter Node for Suaveplan
Node `http` adapter for the Fetch-native `ApiRuntime`. `toNodeListener(runtime)` returns a `(req, res) => void` listener for `http.createServer`; `dispatchNode(runtime, req, res)` is the awaitable `IncomingMessage ⇄ Request` bridge (streamed body, `duplex: "half"`, fail-closed 500) that every host-framework adapter reuses. `nodeBodyToReadableStream(source)` wraps a Node `Readable` as a WHATWG `ReadableStream`. Imports only `node:*`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `toNodeListener` | Returns a `(req, res) => void` listener for `http.createServer` |
| `dispatchNode` | Single-cycle bridge — converts `IncomingMessage → Request`, runs `runtime.fetch`, pipes `Response → ServerResponse` |
| `nodeBodyToReadableStream` | Wraps a Node `Readable` as a WHATWG `ReadableStream` with `duplex: "half"` |
## File Structure
```
api-stack/api-adapters/api-adapter-node/
src/node/
node.ts # toNodeListener, dispatchNode, nodeBodyToReadableStream
node.types.ts # NodeRequestListener
```
Sibling packages:
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-fastify` / `@suaveplan/api-adapter-koa` — all delegate to `dispatchNode`
- `@suaveplan/api-adapter-hono` — WinterCG-native, does not need this bridge
- `@suaveplan/api-adapter-kit` — parity conformance harness
## Patterns
### Quick Start
```ts
import { createServer } from "node:http";
import { createApi } from "@suaveplan/api";
import { toNodeListener } from "@suaveplan/api-adapter-node";
const runtime = createApi({ plugins: [], capabilities: [] });
const server = createServer(toNodeListener(runtime));
server.listen(3000);
```
### Streaming Body Bridge
```ts
import { PassThrough } from "node:stream";
import { nodeBodyToReadableStream } from "@suaveplan/api-adapter-node";
const source = new PassThrough();
const body = nodeBodyToReadableStream(source);
const request = new Request("http://host/upload", {
method: "POST",
body,
// @ts-expect-error duplex is required for streaming bodies
duplex: "half",
});
source.write("chunk-1");
source.end();
await request.text(); // "chunk-1"
```
## Constraints
- **Streams both directions**: large and chunked payloads never buffer fully in memory.
- **Fail-closed 500**: a rejected `runtime.fetch` ends the socket with a 500 (only writing the head if it has not already been sent).
- **URL absolutization**: `IncomingMessage.url` is only the path; the bridge absolutizes against the `host` header because WHATWG `Request` requires an absolute URL.
- **duplex: "half" mandatory**: the WHATWG Fetch spec requires it whenever a streaming body is supplied; omitting it throws.
- **Body attached only for non-GET/HEAD**: GET and HEAD requests do not carry a body per HTTP semantics.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-node`.
2. Use `toNodeListener(runtime)` with `http.createServer` for standalone Node deployment.
3. Use `dispatchNode(runtime, req, res)` directly when building custom framework integrations.
4. Use `nodeBodyToReadableStream(source)` when you need to bridge any Node `Readable` to a WHATWG `ReadableStream`.
5. Always pass `duplex: "half"` when constructing a `Request` with a streaming body.

View File

@@ -0,0 +1,94 @@
---
name: sp-api-adapter-registry
description: Explicit, side-effect-free name-to-adapter resolution for the api-adapter family. Register, resolve, has, unregister, list — with fail-closed duplicates and typed generics.
---
# API Adapter Registry for Suaveplan
Explicit, side-effect-free name→adapter resolution for the `@suaveplan/api-adapter-*` family. `createAdapterRegistry()` returns a registry whose `register`/`resolve`/`has`/`unregister`/`list` operate on framework mount functions keyed by a stable name. Duplicate registration fails closed (`ADAPTER_REGISTRY.DUPLICATE`) and unknown resolution throws (`ADAPTER_REGISTRY.NOT_REGISTERED`). Nothing auto-registers on import, so a third-party adapter joins without editing any core package, and an unused registry tree-shakes away.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `createAdapterRegistry` | Factory — returns an empty, strongly typed registry |
| `register` | Records a mount under a name; throws on duplicate |
| `resolve` | Returns the mount for a name; throws when not registered |
| `has` | `true` when a name is registered |
| `unregister` | Removes an entry; idempotent — never throws |
| `list` | Registered names in insertion order (deterministic, backed by `Map`) |
| `ADAPTER_REGISTRY_CODES` | Error namespace — `DUPLICATE`, `NOT_REGISTERED` |
## File Structure
```
api-stack/api-adapters/api-adapter-registry/
src/registry/
registry.ts # createAdapterRegistry + AdapterRegistry interface
registry.types.ts # AdapterMap, AdapterMount, typed generics
src/error-codes/ # ADAPTER_REGISTRY_CODES + AdapterRegistryError
```
Sibling packages:
- `@suaveplan/api-adapter-express` / `@suaveplan/api-adapter-fastify` / `@suaveplan/api-adapter-hono` / `@suaveplan/api-adapter-koa` / `@suaveplan/api-adapter-nestjs` / `@suaveplan/api-adapter-node` — all can be registered in the registry
## Patterns
### Quick Start
```ts
import { createAdapterRegistry } from "@suaveplan/api-adapter-registry";
const registry = createAdapterRegistry();
registry.register("hono", mountHono).register("node", toNodeListener);
if (registry.has("hono")) {
const mount = registry.resolve("hono");
mount(app, runtime);
}
registry.list(); // ["hono", "node"]
```
### Typed Registry
```ts
import { createAdapterRegistry, AdapterRegistryError } from "@suaveplan/api-adapter-registry";
const registry = createAdapterRegistry<{
hono: typeof mountHono;
node: typeof toNodeListener;
}>();
registry.register("hono", mountHono).register("node", toNodeListener);
const mount = registry.resolve("hono"); // typed as typeof mountHono
// Replace an adapter: unregister first, then register
registry.unregister("hono");
registry.register("hono", mountHono);
try {
registry.register("hono", mountHono); // duplicate — fails closed
} catch (error) {
if (error instanceof AdapterRegistryError) {
console.error(error.code, error.context?.["name"]); // ADAPTER_REGISTRY.DUPLICATE hono
}
}
```
## Constraints
- **Explicit registration only**: nothing auto-registers on import; the host must call `register`.
- **Fail-closed duplicates**: `register` rejects a duplicate; callers must `unregister` first to replace.
- **Fail-fast resolution**: `resolve` throws at the lookup site rather than returning `undefined`.
- **Tree-shakeable**: `sideEffects: false`; an unused registry is dropped by the bundler.
- **Universal tier**: imports only `@suaveplan/error` and `@suaveplan/types`.
## Checklist
1. Install: `bun add @suaveplan/api-adapter-registry`.
2. Create a registry with `createAdapterRegistry<Adapters>()` for full type safety.
3. Register adapters explicitly; never assume a name is present without `has()` or a try/catch.
4. Use `unregister` before re-registering to avoid `ADAPTER_REGISTRY.DUPLICATE`.
5. Pass typed `Adapters` to recover exact mount-function signatures at `resolve` call sites.

View File

@@ -0,0 +1,27 @@
---
name: api-stack-api-audit-navigator
description: Navigator for api-stack/api-audit. Pick a library to get detailed API guidance.
---
# Api audit (api-stack)
Packages in `api-stack/api-audit`.
## Libraries
### @suaveplan/api-audit-file
File-rotating, HMAC-SHA-256-signed AuditSink for the Fetch-native @suaveplan/api platform. Implements the @suaveplan/api-types `AuditSink` contract (`write(event: AuditEvent)`) so it drops into `audit...
→ Skill: `sp-api-audit-file/SKILL.md`
### @suaveplan/api-audit-stdout
Stdout `AuditSink` for the Fetch-native @suaveplan/api platform. Implements the @suaveplan/api-types `AuditSink` contract (`write(event: AuditEvent)`) so it drops into `auditMiddleware({ sinks: [...] ...
→ Skill: `sp-api-audit-stdout/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,86 @@
---
name: sp-api-audit-file
description: File-rotating, HMAC-SHA-256-signed AuditSink for the Fetch-native API platform. JSON-per-line with rotation, retention, and bounded backpressure.
---
# API Audit File for Suaveplan
File-rotating, HMAC-SHA-256-signed `AuditSink` for the Fetch-native `@suaveplan/api` platform. Implements the `AuditSink` contract (`write(event: AuditEvent)`) so it drops into `auditMiddleware({ sinks: [...] })`. Writes one JSON line per record with an HMAC signature over canonical bytes, rolls the active file on size OR age, prunes the oldest rotated siblings to a `maxFiles` retention cap, bounds backpressure with an observable drop counter, and offers append + strict-sync write modes. Server-tier — Bun + Node.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `createFileAuditSink` | Factory — composes schema, HMAC, rotation, retention, and instruments into an `AuditSink` |
| `buildSignedLine` / `canonicalise` | Produces canonical bytes and appends an HMAC-SHA-256 signature |
| `rotate` / `shouldRotate` | Rolls the active file when `maxSizeBytes` or `maxAgeMs` is exceeded |
| `enforceRetention` | Deletes oldest rotated files until at most `maxFiles` remain |
| `nodeFsAdapter` / `FsAdapter` | Injectable filesystem abstraction; default uses `node:fs/promises` |
| `createAuditFileInstruments` | OTel counters — `api.audit.records_total{sink="file"}`, `api.audit.dropped_total`, `api.audit.rotation_evicted_total` |
| `AUDIT_FILE_CODES` | Error namespace — `INVALID_RECORD`, `WRITER_FAILED`, `QUEUE_FULL`, `ROTATION_FAILED`, `RETENTION_FAILED`, `PATH_NOT_WRITEABLE` |
## File Structure
```
api-stack/api-audit/api-audit-file/
src/sink/ # createFileAuditSink, FileAuditSink, AppendMode
src/schema/ # AuditRecord, canonicalise, buildSignedLine
src/hmac/ # signRecord, verifyRecord
src/rotation/ # RotationPolicy, shouldRotate, rotate
src/retention/ # enforceRetention
src/fs-helpers/ # FsAdapter, nodeFsAdapter, listRotatedFiles
src/instruments/ # AuditFileInstruments, SINK_ATTRIBUTE
src/error-codes/ # AUDIT_FILE_CODES
```
Sibling packages:
- `@suaveplan/api-audit-stdout` — sibling sink for log-shipped environments
- `@suaveplan/api-middleware` — provides `auditMiddleware` that fans out to sinks
- `@suaveplan/api-plugin-audit` — capture seam that consumes sinks
- `@suaveplan/crypto-hash` — HMAC-SHA-256 implementation
## Patterns
### Quick Start
```ts
import { auditMiddleware } from "@suaveplan/api-middleware";
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",
});
const middleware = auditMiddleware({ sinks: [sink] });
// Mount in createApi({ middleware: [middleware] })
// Call sink.dispose() on shutdown to flush the queue.
```
### Verifying a Record
```ts
import { verifyRecord } from "@suaveplan/api-audit-file/hmac";
const isValid = verifyRecord(record, hmacKey);
```
## Constraints
- **Server-tier only**: depends on `node:fs/promises`; does not run in the browser.
- **HMAC key required**: without it, signatures cannot be produced or verified.
- **Rotation is size OR age**: whichever threshold is hit first triggers a roll.
- **Retention is hard-capped**: `maxFiles` is enforced after every rotation; oldest files are deleted first.
- **Drops are observable**: when backpressure exceeds bounds, the `api.audit.dropped_total` counter increments.
- **Call dispose() on shutdown**: flushes any queued writes and closes the file handle.
## Checklist
1. Install: `bun add @suaveplan/api-audit-file`.
2. Provide an `hmacKey` (Uint8Array) for tamper-evident signatures.
3. Configure `rotation` with `maxSizeBytes`, `maxAgeMs`, and `maxFiles`.
4. Choose `appendMode`: `"append"` (fast, async) or `"strict-sync"` (durable, synchronous).
5. Pass the sink to `auditMiddleware({ sinks: [sink] })`.
6. Call `await sink.dispose()` on application shutdown to flush pending writes.

View File

@@ -0,0 +1,80 @@
---
name: sp-api-audit-stdout
description: Stdout AuditSink for the Fetch-native API platform. HMAC-signed JSON-per-line with bounded backpressure, observable drops, and injectable LineWriter.
---
# API Audit Stdout for Suaveplan
Stdout `AuditSink` for the Fetch-native `@suaveplan/api` platform. Implements the `AuditSink` contract (`write(event: AuditEvent)`) so it drops into `auditMiddleware({ sinks: [...] })`. Writes one HMAC-SHA-256-signed JSON line per record to an injectable `LineWriter` (default `process.stdout`) with bounded backpressure (`maxQueueMs` default 100ms), an observable drop counter, and a once-per-minute drop-warning throttle. Drops are never silent.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `createStdoutAuditSink` | Factory — composes schema, HMAC, instruments, and options into an `AuditSink` |
| `signRecord` / `verifyRecord` | HMAC-SHA-256 signature production and verification over canonical bytes |
| `canonicalBytes` | Produces the canonical byte representation of an audit record for signing |
| `createStdoutAuditInstruments` | OTel counters — `api.audit.records_total{sink="stdout", outcome}` and `api.audit.dropped_total{sink="stdout", reason}` |
| `AUDIT_STDOUT_CODES` | Error namespace — `INVALID_RECORD`, `WRITER_FAILED`, `QUEUE_FULL`, `INVALID_OPTIONS` |
## File Structure
```
api-stack/api-audit/api-audit-stdout/
src/sink/ # createStdoutAuditSink, StdoutAuditSink, LineWriter
src/schema/ # AuditRecord, canonicalBytes, auditRecordSchema
src/hmac/ # signRecord, verifyRecord, HMAC_FIELD
src/options/ # scalar option schema + defaults (maxQueueMs = 100)
src/instruments/ # StdoutAuditInstruments, SINK_ATTRIBUTE
src/error-codes/ # AUDIT_STDOUT_CODES
```
Sibling packages:
- `@suaveplan/api-audit-file` — sibling sink for durable local storage with rotation + retention
- `@suaveplan/api-middleware` — provides `auditMiddleware` that fans out to sinks
- `@suaveplan/api-plugin-audit` — capture seam that consumes sinks
- `@suaveplan/crypto-hash` — HMAC-SHA-256 implementation
## Patterns
### Quick Start
```ts
import { auditMiddleware } from "@suaveplan/api-middleware";
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout/sink";
const sink = createStdoutAuditSink({
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
});
const middleware = auditMiddleware({ sinks: [sink] });
// Mount in createApi({ middleware: [middleware] })
```
### Custom LineWriter
```ts
const lines: string[] = [];
const sink = createStdoutAuditSink({
hmacKey,
lineWriter: {
write: (line) => { lines.push(line); },
},
});
```
## Constraints
- **Server-tier only**: writes to `process.stdout` by default; does not run in the browser.
- **Backpressure bounded**: `maxQueueMs` (default 100ms) is the maximum queue wait; exceeding it drops the record.
- **Drops are never silent**: the `api.audit.dropped_total` counter increments and a warning is logged (subject to `warnThrottleMs`).
- **HMAC key required**: without it, signatures cannot be produced or verified.
- **Injectable LineWriter**: default is `process.stdout`; swap for testing or custom log shipping.
## Checklist
1. Install: `bun add @suaveplan/api-audit-stdout`.
2. Provide an `hmacKey` (Uint8Array) for tamper-evident signatures.
3. Pass the sink to `auditMiddleware({ sinks: [sink] })`.
4. For testing, inject a custom `LineWriter` to capture lines instead of writing to stdout.
5. Monitor `api.audit.dropped_total{sink="stdout"}` for backpressure events.

View File

@@ -0,0 +1,57 @@
---
name: api-stack-api-auth-navigator
description: Navigator for api-stack/api-auth. Pick a library to get detailed API guidance.
---
# Api auth (api-stack)
Packages in `api-stack/api-auth`.
## Libraries
### @suaveplan/api-auth
Authentication-strategy meta-package for the Suaveplan API platform. Each strategy is a factory returning a frozen SessionResolver (Request -> Principal | null) that plugs into createApi({ sessionReso...
→ Skill: `sp-api-auth/SKILL.md`
### @suaveplan/api-auth-anonymous
Anonymous (no-authentication) session-resolver strategy for the @suaveplan API auth seam. createAnonymousResolver(options?) returns a frozen SessionResolver (Request -> Principal | null) that resolves...
→ Skill: `sp-api-auth-anonymous/SKILL.md`
### @suaveplan/api-auth-bearer
Bearer-token session-resolver strategy for the @suaveplan API auth seam. createBearerResolver(options) returns a frozen SessionResolver (Request -> Principal | null) that authenticates by matching the...
→ Skill: `sp-api-auth-bearer/SKILL.md`
### @suaveplan/api-auth-better-auth
Better-Auth-backed SessionResolver for the Suaveplan API platform. The heavy authentication integration (kept separate from the lightweight @suaveplan/api-auth strategies): createBetterAuthSessionReso...
→ Skill: `sp-api-auth-better-auth/SKILL.md`
### @suaveplan/api-auth-codes
Shared stable error-code registry for the @suaveplan API auth seam. Exports AUTH_CODES (defineCodes("AUTH", ["OPTIONS_INVALID", "CHAIN_EMPTY", "VERIFIER_FAILED"])), the AuthError class extending @suav...
→ Skill: `sp-api-auth-codes/SKILL.md`
### @suaveplan/api-auth-jwt
JWT session-resolver strategy for the @suaveplan API auth seam. createJwtResolver(options) returns a frozen SessionResolver (Request -> Principal | null) that reads a bearer JWT from the Authorization...
→ Skill: `sp-api-auth-jwt/SKILL.md`
### @suaveplan/api-auth-static
Static (fixed-principal) session-resolver strategy for the @suaveplan API auth seam. createStaticResolver(options) returns a frozen SessionResolver (Request -> Principal | null) that resolves the same...
→ Skill: `sp-api-auth-static/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,100 @@
---
name: sp-api-auth-anonymous
description: Anonymous (no-authentication) SessionResolver strategy for the Fetch-native API platform. Returns null for every request by default, or one fixed guest Principal when configured. Stateless, allocation-free, and never throws.
---
# @suaveplan/api-auth-anonymous
## Purpose
Anonymous (no-authentication) session-resolver strategy for the Suaveplan API platform. `createAnonymousResolver(options?)` returns a frozen `SessionResolver` (`resolve(request): Promise<Principal | null>`) that plugs into `createApi({ sessionResolver })`.
With no options it returns `null` for every request, marking the surface unauthenticated. Supply `{ principal }` to resolve every request to one fixed guest identity instead. It is the explicit, auditable way to declare a surface unauthenticated, and the terminal fallback in a `createResolverChain`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAnonymousResolver` | `src/anonymous/anonymous.ts` | Factory returning a frozen always-null or fixed-identity `SessionResolver` |
| `AnonymousResolverOptions` | `src/anonymous/anonymous.ts` | Options interface (`principal?: Principal`) |
## File Structure
```
src/
index.ts # Exports createAnonymousResolver + AnonymousResolverOptions
anonymous/
anonymous.ts # Factory implementation
anonymous.test.ts # Unit tests
index.ts # Subpath barrel
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-anonymous` | `createAnonymousResolver`, `AnonymousResolverOptions` |
| `@suaveplan/api-auth-anonymous/anonymous` | Same from feature subpath |
## Patterns
### Unauthenticated surface
```ts
import { createApi } from "@suaveplan/api";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous";
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createAnonymousResolver(), // every request => ctx.principal null
});
```
### Fixed guest identity for public routes
```ts
import type { PrincipalId, Scope } from "@suaveplan/api-types/principal";
const GUEST = {
id: "guest" as PrincipalId,
scopes: ["public:read"] as readonly Scope[],
claims: { anonymous: true },
};
const runtime = createApi({
sessionResolver: createAnonymousResolver({ principal: GUEST }),
});
```
### Terminal fallback in a chain
```ts
import { createResolverChain } from "@suaveplan/api-auth/chain";
import { createJwtResolver } from "@suaveplan/api-auth-jwt";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous";
const runtime = createApi({
sessionResolver: createResolverChain(
createJwtResolver({ hmacSecret: process.env.JWT_SECRET! }),
createAnonymousResolver(), // fallback: unauthenticated
),
});
```
## Constraints
- **Never throws** — no error codes defined; the resolver is always safe to call.
- **Stateless** — holds no per-request state; the fixed principal reference is captured at construction.
- **Allocation-free per request** — `resolve()` simply returns the pre-captured value.
- **Empty options equivalent to no options** — `createAnonymousResolver({})` and `createAnonymousResolver()` behave identically.
- **Dependencies**: `@suaveplan/api-types`, `tslib`.
## Error Codes
None. This package carries no error codes because the resolver never throws.
## Checklist
- [ ] Read `src/anonymous/anonymous.ts` before changing resolver behaviour
- [ ] Read `src/anonymous/anonymous.test.ts` before modifying test assertions
- [ ] No new error codes should be added here — the anonymous resolver is defined as never-throwing

View File

@@ -0,0 +1,121 @@
---
name: sp-api-auth-bearer
description: Bearer-token SessionResolver strategy for the Fetch-native API platform. Constant-time match of the Authorization header credential against a fixed in-memory token->principal table via @suaveplan/crypto-utils constantTimeEqual. Configurable header and scheme.
---
# @suaveplan/api-auth-bearer
## Purpose
Bearer-token session-resolver strategy for the Suaveplan API platform. `createBearerResolver(options)` returns a frozen `SessionResolver` that authenticates by matching the bearer credential on the `Authorization` header against a fixed in-memory token→principal table.
This is a real, complete verification: the presented token is compared to every entry with `constantTimeEqual` from `@suaveplan/crypto-utils`, walking the whole table without short-circuiting so the matching position is not leaked by timing.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createBearerResolver` | `src/bearer/bearer.ts` | Factory returning a frozen bearer-token `SessionResolver` |
| `BearerResolverOptions` | `src/bearer/bearer.ts` | Options: `tokens`, `headerName?`, `scheme?` |
| `BearerTokenEntry` | `src/bearer/bearer.ts` | One `{ token, principal }` entry in the resolver's table |
| `extractCredential` | `src/bearer/bearer.ts` | Reads `<scheme> <credential>` from the configured header |
| `matchToken` | `src/bearer/bearer.ts` | Constant-time table lookup without short-circuiting |
## File Structure
```
src/
index.ts # Exports createBearerResolver + option types
bearer/
bearer.ts # Factory + credential extraction + constant-time match
bearer.test.ts # Unit tests
index.ts # Subpath barrel
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-bearer` | `createBearerResolver`, `BearerResolverOptions`, `BearerTokenEntry` |
| `@suaveplan/api-auth-bearer/bearer` | Same from feature subpath |
## Patterns
### Basic bearer-token auth
```ts
import { createApi } from "@suaveplan/api";
import { createBearerResolver } from "@suaveplan/api-auth-bearer";
import type { PrincipalId, Scope } from "@suaveplan/api-types/principal";
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createBearerResolver({
tokens: [
{
token: "sk_live_abc123",
principal: {
id: "u_alice" as PrincipalId,
scopes: ["billing:read"] as readonly Scope[],
claims: {},
},
},
],
}),
});
```
### Custom header and scheme
```ts
const resolver = createBearerResolver({
tokens: [{ token: "k", principal: alice }],
headerName: "x-api-key",
scheme: "Token",
});
// Matches: X-Api-Key: Token k
```
### Constant-time matching guarantee
```ts
// The resolver walks every entry and compares with constantTimeEqual.
// Even the POSITION of a match does not leak by timing.
const resolver = createBearerResolver({
tokens: [
{ token: "tok-alice", principal: alice },
{ token: "tok-bob", principal: bob },
],
});
```
### Soft failures return null (not errors)
```ts
// All of these return null — the request is treated as unauthenticated
await resolver.resolve(new Request("https://api.test")); // no header
await resolver.resolve(withAuth("Basic tok-alice")); // wrong scheme
await resolver.resolve(withAuth("Bearer")); // empty credential
await resolver.resolve(withAuth("Bearer nope")); // unknown token
```
## Constraints
- **Constant-time matching** — `constantTimeEqual` compares every entry; no short-circuiting.
- **Non-empty table required** — empty `tokens` array raises `AUTH.OPTIONS_INVALID` at construction (Zod-validated).
- **Soft failures are null** — missing header, wrong scheme, empty credential, unknown token all return `null`.
- **Case-insensitive scheme matching** — both header name and scheme are compared case-insensitively.
- **Dependencies**: `@suaveplan/api-auth-codes`, `@suaveplan/api-types`, `@suaveplan/crypto-utils`, `@suaveplan/error`, `tslib`, `zod`.
## Error Codes
| Code | Raised When |
|---|---|
| `AUTH.OPTIONS_INVALID` | Empty token table, or options fail Zod boundary validation |
## Checklist
- [ ] Read `src/bearer/bearer.ts` before modifying token matching logic
- [ ] Read `src/bearer/bearer.test.ts` before changing test assertions
- [ ] Preserve constant-time matching — never short-circuit the token comparison loop
- [ ] Soft failures must return `null`, never throw

View File

@@ -0,0 +1,175 @@
---
name: sp-api-auth-better-auth
description: Better-Auth-backed SessionResolver for the Fetch-native API platform. Wraps a real Better-Auth instance (backed by @suaveplan/better-auth-surrealdb), validates session cookie/bearer via auth.api.getSession, and maps (user, session) onto the frozen Principal contract. Ships an AuthProviderAdapter, default principal mapping with sensitive-field redaction, a 4-code AUTH_BA.* error registry, and a ./testing conformance harness.
---
# @suaveplan/api-auth-better-auth
## Purpose
Better-Auth-backed `SessionResolver` for the Suaveplan API platform — the heavy authentication integration, kept separate from the lightweight strategies in `@suaveplan/api-auth`.
`createBetterAuthSessionResolver` wraps a real Better-Auth instance backed by `@suaveplan/better-auth-surrealdb`, validates the session cookie/bearer via `auth.api.getSession({ headers })`, and maps the authenticated `(user, session)` view onto the frozen `@suaveplan/api-types` `Principal` contract.
Frameworks stay guests: the resolver mounts no routes and imports no router; it only reads the session Better-Auth already established.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createBetterAuthSessionResolver` | `src/session/create-session.ts` | Flagship `SessionResolver` factory |
| `mapBetterAuthSessionToPrincipal` | `src/principal/map-session-to-principal.ts` | Default `(user, session) → Principal` mapping with redaction |
| `createBetterAuthProvider` | `src/provider/create-provider.ts` | `AuthProviderAdapter` (resolver + logout + authorize/callback) |
| `AUTH_BA_CODES` / `BetterAuthAuthError` | `src/error-codes/error-codes.ts` | 4-code `AUTH_BA.*` error registry |
| `BetterAuthLike` / `PrincipalMapping` | `src/types/better-auth.ts` | Structural contracts and mapping type |
| `runBetterAuthSessionConformance` | `src/testing/run-session-conformance.ts` | Conformance harness for third-party adapters |
## File Structure
```
src/
index.ts # Barrel: resolver, mapping, provider, error codes, types
session/
create-session.ts # createBetterAuthSessionResolver
create-session.test.ts # Unit tests (injected getSession seam)
create-session.spec-conformance.test.ts
index.ts
principal/
map-session-to-principal.ts # Default mapping + redaction
map-session-to-principal.test.ts
index.ts
provider/
create-provider.ts # createBetterAuthProvider (AuthProviderAdapter)
create-provider.test.ts
index.ts
error-codes/
error-codes.ts # AUTH_BA_CODES + BetterAuthAuthError
error-codes.test.ts
error-codes.spec-conformance.test.ts
index.ts
types/
better-auth.ts # BetterAuthLike, session/user views, PrincipalMapping
index.ts
testing/
run-session-conformance.ts # Conformance harness
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-better-auth` | All (barrel) |
| `@suaveplan/api-auth-better-auth/session` | `createBetterAuthSessionResolver`, `BetterAuthSessionResolverOptions` |
| `@suaveplan/api-auth-better-auth/principal` | `mapBetterAuthSessionToPrincipal` |
| `@suaveplan/api-auth-better-auth/provider` | `createBetterAuthProvider`, `BetterAuthProviderOptions` |
| `@suaveplan/api-auth-better-auth/error-codes` | `AUTH_BA_CODES`, `BetterAuthAuthError`, `BetterAuthAuthErrorCode` |
| `@suaveplan/api-auth-better-auth/types` | `BetterAuthLike`, `BetterAuthSessionView`, `BetterAuthUserView`, `PrincipalMapping`, etc. |
| `@suaveplan/api-auth-better-auth/testing` | `runBetterAuthSessionConformance` |
## Patterns
### Basic session resolver
```ts
import { createApi } from "@suaveplan/api";
import { createBetterAuthSessionResolver } from "@suaveplan/api-auth-better-auth/session";
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
import { SurrealAdapter } from "@suaveplan/surrealdb";
import { betterAuth } from "better-auth";
const client = new SurrealAdapter({ url: "ws://localhost:8000", namespace: "auth", database: "auth", username: "root", password: process.env.SURREAL_PASSWORD ?? "" });
await client.connect();
await applyBetterAuthSchema(client);
const auth = betterAuth({
database: betterAuthSurrealAdapter({ client }),
secret: process.env.BETTER_AUTH_SECRET ?? "",
baseURL: "http://localhost:3000",
emailAndPassword: { enabled: true },
});
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createBetterAuthSessionResolver({ auth }),
});
```
### Auth provider adapter (social flow)
```ts
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth/provider";
const provider = createBetterAuthProvider({ auth, socialProvider: "github" });
const principal = await provider.resolver.resolve(request);
if (principal) await provider.logout(principal);
const authorizeUrl = provider.buildAuthorizeUrl(state, redirectUri);
```
### Custom principal mapping
```ts
import type { Principal, PrincipalId } from "@suaveplan/api-types/principal";
const resolver = createBetterAuthSessionResolver({
auth,
principalMapping: ({ user }): Principal => ({
id: `mapped:${String(user.id)}` as PrincipalId,
scopes: [],
claims: { custom: true },
}),
});
```
### Default mapping details
The default `mapBetterAuthSessionToPrincipal` derives:
- `id` ← `user.id` (must be non-empty string)
- `scopes` ← `user.roles` filtered to non-empty strings
- `tenantId` ← `session.activeOrganizationId` (fallback to `user.tenantId`)
- `claims` ← all own-enumerable user fields except sensitive redactions (`password`, `passwordHash`, `salt`, `twoFactorSecret`, `magicLinkToken`, `resetPasswordToken`), plus `session_id` and `session_token`
### Error handling
```ts
import { AUTH_BA_CODES, BetterAuthAuthError } from "@suaveplan/api-auth-better-auth/error-codes";
try {
await resolver.resolve(request);
} catch (err) {
if (err instanceof BetterAuthAuthError) {
// OPTIONS_INVALID: auth.api.getSession not callable
// INVALID_SESSION: getSession threw (unhealthy store)
// PRINCIPAL_MAPPING_FAILED: mapping threw or returned invalid principal
}
}
```
## Constraints
- **Peer dependency**: `better-auth` ^1.4.0 — type-only at this package's boundary; never imported at runtime.
- **Frozen resolver** — `createBetterAuthSessionResolver` returns `Object.freeze({ resolve })`.
- **Principal validation** — custom mapping output is validated against a Zod schema (`id: non-empty string`, `scopes: readonly string[]`, etc.); invalid output raises `AUTH_BA.PRINCIPAL_MAPPING_FAILED`.
- **Sensitive field redaction** — default mapping strips `password`, `passwordHash`, `salt`, `twoFactorSecret`, `magicLinkToken`, `resetPasswordToken` from claims.
- **No mock integration** — the integration suite (`tests/integration/better-auth-surreal-e2e.test.ts`) stands up real `betterAuth(...)` over a real SurrealDB store (testcontainers).
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/better-auth-surrealdb`, `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`. Peer: `better-auth`.
## Error Codes
| Code | Raised When |
|---|---|
| `AUTH_BA.OPTIONS_INVALID` | `auth.api.getSession` is not callable at construction |
| `AUTH_BA.INVALID_SESSION` | `getSession` threw (unhealthy instance/store, distinct from "no session") |
| `AUTH_BA.PRINCIPAL_MAPPING_FAILED` | Mapping threw, or returned structurally invalid Principal |
| `AUTH_BA.LOGOUT_FAILED` | Session revocation via `internalAdapter.deleteSession` threw |
## Checklist
- [ ] Read `src/session/create-session.ts` before modifying resolver behaviour
- [ ] Read `src/principal/map-session-to-principal.ts` before changing mapping or redaction
- [ ] Read `src/provider/create-provider.ts` before modifying provider adapter
- [ ] Read `src/error-codes/error-codes.ts` before adding/removing error codes
- [ ] Preserve the `better-auth` type-only peer dependency — never import it at runtime
- [ ] Sensitive field redaction list (`SENSITIVE_USER_FIELDS`) must be kept in sync with security policy
- [ ] The `./testing` conformance harness must remain runner-agnostic

View File

@@ -0,0 +1,92 @@
---
name: sp-api-auth-codes
description: Shared stable error-code registry for the @suaveplan API auth seam. Exports AUTH_CODES via defineCodes("AUTH", [...]), the AuthError class extending SuaveplanError, and the AuthErrorCode union. Factored out to break dependency cycles between api-auth-<strategy> packages.
---
# @suaveplan/api-auth-codes
## Purpose
Shared stable error-code registry for the `@suaveplan` API auth seam. This package owns the `AUTH.*` codes and the `AuthError` class so every `api-auth-<strategy>` package and the `@suaveplan/api-auth` chain combinator import them from one place.
Factoring the registry out of any single strategy package breaks the dependency cycle that would otherwise form between the strategies (e.g. `bearer` and `jwt` both needing the same codes), while keeping a single source of truth for the namespace.
## Architecture
| Component | Source | Role |
|---|---|---|
| `AUTH_CODES` | `src/error-codes/error-codes.ts` | `defineCodes("AUTH", ["OPTIONS_INVALID", "CHAIN_EMPTY", "VERIFIER_FAILED"])` |
| `AuthError` | `src/error-codes/error-codes.ts` | Concrete error class extending `SuaveplanError<AuthErrorCode>` |
| `AuthErrorCode` | `src/error-codes/error-codes.ts` | Union type of all `AUTH.*` code strings |
## File Structure
```
src/
index.ts # Exports AUTH_CODES, AuthError, AuthErrorCode
error-codes/
error-codes.ts # Registry + error class implementation
error-codes.test.ts # Unit tests
index.ts # Subpath barrel
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-codes` | `AUTH_CODES`, `AuthError`, `AuthErrorCode` |
| `@suaveplan/api-auth-codes/error-codes` | Same from feature subpath |
## Patterns
### Branch on error code
```ts
import { AUTH_CODES, AuthError } from "@suaveplan/api-auth-codes";
try {
// ... construct a strategy with bad options
} catch (error) {
if (error instanceof AuthError && error.code === AUTH_CODES.OPTIONS_INVALID) {
console.error("auth strategy misconfigured", error.context);
}
}
```
### Codes are only for operator-fixable failures
Per-request authentication misses (missing header, unknown token, bad signature, expired token) are **NOT** errors — the strategies return `null` so the pipeline serves the request as unauthenticated.
| Code | Raised When |
|---|---|
| `AUTH.OPTIONS_INVALID` | A strategy factory is constructed with options that fail Zod boundary validation, or a JWT verification source is mis-declared |
| `AUTH.CHAIN_EMPTY` | `createResolverChain()` is called with zero resolvers |
| `AUTH.VERIFIER_FAILED` | A verified JWT payload is not a JSON object, an injected verifier raises a hard failure, or a chained resolver throws (fail-closed) |
### Extending the registry
```ts
import { defineCodes } from "@suaveplan/error";
// In a new auth package (e.g. api-auth-oauth):
export const AUTH_OAUTH_CODES = defineCodes("AUTH_OAUTH", [
"PROVIDER_NOT_CONFIGURED",
"CALLBACK_INVALID",
] as const);
```
## Constraints
- **Zero strategy code** — this package contains only error codes and the error class; no authentication logic.
- **Shared by all auth packages** — `@suaveplan/api-auth`, `@suaveplan/api-auth-bearer`, `@suaveplan/api-auth-jwt`, and any future `api-auth-*` strategy packages.
- **Uses `defineCodes`** — codes are declared with `@suaveplan/error`'s `defineCodes("AUTH", [...])`; never hand-write code string literals.
- **Soft failures are not errors** — per-request auth misses return `null` from resolvers, never raise `AuthError`.
- **Dependencies**: `@suaveplan/error`, `tslib`.
## Checklist
- [ ] Read `src/error-codes/error-codes.ts` before modifying the registry
- [ ] Read `src/error-codes/error-codes.test.ts` before changing test assertions
- [ ] New codes must be operator-fixable; per-request soft failures belong as `null` returns in resolvers
- [ ] Every new code must be used by at least one call site in a consuming package
- [ ] Never add strategy logic to this package — keep it purely the code registry

View File

@@ -0,0 +1,143 @@
---
name: sp-api-auth-jwt
description: JWT session-resolver strategy for the Fetch-native API auth seam. createJwtResolver(options) reads a bearer JWT from Authorization, verifies with real @suaveplan/jws compactVerify HMAC (HS256/384/512) or an injected verify seam, enforces iss/aud allowlists and exp/nbf with clock-skew tolerance, and maps claims to Principal via a pluggable mapper.
---
# @suaveplan/api-auth-jwt
## Purpose
JWT session-resolver strategy for the Suaveplan API platform. `createJwtResolver(options)` returns a frozen `SessionResolver` that reads a bearer JWT from the `Authorization` header, verifies it with a real verifier, enforces standard temporal and identity claims, and projects verified claims onto a `Principal`.
Two verification sources are supported, exactly one configured:
- `hmacSecret` — built-in real HMAC verification via `@suaveplan/jws` `compactVerify` (HS256/384/512). Genuine signature checking, no mock.
- `verify` — an injected `(token) => Promise<Claims>` seam for JWKS-backed or asymmetric verifiers.
After cryptographic verification it enforces `iss`/`aud` allowlists and `exp`/`nbf` with configurable `clockToleranceSec` skew and an injectable `Clock`, then runs a pluggable `claimsToPrincipal` mapper.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createJwtResolver` | `src/jwt/jwt.ts` | Factory returning a frozen JWT `SessionResolver` |
| `defaultClaimsToPrincipal` | `src/jwt/claims-mapper.ts` | Default `Claims → Principal` projection (`sub`→id, `scope`/`scopes`→scopes, `tenant_id`→tenantId) |
| `JwtResolverOptions` | `src/jwt/jwt.types.ts` | Options: `hmacSecret`/`verify`, `algorithms`, `issuer`, `audience`, `clockToleranceSec`, `clock`, `claimsToPrincipal`, `headerName` |
| `resolveVerifier` | `src/jwt/jwt.ts` | Enforces exactly-one verification source; builds HMAC verifier or returns injected seam |
| `extractToken` | `src/jwt/jwt.ts` | Reads `Bearer <token>` from configured header |
| `temporalOk` | `src/jwt/jwt.ts` | Enforces `exp`/`nbf` with skew tolerance |
## File Structure
```
src/
index.ts # Exports createJwtResolver, defaultClaimsToPrincipal, types
jwt/
jwt.ts # Main factory + verification pipeline
jwt.test.ts # Unit tests (real HS256 sign/verify, injected seam)
claims-mapper.ts # defaultClaimsToPrincipal + scope extraction
jwt.types.ts # JwtResolverOptions, JwtVerify, ClaimsToPrincipal
index.ts # Subpath barrel
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-jwt` | `createJwtResolver`, `defaultClaimsToPrincipal`, `JwtResolverOptions`, `JwtVerify`, `ClaimsToPrincipal` |
| `@suaveplan/api-auth-jwt/jwt` | Same from feature subpath |
## Patterns
### Built-in HMAC verification
```ts
import { createApi } from "@suaveplan/api";
import { createJwtResolver } from "@suaveplan/api-auth-jwt";
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createJwtResolver({
hmacSecret: process.env.JWT_SECRET!,
algorithms: ["HS256", "HS384"],
issuer: "my-app",
audience: ["api", "web"],
clockToleranceSec: 5,
}),
});
```
### Injected verifier seam (JWKS / asymmetric)
```ts
const resolver = createJwtResolver({
verify: async (token) => {
// Your JWKS or asymmetric verification
return await jwksVerify(token);
},
claimsToPrincipal: (claims) => ({
id: String(claims.uid) as PrincipalId,
scopes: [],
claims,
}),
});
```
### Custom claims mapper
```ts
const resolver = createJwtResolver({
hmacSecret: SECRET,
claimsToPrincipal: (claims) => ({
id: String(claims.uid) as PrincipalId,
scopes: [],
claims,
}),
});
```
### Soft failures return null
```ts
// All of these return null (unauthenticated), never throw:
await resolver.resolve(new Request("https://api.test")); // no header
await resolver.resolve(bearer("tampered-token")); // bad signature
await resolver.resolve(bearer(await sign({ sub: "u", exp: past }))); // expired
await resolver.resolve(bearer(await sign({ scope: "a" }))); // no sub claim
```
### Hard failures throw AUTH.VERIFIER_FAILED
```ts
// Injected verifier throwing AuthError → re-raised as hard failure
const resolver = createJwtResolver({
verify: async () => {
throw new AuthError(AUTH_CODES.VERIFIER_FAILED, "kms down");
},
});
// → throws AuthError with code AUTH_CODES.VERIFIER_FAILED
```
## Constraints
- **Exactly one verification source** — both `hmacSecret` and `verify` or neither raises `AUTH.OPTIONS_INVALID` at construction.
- **Empty hmacSecret rejected** — empty string or zero-length `Uint8Array` raises `AUTH.OPTIONS_INVALID`.
- **Real HMAC verification** — `compactVerify` from `@suaveplan/jws` performs genuine signature checking.
- **Soft vs hard failures** — cryptographic rejection (bad sig, expired) → `null`; injected `AuthError` → re-thrown as `AUTH.VERIFIER_FAILED`.
- **Case-insensitive Bearer scheme** — header name is case-insensitive; "Bearer" prefix is case-insensitive.
- **Clock injectable** — default is `Date.now`; override for deterministic tests.
- **Dependencies**: `@suaveplan/api-auth-codes`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/jws`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Raised When |
|---|---|
| `AUTH.OPTIONS_INVALID` | Zero or two verification sources configured; empty `hmacSecret` |
| `AUTH.VERIFIER_FAILED` | Verified JWT payload is not a JSON object; injected verifier threw `AuthError` |
## Checklist
- [ ] Read `src/jwt/jwt.ts` before modifying verification pipeline
- [ ] Read `src/jwt/jwt.test.ts` before changing test assertions
- [ ] Preserve the exactly-one-verification-source guard
- [ ] Preserve soft-failure semantics: cryptographic rejections return `null`
- [ ] The injected `verify` seam must not be called at construction time — only at `resolve()`

View File

@@ -0,0 +1,115 @@
---
name: sp-api-auth-static
description: Static (fixed-principal) SessionResolver strategy for the Fetch-native API auth seam. createStaticResolver(options) returns a frozen SessionResolver that resolves the same validated, deeply frozen Principal for every request. Zod-validated at construction with AUTH.OPTIONS_INVALID on invalid input. Intended for tests, local development, and trusted service-to-service deployments.
---
# @suaveplan/api-auth-static
## Purpose
Static (fixed-principal) session-resolver strategy for the Suaveplan API platform. `createStaticResolver(options)` returns a frozen `SessionResolver` that resolves the same `Principal` for every request, ignoring request material.
Intended for tests, local development, and trusted single-tenant service-to-service deployments where the caller identity is known at process start and need not be derived from the request.
The supplied principal is Zod-validated at construction and re-frozen into a normalised, immutable value, so later mutation of the caller's object cannot leak into resolved sessions.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createStaticResolver` | `src/static/static.ts` | Factory returning a frozen static-identity `SessionResolver` |
| `StaticResolverOptions` | `src/static/static.ts` | Options interface (`principal: Principal`) |
| `principalSchema` | `src/static/static.ts` | Zod boundary: `id` non-empty, `scopes` string array, optional `tenantId` |
| `freezePrincipal` | `src/static/static.ts` | Re-assembles validated data into a deeply frozen `Principal` |
## File Structure
```
src/
index.ts # Exports createStaticResolver + StaticResolverOptions
static/
static.ts # Factory + Zod validation + deep freeze
static.test.ts # Unit tests
index.ts # Subpath barrel
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth-static` | `createStaticResolver`, `StaticResolverOptions` |
| `@suaveplan/api-auth-static/static` | Same from feature subpath |
## Patterns
### Fixed service identity
```ts
import { createApi } from "@suaveplan/api";
import { createStaticResolver } from "@suaveplan/api-auth-static";
import type { PrincipalId, Scope } from "@suaveplan/api-types/principal";
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createStaticResolver({
principal: {
id: "svc-worker" as PrincipalId,
scopes: ["jobs:run"] as readonly Scope[],
claims: { service: true },
},
}),
});
```
### Test fixture with admin principal
```ts
const ADMIN = {
id: "u_admin" as PrincipalId,
scopes: ["admin:*"] as readonly Scope[],
claims: { role: "admin" },
};
const resolver = createStaticResolver({ principal: ADMIN });
const p = await resolver.resolve(new Request("https://api.test"));
expect(p?.id).toBe("u_admin");
```
### Immutability guarantee
```ts
const mutable = {
id: "u_2" as PrincipalId,
scopes: ["read"] as Scope[],
claims: { v: 1 } as Record<string, unknown>,
};
const resolver = createStaticResolver({ principal: mutable });
mutable.scopes.push("write" as Scope);
mutable.claims["v"] = 2;
const p = await resolver.resolve(new Request("https://api.test"));
expect(p?.scopes).toEqual(["read"]); // original value preserved
expect(p?.claims).toEqual({ v: 1 }); // original value preserved
expect(Object.isFrozen(p)).toBe(true); // deeply frozen
```
## Constraints
- **Zod-validated at construction** — invalid principal shape raises `AUTH.OPTIONS_INVALID`.
- **Empty `id` rejected** — a principal must be identifiable before it reaches `ctx.principal`.
- **Deeply frozen output** — returned principal is `Object.freeze`-d so mutations cannot leak.
- **Ignores request material** — `resolve()` takes a `Request` but does not read from it.
- **Dependencies**: `@suaveplan/api-auth-codes`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`.
## Error Codes
| Code | Raised When |
|---|---|
| `AUTH.OPTIONS_INVALID` | Principal fails Zod boundary validation (empty `id`, invalid `scopes`, etc.) |
## Checklist
- [ ] Read `src/static/static.ts` before modifying validation or freezing logic
- [ ] Read `src/static/static.test.ts` before changing test assertions
- [ ] Preserve the immutability guarantee — the resolved principal must be deeply frozen
- [ ] Empty `id` must remain rejected at construction time

View File

@@ -0,0 +1,123 @@
---
name: sp-api-auth
description: Authentication-strategy meta-package for the Fetch-native API platform. Re-exports anonymous, static, bearer, and JWT strategies from standalone sibling packages, and owns the chain combinator (first-non-null-wins composition, fail-closed on throw).
---
# @suaveplan/api-auth
## Purpose
Authentication-strategy meta-package for the Suaveplan API platform. Each strategy is a factory returning a frozen `SessionResolver` (`Request -> Principal | null`) that plugs into `createApi({ sessionResolver })` to populate `ctx.principal` before any route handler executes.
Following the `api-<seam>-<plugin>` extension convention, the concrete strategies live in standalone sibling packages. `@suaveplan/api-auth` owns the `chain` combinator and re-exports every strategy on identical subpaths for back-compat.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createResolverChain` | `src/chain/chain.ts` | First-non-null-wins composition of `SessionResolver`s; fail-closed on throw |
| `createAnonymousResolver` | `src/anonymous/index.ts` | Re-export from `@suaveplan/api-auth-anonymous` |
| `createStaticResolver` | `src/static/index.ts` | Re-export from `@suaveplan/api-auth-static` |
| `createBearerResolver` | `src/bearer/index.ts` | Re-export from `@suaveplan/api-auth-bearer` |
| `createJwtResolver` | `src/jwt/index.ts` | Re-export from `@suaveplan/api-auth-jwt` |
| `AUTH_CODES` / `AuthError` | `src/error-codes/index.ts` | Re-export from `@suaveplan/api-auth-codes` |
## File Structure
```
src/
index.ts # Re-exports all strategies + chain + error codes
chain/
chain.ts # createResolverChain implementation
chain.test.ts # Unit tests for chain composition
index.ts # Subpath barrel
anonymous/index.ts # Re-export barrel → @suaveplan/api-auth-anonymous
bearer/index.ts # Re-export barrel → @suaveplan/api-auth-bearer
jwt/index.ts # Re-export barrel → @suaveplan/api-auth-jwt
static/index.ts # Re-export barrel → @suaveplan/api-auth-static
error-codes/index.ts # Re-export barrel → @suaveplan/api-auth-codes
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-auth` | All strategies + chain + AUTH_CODES + AuthError |
| `@suaveplan/api-auth/chain` | `createResolverChain` |
| `@suaveplan/api-auth/anonymous` | `createAnonymousResolver` |
| `@suaveplan/api-auth/static` | `createStaticResolver` |
| `@suaveplan/api-auth/bearer` | `createBearerResolver` |
| `@suaveplan/api-auth/jwt` | `createJwtResolver`, `defaultClaimsToPrincipal` |
| `@suaveplan/api-auth/error-codes` | `AUTH_CODES`, `AuthError` |
## Patterns
### Chain composition (JWT → Bearer → Anonymous)
```ts
import { createApi } from "@suaveplan/api";
import { createJwtResolver } from "@suaveplan/api-auth/jwt";
import { createBearerResolver } from "@suaveplan/api-auth/bearer";
import { createAnonymousResolver } from "@suaveplan/api-auth/anonymous";
import { createResolverChain } from "@suaveplan/api-auth/chain";
const runtime = createApi({
plugins: [/* ... */],
sessionResolver: createResolverChain(
createJwtResolver({ hmacSecret: process.env.JWT_SECRET! }),
createBearerResolver({
tokens: [{ token: "sk_live_abc123", principal: { id: "u_alice", scopes: ["billing:read"], claims: {} } }],
}),
createAnonymousResolver(),
),
});
```
### Fail-closed on resolver throw
If any resolver in the chain throws, the chain re-raises `AUTH.VERIFIER_FAILED` wrapping the cause:
```ts
import { AUTH_CODES, AuthError } from "@suaveplan/api-auth/error-codes";
// This will throw AuthError with code AUTH_CODES.VERIFIER_FAILED
try {
await chain.resolve(request);
} catch (err) {
if (err instanceof AuthError && err.code === AUTH_CODES.VERIFIER_FAILED) {
// Infrastructure failure — not an unauthenticated request
}
}
```
### Empty chain guard
```ts
import { createResolverChain } from "@suaveplan/api-auth/chain";
// Throws AuthError AUTH_CODES.CHAIN_EMPTY at construction
createResolverChain(); // ❌ at least one resolver required
```
## Constraints
- **Zero runtime strategy code** — all strategies are re-exports from standalone sibling packages; this package only owns `createResolverChain`.
- **Fail-closed** — resolver throws are never swallowed; they surface as `AUTH.VERIFIER_FAILED`.
- **Short-circuit** — the first non-null principal wins; later resolvers are not invoked.
- **Empty chain** — construction with zero resolvers raises `AUTH_CODES.CHAIN_EMPTY`.
- **Dependencies**: `@suaveplan/api-auth-{anonymous,bearer,codes,jwt,static}`, `@suaveplan/api-types`, `tslib`.
## Error Codes
| Code | Raised When |
|---|---|
| `AUTH.CHAIN_EMPTY` | `createResolverChain()` called with zero resolvers |
| `AUTH.VERIFIER_FAILED` | A resolver in the chain threw (fail-closed) |
| `AUTH.OPTIONS_INVALID` | Raised by sibling strategy packages (e.g. empty bearer table) |
## Checklist
- [ ] Read the chain implementation in `src/chain/chain.ts` before extending composition behaviour
- [ ] Read `src/chain/chain.test.ts` before modifying chain semantics
- [ ] If adding a new strategy subpath, create the standalone `api-auth-<strategy>` package first, then add a re-export barrel here
- [ ] Keep the `api-<seam>-<plugin>` convention — strategies are plugins for the auth seam

View File

@@ -0,0 +1,81 @@
---
name: api-stack-api-capabilities-navigator
description: Navigator for api-stack/api-capabilities. Pick a library to get detailed API guidance.
---
# Api capabilities (api-stack)
Packages in `api-stack/api-capabilities`.
## Libraries
### @suaveplan/api-capability-database-memory
Faithful in-memory AdminDatabaseLike reference implementation (MemoryDatabase) over a real minimal SQL engine (SqlStore: CREATE/INSERT/SELECT/UPDATE/DELETE, positional + named params, prepared stateme...
→ Skill: `sp-api-capability-database-memory/SKILL.md`
### @suaveplan/api-capability-events-bus-memory
Rich in-memory ExtendedEventBusLike reference implementation (createExtendedEventsBus) for the events.extended capability tier of the Suaveplan API capability system. Delegates pub/sub and its bounded...
→ Skill: `sp-api-capability-events-bus-memory/SKILL.md`
### @suaveplan/api-capability-events-kit
Tier-2 glue for the events.extended capability of the Suaveplan API capability system: the stable EVENTS_CAP.* error-code registry + EventsCapError, two strict Zod boundary schemas (eventEnvelopeSchem...
→ Skill: `sp-api-capability-events-kit/SKILL.md`
### @suaveplan/api-capability-events-memory
Faithful in-memory CapabilityEventBusLike reference implementation (MemoryEvents) for the Suaveplan API capability system: real per-topic publish/subscribe with idempotent unsubscribe and awaited deli...
→ Skill: `sp-api-capability-events-memory/SKILL.md`
### @suaveplan/api-capability-jobs-memory
Faithful in-memory JobRegistrationHandle reference implementation (MemoryJobs) plus a trigger-on-demand runner for the Suaveplan API capability system: registerCron/registerQueued record handlers by n...
→ Skill: `sp-api-capability-jobs-memory/SKILL.md`
### @suaveplan/api-capability-kit
Shared foundation for the @suaveplan/api-capability-* family: the stable CAP.* error-code registry + CapabilityError (thrown by the memory impls and the generic proxy alike) and the five runner-agnost...
→ Skill: `sp-api-capability-kit/SKILL.md`
### @suaveplan/api-capability-logger-kit
Contract glue for the logger.structured capability tier: the stable LOGGER_CAP.* error-code registry + LoggerCapError, the universal-tier level helpers (LOG_LEVELS/compareLevels/isLogRecord), the Zod ...
→ Skill: `sp-api-capability-logger-kit/SKILL.md`
### @suaveplan/api-capability-logger-memory
Faithful in-memory CapabilityLoggerLike reference implementation (MemoryLogger) for the Suaveplan API capability system: records every debug/info/warn/error call as a structured line on a shared sink,...
→ Skill: `sp-api-capability-logger-memory/SKILL.md`
### @suaveplan/api-capability-logger-structured-memory
Faithful in-memory StructuredLoggerLike reference implementation (createReferenceLogger) for the logger.structured capability tier: six severity rungs, pluggable sinks with per-sink min-level, redacto...
→ Skill: `sp-api-capability-logger-structured-memory/SKILL.md`
### @suaveplan/api-capability-proxy
The ONE generic remote-capability proxy for the Suaveplan API platform — replaces the five per-capability *-worker-proxy packages of the old design (redesign Keystone 2). createRemoteCapabilityProxy +...
→ Skill: `sp-api-capability-proxy/SKILL.md`
### @suaveplan/api-capability-secrets-memory
Faithful in-memory SecretsLike reference implementation (MemorySecrets) for the Suaveplan API capability system: a seeded name -> value store that resolves via get, throws CAP.SECRET_NOT_FOUND from re...
→ Skill: `sp-api-capability-secrets-memory/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,103 @@
---
name: sp-api-capability-database-memory
description: Faithful in-memory AdminDatabaseLike reference implementation (MemoryDatabase) over a real minimal SQL engine (SqlStore: CREATE/INSERT/SELECT/UPDATE/DELETE, positional + named params, prepared statements, snapshot-rollback transactions) plus admin→tenant-scoped wrapper. The fixture that runDatabaseConformance measures every database engine adapter against.
---
# @suaveplan/api-capability-database-memory
## Purpose
Faithful in-memory `AdminDatabaseLike` reference implementation for the Suaveplan API capability system. Provides `MemoryDatabase` — a real minimal SQL engine (`SqlStore`) supporting CREATE, INSERT, SELECT, UPDATE, DELETE with positional and named parameters, prepared statements, and snapshot-rollback transactions — plus `createTenantScoped` / `scopeSql` for wrapping an admin database as a tenant-scoped one.
This is the fixture that `@suaveplan/api-capability-kit`'s `runDatabaseConformance` harness measures every database engine adapter against. Its conformance test also runs against both the direct implementation AND the same implementation behind `@suaveplan/api-capability-proxy`, proving the proxy stays transparent.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemoryDatabase` | `src/memory-database/memory-database.ts` | In-memory `AdminDatabaseLike` implementation over `SqlStore` |
| `SqlStore` | `src/sql-store/sql-store.ts` | Minimal SQL engine (CREATE/INSERT/SELECT/UPDATE/DELETE, params, prepared statements, transactions) |
| `createTenantScoped` | `src/tenant-scoping/tenant-scoping.ts` | Wraps `AdminDatabaseLike` as `TenantScopedDatabaseLike` |
| `scopeSql` | `src/tenant-scoping/tenant-scoping.ts` | SQL scoping helper for tenant filtering |
## File Structure
```
src/
index.ts # Exports MemoryDatabase, SqlStore, createTenantScoped, scopeSql
memory-database/
memory-database.ts # AdminDatabaseLike implementation
memory-database.md # Module docs
memory-database.test.ts # Conformance + direct tests
sql-store/
sql-store.ts # Minimal SQL engine
sql-store.md # Module docs
sql-store.test.ts # Engine unit tests
tenant-scoping/
tenant-scoping.ts # Tenant-scoped wrapper
tenant-scoping.md # Module docs
tenant-scoping.test.ts # Wrapper tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-database-memory` | `MemoryDatabase`, `SqlStore`, `createTenantScoped`, `scopeSql`, `Bindings`, `Row` |
| `@suaveplan/api-capability-database-memory/memory-database/memory-database` | `MemoryDatabase` |
| `@suaveplan/api-capability-database-memory/sql-store/sql-store` | `SqlStore`, `Bindings`, `Row` |
| `@suaveplan/api-capability-database-memory/tenant-scoping/tenant-scoping` | `createTenantScoped`, `scopeSql` |
## Patterns
### Basic SQL operations
```ts
import { MemoryDatabase } from "@suaveplan/api-capability-database-memory";
const db = new MemoryDatabase();
await db.execute("CREATE TABLE users (id, name)");
await db.execute("INSERT INTO users VALUES (?, ?)", [1, "Alice"]);
const rows = await db.query("SELECT * FROM users WHERE id = ?", [1]);
```
### Named parameters
```ts
await db.execute("INSERT INTO users VALUES (:id, :name)", { id: 2, name: "Bob" });
```
### Transactions with rollback
```ts
const tx = await db.beginTransaction();
try {
await tx.execute("INSERT INTO users VALUES (?, ?)", [3, "Carol"]);
await tx.commit();
} catch {
await tx.rollback();
}
```
### Tenant scoping
```ts
import { createTenantScoped } from "@suaveplan/api-capability-database-memory";
const scoped = createTenantScoped(db, "tenant_abc");
// All reads/writes are implicitly filtered to tenant_abc
```
## Constraints
- **Reference implementation** — this is the canonical in-memory database; all other database adapters are measured against it via `runDatabaseConformance`.
- **Real SQL engine** — `SqlStore` is not a mock; it parses and executes SQL deterministically in-memory.
- **Proxy transparency** — conformance tests prove the implementation works identically behind `@suaveplan/api-capability-proxy`.
- **Dependencies**: `@suaveplan/api-capability-kit`, `@suaveplan/api-types`, `tslib`. Dev: `@suaveplan/api-capability-proxy`.
## Checklist
- [ ] Read `src/memory-database/memory-database.ts` before changing the AdminDatabaseLike implementation
- [ ] Read `src/sql-store/sql-store.ts` before modifying the SQL engine
- [ ] Read `src/tenant-scoping/tenant-scoping.ts` before changing tenant scoping logic
- [ ] Preserve proxy transparency — changes must pass both direct and proxied conformance tests

View File

@@ -0,0 +1,119 @@
---
name: sp-api-capability-events-bus-memory
description: Rich in-memory ExtendedEventBusLike reference implementation for the events.extended capability tier. Delegates pub/sub to @suaveplan/events (EventBus + BoundedQueue) and layers interceptor chain, onError channel, idempotency-key dedup, bounded-queue config, and per-topic publish-history recording. Raises EVENTS_CAP.* codes at the boundary. Passes runEventsBusConformance.
---
# @suaveplan/api-capability-events-bus-memory
## Purpose
Rich in-memory `ExtendedEventBusLike` reference implementation for the `events.extended` capability tier of the Suaveplan API capability system.
`createExtendedEventsBus` delegates pub/sub and its bounded queue to foundation `@suaveplan/events` (`EventBus` + `BoundedQueue`) so it inherits production delivery and backpressure semantics, and layers an interceptor chain, an `onError` channel, idempotency-key dedup, bounded-queue configuration, and a per-topic publish-history recording surface (`published` / `topics`) on top.
Passes the `@suaveplan/api-capability-events-kit` `runEventsBusConformance` harness against itself.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createExtendedEventsBus` | `src/bus/bus.ts` | Factory returning an `ExtendedEventBusLike` |
| `ExtendedEventsBus` | `src/bus/bus.ts` | Interface: subscribe, publish, publishAsync, published, topics, onError, dispose |
| `History` | `src/history/history.ts` | Append-only per-topic recording layer backing `published(topic)` |
| `extendedEventsBusOptionsSchema` | `src/options/options.ts` | Zod boundary for bus options |
| `EVENTS_BUS_MEMORY_CODES` | `src/error-codes/error-codes.ts` | Package-private error-code registry |
## File Structure
```
src/
index.ts # Exports bus, history, options, error codes
bus/
bus.ts # createExtendedEventsBus implementation
bus.md # Module docs
bus.test.ts # Unit + conformance tests
index.ts
history/
history.ts # History class for per-topic recording
history.md
history.test.ts
index.ts
options/
options.ts # Options + Zod schema
options.md
options.test.ts
index.ts
error-codes/
error-codes.ts # Package-private error codes
error-codes.test.ts
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-events-bus-memory` | `createExtendedEventsBus`, `ExtendedEventsBus`, `History`, options, error codes |
| `@suaveplan/api-capability-events-bus-memory/bus/bus` | `createExtendedEventsBus`, `ExtendedEventsBus` |
| `@suaveplan/api-capability-events-bus-memory/history/history` | `History` |
| `@suaveplan/api-capability-events-bus-memory/options/options` | `ExtendedEventsBusOptions`, `extendedEventsBusOptionsSchema`, etc. |
| `@suaveplan/api-capability-events-bus-memory/error-codes/error-codes` | `EVENTS_BUS_MEMORY_CODES`, `EventsBusMemoryError` |
## Patterns
### Basic pub/sub with history
```ts
import { createExtendedEventsBus } from "@suaveplan/api-capability-events-bus-memory";
const bus = createExtendedEventsBus();
const sub = bus.subscribe("user.created", (payload) => console.log(payload));
await bus.publish("user.created", { id: "u1" });
console.log(bus.published("user.created")); // [{ id: "u1" }]
sub.dispose();
bus.dispose();
```
### With bounded queue and backpressure
```ts
const bus = createExtendedEventsBus({
queue: { capacity: 100, policy: "block" },
});
```
### Interceptor chain
```ts
const bus = createExtendedEventsBus({
interceptors: [
async (envelope, next) => {
console.log("before", envelope.type);
await next();
console.log("after", envelope.type);
},
],
});
```
### Error channel
```ts
bus.onError((error) => {
console.error("Bus error:", error);
});
```
## Constraints
- **Delegates to foundation** — pub/sub and bounded queue use `@suaveplan/events`; never reimplement delivery semantics.
- **Kit → variant dependency only** — this package depends on `@suaveplan/api-capability-events-kit` for conformance; the kit imports no implementation.
- **Tier-2 invariants** — passes: backpressure block/throw, interceptor order/short-circuit/throw, idempotency-key dedup.
- **Dependencies**: `@suaveplan/api-capability-events-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/events`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/bus/bus.ts` before modifying bus behaviour
- [ ] Read `src/history/history.ts` before changing recording semantics
- [ ] Preserve delegation to `@suaveplan/events` — do not reimplement pub/sub delivery
- [ ] Ensure `runEventsBusConformance` continues to pass after changes

View File

@@ -0,0 +1,112 @@
---
name: sp-api-capability-events-kit
description: Tier-2 glue for the events.extended capability: stable EVENTS_CAP.* error-code registry + EventsCapError, two Zod boundary schemas (eventEnvelopeSchema + boundedQueueOptsSchema), and the runner-agnostic runEventsBusConformance harness over ExtendedEventBusLike. Imports NO implementation package.
---
# @suaveplan/api-capability-events-kit
## Purpose
Tier-2 glue for the `events.extended` capability of the Suaveplan API capability system. Ships three things every `ExtendedEventBusLike` implementation depends on:
1. **Stable `EVENTS_CAP.*` error-code registry + `EventsCapError`** — thrown by every conforming bus.
2. **Two strict Zod boundary schemas** — `eventEnvelopeSchema` + `boundedQueueOptsSchema` (adopting the foundation `@suaveplan/events` 4-value `policy` field).
3. **Runner-agnostic `runEventsBusConformance` harness** — parameterized over a caller-supplied factory and importing NO implementation package, so no `kit → variant` dependency cycle can form.
The minimal `events` capability (`CapabilityEventBusLike` = `emit`/`on`) and its tier-1 conformance harness (`runEventsConformance`) live in `@suaveplan/api-capability-kit` and stay unchanged. This package adds the richer `events.extended` → `ExtendedEventBusLike` surface.
## Architecture
| Component | Source | Role |
|---|---|---|
| `EVENTS_CAP_CODES` | `src/error-codes/error-codes.ts` | Stable `EVENTS_CAP.*` registry via `defineCodes` |
| `EventsCapError` | `src/error-codes/error-codes.ts` | Concrete error class extending `SuaveplanError` |
| `eventEnvelopeSchema` | `src/schemas/schemas.ts` | Zod schema for event envelope validation |
| `boundedQueueOptsSchema` | `src/schemas/schemas.ts` | Zod schema for bounded queue options |
| `runEventsBusConformance` | `src/testing/conformance.ts` | Runner-agnostic conformance harness |
| `invariants.ts` / `invariants-late.ts` | `src/testing/` | First/second halves of the invariant suite |
## File Structure
```
src/
index.ts # Barrel: error codes, schemas, conformance harness, types
error-codes/
error-codes.ts # EVENTS_CAP_CODES + EventsCapError
error-codes.test.ts
index.ts
schemas/
schemas.ts # eventEnvelopeSchema + boundedQueueOptsSchema
schemas.test.ts
index.ts
testing/
conformance.ts # runEventsBusConformance harness
invariants.ts # Ordering + duplicate-registration invariants
invariants-late.ts # Unsubscribe-during-publish + late-subscriber invariants
conformance.test.ts
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-events-kit` | `EVENTS_CAP_CODES`, `EventsCapError`, `eventEnvelopeSchema`, `boundedQueueOptsSchema`, `runEventsBusConformance`, all types |
| `@suaveplan/api-capability-events-kit/error-codes/error-codes` | `EVENTS_CAP_CODES`, `EventsCapError`, `EventsCapCode` |
| `@suaveplan/api-capability-events-kit/schemas/schemas` | `eventEnvelopeSchema`, `boundedQueueOptsSchema` |
| `@suaveplan/api-capability-events-kit/testing/conformance` | `runEventsBusConformance` |
## Patterns
### Running conformance against an implementation
```ts
import { runEventsBusConformance } from "@suaveplan/api-capability-events-kit";
import { createExtendedEventsBus } from "@suaveplan/api-capability-events-bus-memory";
runEventsBusConformance("createExtendedEventsBus", () => createExtendedEventsBus());
```
### Validating event envelopes
```ts
import { eventEnvelopeSchema, boundedQueueOptsSchema } from "@suaveplan/api-capability-events-kit";
const envelope = eventEnvelopeSchema.parse({ type: "user.created", payload: { id: "u1" } });
const queueOpts = boundedQueueOptsSchema.parse({ capacity: 2, policy: "throw" });
```
### Throwing typed errors
```ts
import { EVENTS_CAP_CODES, EventsCapError } from "@suaveplan/api-capability-events-kit";
throw new EventsCapError(
EVENTS_CAP_CODES.BACKPRESSURE_QUEUE_FULL,
"error.events_cap.backpressure_queue_full"
);
```
## Constraints
- **Imports NO implementation package** — the kit depends only on `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `tslib`, `zod`. This breaks the `kit → variant` cycle.
- **Tier-2 invariants** — the harness covers six invariants the minimal `emit`/`on` base cannot express: backpressure block/throw, interceptor order/short-circuit/throw, and idempotency-key dedup.
- **Explicit named re-exports** — no `export *` per CLAUDE.md rule 10.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `tslib`, `zod`.
## Error Codes
| Code | Meaning |
|---|---|
| `EVENTS_CAP.BACKPRESSURE_QUEUE_FULL` | Bounded queue at capacity |
| `EVENTS_CAP.INTERCEPTOR_REJECTED` | Interceptor short-circuited or threw |
| `EVENTS_CAP.IDEMPOTENCY_DUPLICATE` | Duplicate idempotency key detected |
(Exact codes defined in `src/error-codes/error-codes.ts`; consult source for full registry.)
## Checklist
- [ ] Read `src/testing/conformance.ts` before modifying harness behaviour
- [ ] Read `src/testing/invariants.ts` and `src/testing/invariants-late.ts` before changing invariant definitions
- [ ] Never import an implementation package — keep the kit cycle-free
- [ ] New invariants must be added to both the harness and at least one reference implementation's test

View File

@@ -0,0 +1,70 @@
---
name: sp-api-capability-events-memory
description: Faithful in-memory CapabilityEventBusLike reference implementation (MemoryEvents) for the Suaveplan API capability system. Real per-topic pub/sub with idempotent unsubscribe and awaited delivery. The fixture that runEventsConformance measures every event-bus implementation against.
---
# @suaveplan/api-capability-events-memory
## Purpose
Faithful in-memory `CapabilityEventBusLike` reference implementation (`MemoryEvents`) for the Suaveplan API capability system. Provides real per-topic publish/subscribe with idempotent unsubscribe and awaited delivery (callers can `await emit`).
This is the fixture that `@suaveplan/api-capability-kit`'s `runEventsConformance` harness measures every event-bus implementation against. Its conformance test also runs against both the direct implementation AND the same implementation behind `@suaveplan/api-capability-proxy`, proving the proxy stays transparent.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemoryEvents` | `src/memory-events/memory-events.ts` | In-memory `CapabilityEventBusLike` (emit/on) |
| `EventListener` | `src/memory-events/memory-events.ts` | Listener type for the bus |
## File Structure
```
src/
index.ts # Exports MemoryEvents, EventListener
memory-events/
memory-events.ts # Implementation
memory-events.md # Module docs
memory-events.test.ts # Conformance + direct tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-events-memory` | `MemoryEvents`, `EventListener` |
| `@suaveplan/api-capability-events-memory/memory-events/memory-events` | `MemoryEvents`, `EventListener` |
## Patterns
### Basic pub/sub
```ts
import { MemoryEvents } from "@suaveplan/api-capability-events-memory";
const bus = new MemoryEvents();
const off = bus.on("user.created", (payload) => console.log(payload));
await bus.emit("user.created", { id: "u1" });
off(); // idempotent unsubscribe
```
### Awaited delivery
```ts
// emit awaits all listeners before resolving
await bus.emit("order.placed", { orderId: "ord_123" });
// Safe to proceed knowing all handlers have run
```
## Constraints
- **Reference implementation** — the canonical in-memory event bus; all other implementations measured against it.
- **Proxy transparency** — conformance tests prove identical behaviour behind `@suaveplan/api-capability-proxy`.
- **Idempotent unsubscribe** — calling the returned unsubscriber multiple times is safe.
- **Dependencies**: `@suaveplan/api-types`, `tslib`. Dev: `@suaveplan/api-capability-kit`, `@suaveplan/api-capability-proxy`.
## Checklist
- [ ] Read `src/memory-events/memory-events.ts` before modifying bus behaviour
- [ ] Preserve proxy transparency — changes must pass both direct and proxied conformance tests

View File

@@ -0,0 +1,91 @@
---
name: sp-api-capability-jobs-memory
description: Faithful in-memory JobRegistrationHandle reference implementation (MemoryJobs) plus trigger-on-demand runner. registerCron/registerQueued record handlers by name (last write wins); runCron/runQueued/triggerAll drive them deterministically. The fixture that runJobsConformance measures every jobs implementation against.
---
# @suaveplan/api-capability-jobs-memory
## Purpose
Faithful in-memory `JobRegistrationHandle` reference implementation (`MemoryJobs`) plus a trigger-on-demand runner for the Suaveplan API capability system.
`registerCron` / `registerQueued` record handlers by name (last write wins). `runCron` / `runQueued` / `triggerAll` drive them deterministically.
This is the fixture that `@suaveplan/api-capability-kit`'s `runJobsConformance` harness measures every jobs implementation against. Its conformance test runs against both the direct handle AND the same handle's triggers behind `@suaveplan/api-capability-proxy`, proving the proxy stays transparent.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemoryJobs` | `src/memory-jobs/memory-jobs.ts` | In-memory `JobRegistrationHandle` implementation |
| `CronHandler` | `src/memory-jobs/memory-jobs.ts` | Handler type for cron jobs |
| `QueuedHandler` | `src/memory-jobs/memory-jobs.ts` | Handler type for queued jobs |
## File Structure
```
src/
index.ts # Exports MemoryJobs, CronHandler, QueuedHandler
memory-jobs/
memory-jobs.ts # Implementation
memory-jobs.md # Module docs
memory-jobs.test.ts # Conformance + direct tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-jobs-memory` | `MemoryJobs`, `CronHandler`, `QueuedHandler` |
| `@suaveplan/api-capability-jobs-memory/memory-jobs/memory-jobs` | Same from feature subpath |
## Patterns
### Register and run a cron job
```ts
import { MemoryJobs } from "@suaveplan/api-capability-jobs-memory";
const jobs = new MemoryJobs();
jobs.registerCron("nightly", async () => {
console.log("Running nightly cleanup...");
});
await jobs.runCron("nightly");
```
### Queued job
```ts
jobs.registerQueued("send-email", async (payload) => {
await sendEmail(payload);
});
await jobs.runQueued("send-email", { to: "user@example.com" });
```
### Trigger all
```ts
await jobs.triggerAll(); // runs all registered cron and queued handlers
```
### Last-write-wins registration
```ts
jobs.registerCron("nightly", async () => { console.log("first"); });
jobs.registerCron("nightly", async () => { console.log("second"); });
await jobs.runCron("nightly"); // logs "second"
```
## Constraints
- **Reference implementation** — the canonical in-memory jobs handle; all other implementations measured against it.
- **Last-write-wins** — registering the same name twice replaces the previous handler.
- **Deterministic execution** — `runCron` / `runQueued` / `triggerAll` execute synchronously in order.
- **Proxy transparency** — conformance tests prove identical trigger behaviour behind `@suaveplan/api-capability-proxy`.
- **Dependencies**: `@suaveplan/api-types`, `tslib`. Dev: `@suaveplan/api-capability-kit`, `@suaveplan/api-capability-proxy`.
## Checklist
- [ ] Read `src/memory-jobs/memory-jobs.ts` before modifying job registration or execution
- [ ] Preserve proxy transparency — changes must pass both direct and proxied conformance tests
- [ ] Last-write-wins semantics must be preserved

View File

@@ -0,0 +1,100 @@
---
name: sp-api-capability-kit
description: Shared foundation for the api-capability-* family. Ships the stable CAP.* error-code registry + CapabilityError, and five runner-agnostic conformance harnesses (runDatabaseConformance, runSecretsConformance, runEventsConformance, runJobsConformance, runLoggerConformance), each parameterized over a caller-supplied factory. Imports NO implementation package.
---
# @suaveplan/api-capability-kit
## Purpose
Shared foundation for the `@suaveplan/api-capability-*` family. Ships two things every capability package depends on:
1. **Stable `CAP.*` error-code registry + `CapabilityError`** — thrown by the memory implementations and the generic proxy alike.
2. **Five runner-agnostic conformance harnesses** — `runDatabaseConformance`, `runSecretsConformance`, `runEventsConformance`, `runJobsConformance`, `runLoggerConformance` — each fully parameterized over a caller-supplied capability factory.
The harnesses import NO implementation package, so no `kit → memory` dependency cycle can form — every `api-capability-*-memory` package depends on the kit (for its conformance test), never the reverse.
## Architecture
| Component | Source | Role |
|---|---|---|
| `CAPABILITY_CODES` | `src/error-codes/error-codes.ts` | Stable `CAP.*` registry via `defineCodes` |
| `CapabilityError` | `src/error-codes/error-codes.ts` | Typed error class extending `SuaveplanError` |
| `runDatabaseConformance` | `src/conformance/conformance.ts` | Database capability conformance harness |
| `runSecretsConformance` | `src/conformance/conformance.ts` | Secrets capability conformance harness |
| `runEventsConformance` | `src/conformance/conformance.ts` | Events capability conformance harness |
| `runJobsConformance` | `src/conformance/conformance.ts` | Jobs capability conformance harness |
| `runLoggerConformance` | `src/conformance/conformance.ts` | Logger capability conformance harness |
## File Structure
```
src/
index.ts # Barrel: error codes + all conformance harnesses + factory types
error-codes/
error-codes.ts # CAPABILITY_CODES + CapabilityError
error-codes.test.ts
index.ts
conformance/
conformance.ts # All five conformance harnesses
conformance.md # Module docs
conformance.test.ts
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-kit` | `CAPABILITY_CODES`, `CapabilityError`, all conformance harnesses, all factory types |
| `@suaveplan/api-capability-kit/error-codes/error-codes` | `CAPABILITY_CODES`, `CapabilityError`, `CapabilityCode` |
| `@suaveplan/api-capability-kit/conformance/conformance` | All five `run*Conformance` harnesses + factory types |
## Patterns
### Running conformance against a secrets implementation
```ts
import { runSecretsConformance } from "@suaveplan/api-capability-kit";
import { MemorySecrets } from "@suaveplan/api-capability-secrets-memory";
runSecretsConformance("MemorySecrets", (seed) => new MemorySecrets(seed));
```
### Running conformance against a database implementation
```ts
import { runDatabaseConformance } from "@suaveplan/api-capability-kit";
import { MemoryDatabase } from "@suaveplan/api-capability-database-memory";
runDatabaseConformance("MemoryDatabase", () => new MemoryDatabase());
```
### Typed error handling
```ts
import { CAPABILITY_CODES, CapabilityError } from "@suaveplan/api-capability-kit";
try {
// ... capability operation
} catch (err) {
if (err instanceof CapabilityError && err.code === CAPABILITY_CODES.NOT_FOUND) {
// handle not-found
}
}
```
## Constraints
- **Imports NO implementation package** — the kit depends only on `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `tslib`. This breaks the `kit → memory` cycle.
- **Five conformance harnesses** — database, secrets, events, jobs, logger.
- **Parameterized over factories** — each harness takes a label and a factory function; the caller provides the implementation.
- **Explicit named re-exports** — no `export *` per CLAUDE.md rule 10.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `tslib`.
## Checklist
- [ ] Read `src/conformance/conformance.ts` before modifying any harness
- [ ] Read `src/error-codes/error-codes.ts` before modifying the registry
- [ ] Never import an implementation package — keep the kit cycle-free
- [ ] New conformance harnesses must be parameterized over a factory, not import a concrete implementation

View File

@@ -0,0 +1,121 @@
---
name: sp-api-capability-logger-kit
description: Contract glue for the logger.structured capability tier. LOGGER_CAP.* error codes + LoggerCapError, universal-tier level helpers (LOG_LEVELS/compareLevels/isLogRecord), Zod boundary schemas (logLevelSchema/logRecordSchema), and the parameterised conformance harness runStructuredLoggerConformance. Runtime entries are node:*-free; test-only ./testing subpath permits node:async_hooks and @opentelemetry/api.
---
# @suaveplan/api-capability-logger-kit
## Purpose
Contract glue for the `logger.structured` capability tier. Ships:
1. **Stable `LOGGER_CAP.*` error-code registry + `LoggerCapError`**
2. **Universal-tier level helpers** — `LOG_LEVELS`, `compareLevels`, `isLogRecord`
3. **Zod boundary schemas** — `logLevelSchema`, `logRecordSchema`
4. **Parameterised conformance harness** — `runStructuredLoggerConformance(label, factory)` pinning thirteen structured-logger invariants
The harness imports no concrete variant — so no `kit → variant` cycle can form. The `node:async_hooks` + `@opentelemetry/api` OTel-context test helper lives ONLY under the test-only `./testing` subpath, keeping every runtime entry (`.`, `./error-codes`, `./levels`, `./schemas`) `node:*`-free (universal tier).
## Architecture
| Component | Source | Role |
|---|---|---|
| `LOGGER_CAP_CODES` | `src/error-codes/error-codes.ts` | Stable `LOGGER_CAP.*` registry |
| `LoggerCapError` | `src/error-codes/error-codes.ts` | Typed error class |
| `LOG_LEVELS` / `compareLevels` / `isLogRecord` | `src/levels/levels.ts` | Universal-tier level helpers |
| `logLevelSchema` / `logRecordSchema` | `src/schemas/schemas.ts` | Zod boundary schemas |
| `runStructuredLoggerConformance` | `src/testing/conformance.ts` | Parameterised conformance harness |
## File Structure
```
src/
index.ts # Runtime barrel: error codes, levels, schemas
error-codes/
error-codes.ts # LOGGER_CAP_CODES + LoggerCapError
error-codes.test.ts
index.ts
levels/
levels.ts # LOG_LEVELS, compareLevels, isLogRecord
levels.test.ts
levels.md
index.ts
schemas/
schemas.ts # logLevelSchema, logRecordSchema
schemas.test.ts
schemas.md
index.ts
testing/
conformance.ts # runStructuredLoggerConformance harness
conformance-helpers.ts # Seeded RNG and OTel-context helpers
conformance.test.ts
conformance.md
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-logger-kit` | Runtime: `LOGGER_CAP_CODES`, `LoggerCapError`, `LOG_LEVELS`, `compareLevels`, `isLogRecord`, `logLevelSchema`, `logRecordSchema` |
| `@suaveplan/api-capability-logger-kit/error-codes/error-codes` | `LOGGER_CAP_CODES`, `LoggerCapError`, `LoggerCapCode` |
| `@suaveplan/api-capability-logger-kit/levels/levels` | `LOG_LEVELS`, `compareLevels`, `isLogRecord` |
| `@suaveplan/api-capability-logger-kit/schemas/schemas` | `logLevelSchema`, `logRecordSchema` |
| `@suaveplan/api-capability-logger-kit/testing` | `runStructuredLoggerConformance`, conformance helpers |
## Patterns
### Runtime helpers (universal tier)
```ts
import {
compareLevels,
isLogRecord,
LOG_LEVELS,
LOGGER_CAP_CODES,
LoggerCapError,
logLevelSchema,
logRecordSchema,
} from "@suaveplan/api-capability-logger-kit";
const level = logLevelSchema.parse("info");
const record = logRecordSchema.parse({ level: "info", message: "hello", timestamp: Date.now() });
if (compareLevels("warn", "info") > 0) { /* warn is more severe */ }
```
### Conformance harness (test-only)
```ts
import { runStructuredLoggerConformance } from "@suaveplan/api-capability-logger-kit/testing";
runStructuredLoggerConformance("my-adapter", (config) => ({
logger: createMyLogger({
rng: config.rng,
sampleRate: config.sampleRate,
onError: config.recordError,
}),
}));
```
### Error handling
```ts
throw new LoggerCapError(
LOGGER_CAP_CODES.INVALID_LOG_LEVEL,
"unrecognized log level"
);
```
## Constraints
- **Universal tier for runtime** — `.`, `./error-codes`, `./levels`, `./schemas` import nothing from `node:*` or `@opentelemetry/api`. Pinned by `tests/dep-graph/universal-purity.test.ts`.
- **Test-only `./testing` subpath** — the one place permitted to reach `node:async_hooks` / `@opentelemetry/api`.
- **Kit imports no variant** — no `kit → variant` dependency cycle.
- **Dependencies**: `@opentelemetry/api`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `tslib`, `zod`. Dev: `@suaveplan/opentelemetry`.
## Checklist
- [ ] Read `src/testing/conformance.ts` before modifying harness behaviour
- [ ] Read `src/levels/levels.ts` before changing level comparison logic
- [ ] Preserve universal-tier purity — runtime entries must not import `node:*`
- [ ] OTel-context helpers must stay behind `./testing` only

View File

@@ -0,0 +1,83 @@
---
name: sp-api-capability-logger-memory
description: Faithful in-memory CapabilityLoggerLike reference implementation (MemoryLogger) for the Suaveplan API capability system. Records every debug/info/warn/error call as a structured line on a shared sink, with child(bindings) merging parent + child bindings (child wins). The fixture that runLoggerConformance measures every logger implementation against.
---
# @suaveplan/api-capability-logger-memory
## Purpose
Faithful in-memory `CapabilityLoggerLike` reference implementation (`MemoryLogger`) for the Suaveplan API capability system. Records every `debug`/`info`/`warn`/`error` call as a structured line on a shared sink, with `child(bindings)` merging parent + child bindings (child wins) into descendant lines.
This is the fixture that `@suaveplan/api-capability-kit`'s `runLoggerConformance` harness measures every logger implementation against. Its conformance test runs against both the direct implementation AND the same implementation behind `@suaveplan/api-capability-proxy`, proving the proxy stays transparent.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemoryLogger` | `src/memory-logger/memory-logger.ts` | In-memory `CapabilityLoggerLike` implementation |
| `LogLevel` | `src/memory-logger/memory-logger.ts` | Log level type |
| `RecordedLine` | `src/memory-logger/memory-logger.ts` | Recorded structured line type |
## File Structure
```
src/
index.ts # Exports MemoryLogger, LogLevel, RecordedLine
memory-logger/
memory-logger.ts # Implementation
memory-logger.md # Module docs
memory-logger.test.ts # Conformance + direct tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-logger-memory` | `MemoryLogger`, `LogLevel`, `RecordedLine` |
| `@suaveplan/api-capability-logger-memory/memory-logger/memory-logger` | Same from feature subpath |
## Patterns
### Basic logging
```ts
import { MemoryLogger } from "@suaveplan/api-capability-logger-memory";
const log = new MemoryLogger();
log.info("server started", { port: 8080 });
log.error("connection failed", { err: new Error("timeout") });
```
### Child logger with merged bindings
```ts
const requestLog = log.child({ requestId: "req_123" });
requestLog.info("processing"); // includes { requestId: "req_123" }
const dbLog = requestLog.child({ db: "users" });
dbLog.info("query"); // includes { requestId: "req_123", db: "users" }
```
### Inspecting recorded lines
```ts
// MemoryLogger exposes its recorded lines for test assertions
expect(log.lines).toHaveLength(2);
expect(log.lines[0].level).toBe("info");
expect(log.lines[0].message).toBe("server started");
expect(log.lines[0].bindings).toEqual({ port: 8080 });
```
## Constraints
- **Reference implementation** — the canonical in-memory logger; all other implementations measured against it.
- **Proxy transparency** — conformance tests prove identical behaviour behind `@suaveplan/api-capability-proxy`.
- **Child wins on merge** — when `child(bindings)` is called, child bindings override parent bindings with the same key.
- **Structured lines** — every log call is recorded as a structured object with level, message, timestamp, and bindings.
- **Dependencies**: `@suaveplan/api-types`, `tslib`. Dev: `@suaveplan/api-capability-kit`, `@suaveplan/api-capability-proxy`.
## Checklist
- [ ] Read `src/memory-logger/memory-logger.ts` before modifying logger behaviour
- [ ] Preserve proxy transparency — changes must pass both direct and proxied conformance tests
- [ ] Child binding merge semantics (child wins) must be preserved

View File

@@ -0,0 +1,94 @@
---
name: sp-api-capability-logger-structured-memory
description: In-memory reference implementation of the logger.structured capability tier. createReferenceLogger provides six severity rungs, pluggable sinks with per-sink min-level, redactor chains with bypass detection, FNV-1a deterministic per-traceId sampling, OTel span enrichment, ordered flush() with timeout, and idempotent dispose(). Conforms via runStructuredLoggerConformance.
---
# @suaveplan/api-capability-logger-structured-memory
## Purpose
In-memory reference implementation of the `logger.structured` capability tier. `createReferenceLogger` is a faithful `StructuredLoggerLike` with:
- Six severity rungs
- Pluggable sinks with per-sink min-level
- Redactor chains with bypass detection
- FNV-1a deterministic per-`traceId` sampling
- OTel span enrichment via `@suaveplan/opentelemetry`
- Ordered `flush()` with timeout
- Idempotent `dispose()`
Every failure mode surfaces a `LoggerCapError` carrying the appropriate `LOGGER_CAP.*` code from `@suaveplan/api-capability-logger-kit`. The package proves its conformance by running the kit's `runStructuredLoggerConformance` harness against itself.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createReferenceLogger` | `src/reference-logger/reference-logger.ts` | Factory returning a `StructuredLoggerLike` |
| `ReferenceLoggerOptions` | `src/reference-logger/reference-logger.ts` | Options: level, sinks, redactors, sampleRate, etc. |
| `BypassDetectorEntry` | `src/reference-logger/reference-logger.ts` | Redactor bypass detection metadata |
## File Structure
```
src/
index.ts # Exports createReferenceLogger, ReferenceLoggerOptions, BypassDetectorEntry
reference-logger/
reference-logger.ts # Implementation
reference-logger.md # Module docs
reference-logger.test.ts # Unit + conformance tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-logger-structured-memory` | `createReferenceLogger`, `ReferenceLoggerOptions`, `BypassDetectorEntry` |
| `@suaveplan/api-capability-logger-structured-memory/reference-logger/reference-logger` | Same from feature subpath |
## Patterns
### Basic structured logging
```ts
import { createReferenceLogger } from "@suaveplan/api-capability-logger-structured-memory";
const logger = createReferenceLogger({ level: "info" });
logger.addSink({
id: "collector",
write(record) { /* send to collector */ },
});
logger.info("started", { service: "api" });
await logger.flush();
await logger.dispose();
```
### Per-sink minimum level
```ts
logger.addSink({
id: "error-only",
minLevel: "error",
write(record) { /* only receives error/fatal */ },
});
```
### Deterministic sampling
```ts
const logger = createReferenceLogger({
sampleRate: 0.1, // 10% sampling via FNV-1a hash of traceId
});
```
## Constraints
- **Universal tier** — never imports `node:async_hooks`; OTel span ids via `@suaveplan/opentelemetry` (browser-safe).
- **13-invariant conformance** — proven via `runStructuredLoggerConformance`.
- **LoggerCapError on failures** — every failure mode carries a `LOGGER_CAP.*` code.
- **Idempotent dispose** — calling `dispose()` multiple times is safe.
- **Dependencies**: `@suaveplan/api-capability-logger-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/opentelemetry`, `tslib`. Dev: `@opentelemetry/api`.
## Checklist
- [ ] Read `src/reference-logger/reference-logger.ts` before modifying logger behaviour
- [ ] Preserve conformance with `runStructuredLoggerConformance`
- [ ] Preserve universal-tier purity (no `node:*` imports)

View File

@@ -0,0 +1,94 @@
---
name: sp-api-capability-proxy
description: The ONE generic remote-capability proxy for the Suaveplan API platform. createRemoteCapabilityProxy + createCapabilityHost marshal every capability call across a Transport seam and gate it by permission. createInProcessTransport / portTransport supply the MessageChannel-backed in-process transport. Replaces five per-capability *-worker-proxy packages.
---
# @suaveplan/api-capability-proxy
## Purpose
The ONE generic remote-capability proxy for the Suaveplan API platform — it replaces the five per-capability `*-worker-proxy` packages of the old design.
`createRemoteCapabilityProxy` + `createCapabilityHost` marshal every capability call across a `Transport` seam and gate it by permission; `createInProcessTransport` / `portTransport` supply the `MessageChannel`-backed in-process transport they round-trip over.
The proxy depends ONLY on the structural `Transport` interface, so the `@suaveplan/api-sandbox` worker / vm / wasm transports slot into the same seam.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createRemoteCapabilityProxy` | `src/remote-capability-proxy/remote-capability-proxy.ts` | Client-side proxy factory |
| `createCapabilityHost` | `src/remote-capability-proxy/remote-capability-proxy.ts` | Host-side capability registrar |
| `Transport` | `src/remote-capability-proxy/remote-capability-proxy.ts` | Structural transport interface |
| `PermissionPredicate` | `src/remote-capability-proxy/remote-capability-proxy.ts` | Permission gating function type |
| `createInProcessTransport` | `src/in-process-transport/in-process-transport.ts` | MessageChannel-backed transport factory |
| `portTransport` | `src/in-process-transport/in-process-transport.ts` | Port-based transport helper |
## File Structure
```
src/
index.ts # Exports proxy, host, transport, permissions
remote-capability-proxy/
remote-capability-proxy.ts # Proxy + host + Transport interface
remote-capability-proxy.md # Module docs
remote-capability-proxy.test.ts
in-process-transport/
in-process-transport.ts # MessageChannel transport
in-process-transport.md # Module docs
in-process-transport.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-proxy` | `createRemoteCapabilityProxy`, `createCapabilityHost`, `createInProcessTransport`, `portTransport`, `Transport`, `PermissionPredicate`, `CapabilityHostOptions`, `RemoteCapabilityProxyOptions` |
| `@suaveplan/api-capability-proxy/remote-capability-proxy/remote-capability-proxy` | Proxy + host + types |
| `@suaveplan/api-capability-proxy/in-process-transport/in-process-transport` | Transport implementations |
## Patterns
### In-process proxy round-trip
```ts
import {
createCapabilityHost,
createInProcessTransport,
createRemoteCapabilityProxy,
} from "@suaveplan/api-capability-proxy";
const { local, remote } = createInProcessTransport();
const host = createCapabilityHost({ transport: local });
const proxy = createRemoteCapabilityProxy({ transport: remote });
host.register("database", dbCapability);
const db = proxy.get("database");
await db.query("SELECT * FROM users");
```
### Permission gating
```ts
const host = createCapabilityHost({
transport: local,
permissions: (capability, action) => {
return capability !== "secrets" || action === "get";
},
});
```
## Constraints
- **Generic over all capabilities** — one proxy replaces five per-capability packages.
- **Transport-agnostic** — depends only on the structural `Transport` interface.
- **No proxy → memory cycle** — imports `@suaveplan/api-capability-kit` for `CAP.*` codes + `CapabilityError`, but no memory implementation.
- **Proven transparent** — each capability's conformance harness is re-run against the PROXIED capability.
- **Dependencies**: `@suaveplan/api-capability-kit`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/remote-capability-proxy/remote-capability-proxy.ts` before modifying proxy/host behaviour
- [ ] Read `src/in-process-transport/in-process-transport.ts` before changing transport semantics
- [ ] Preserve transport-agnostic design — never depend on concrete MessageChannel / worker / vm
- [ ] Ensure proxy transparency by re-running conformance harnesses against proxied capabilities

View File

@@ -0,0 +1,77 @@
---
name: sp-api-capability-secrets-memory
description: Faithful in-memory SecretsLike reference implementation (MemorySecrets) for the Suaveplan API capability system. Seeded name→value store with get, require (throws CAP.SECRET_NOT_FOUND), and list. The fixture that runSecretsConformance measures every secrets source against.
---
# @suaveplan/api-capability-secrets-memory
## Purpose
Faithful in-memory `SecretsLike` reference implementation (`MemorySecrets`) for the Suaveplan API capability system. A seeded name → value store that resolves via `get`, throws `CAP.SECRET_NOT_FOUND` from `require`, and enumerates names via `list`.
This is the fixture every other secrets source (env, vault, Bun) is measured against through the `@suaveplan/api-capability-kit` `runSecretsConformance` harness. Its conformance test also runs against both the direct implementation AND the same implementation behind `@suaveplan/api-capability-proxy`, proving the proxy stays transparent.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemorySecrets` | `src/memory-secrets/memory-secrets.ts` | In-memory `SecretsLike` implementation |
| `MemorySecretsSeed` | `src/memory-secrets/memory-secrets.ts` | Seed type for initial values |
## File Structure
```
src/
index.ts # Exports MemorySecrets, MemorySecretsSeed
memory-secrets/
memory-secrets.ts # Implementation
memory-secrets.md # Module docs
memory-secrets.test.ts # Conformance + direct tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-capability-secrets-memory` | `MemorySecrets`, `MemorySecretsSeed` |
| `@suaveplan/api-capability-secrets-memory/memory-secrets/memory-secrets` | Same from feature subpath |
## Patterns
### Basic usage
```ts
import { MemorySecrets } from "@suaveplan/api-capability-secrets-memory";
const secrets = new MemorySecrets({ STRIPE_KEY: "sk_test_123" });
const key = await secrets.get("STRIPE_KEY"); // "sk_test_123"
const key2 = await secrets.require("STRIPE_KEY"); // "sk_test_123"
const missing = await secrets.get("MISSING"); // undefined
```
### require throws on missing
```ts
try {
await secrets.require("MISSING");
} catch (err) {
// err is CapabilityError with code CAPABILITY_CODES.SECRET_NOT_FOUND
}
```
### Listing secrets
```ts
const names = await secrets.list(); // ["STRIPE_KEY"]
```
## Constraints
- **Reference implementation** — the canonical in-memory secrets store; all other sources measured against it.
- **Proxy transparency** — conformance tests prove identical behaviour behind `@suaveplan/api-capability-proxy`.
- **Dependencies**: `@suaveplan/api-capability-kit`, `@suaveplan/api-types`, `tslib`. Dev: `@suaveplan/api-capability-proxy`.
## Checklist
- [ ] Read `src/memory-secrets/memory-secrets.ts` before modifying secrets behaviour
- [ ] Preserve proxy transparency — changes must pass both direct and proxied conformance tests

View File

@@ -0,0 +1,39 @@
---
name: api-stack-api-core-navigator
description: Navigator for api-stack/api-core. Pick a library to get detailed API guidance.
---
# Api core (api-stack)
Packages in `api-stack/api-core`.
## Libraries
### @suaveplan/api
Framework-free Fetch-native plugin runtime for the Suaveplan API platform. Registers plugins, orders them by dependsOn (Kahn topological sort with cycle detection), validates required capabilities aga...
→ Skill: `sp-api/SKILL.md`
### @suaveplan/api-core
Framework-free Fetch-native HTTP engine for the Suaveplan API platform. Owns the radix route matcher, onion middleware composition, the request dispatcher that turns a route table into a WinterCG ApiR...
→ Skill: `sp-api-core/SKILL.md`
### @suaveplan/api-schemas
Shared Zod schemas describing Suaveplan API request/response shapes — single source of truth for server plugins, React data hooks, and generated OpenAPI documents
→ Skill: `sp-api-schemas/SKILL.md`
### @suaveplan/api-types
Types-only contract package for the Suaveplan API plugin foundation. Defines Principal, TenantId, PluginContext, RequestContext, Router, Middleware, CapabilityMap, the PluginManifest type-level view, ...
→ Skill: `sp-api-types/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,163 @@
---
name: sp-api-core
description: Framework-free Fetch-native HTTP engine for the Suaveplan API platform. Owns the radix route matcher, onion middleware composition, the request dispatcher (fetch(Request) => Response), WHATWG Response helpers, RFC 9457 problem+json serialisation, and the ApiError code registry. Plugin-unaware — the host injects context construction. Runs standalone on Bun/Node/Deno/Workers.
---
# @suaveplan/api-core
## Purpose
The HTTP engine the whole API platform runs on. The runtime **is** a WinterCG `fetch(Request) => Promise<Response>` handler, so host frameworks (Hono, Fastify, Express, Koa) and runtimes (Bun, Deno, Node, Workers) are optional *guests* that mount it rather than required hosts.
`api-core` is deliberately **plugin-unaware**: it knows nothing about plugin manifests, lifecycle, capabilities, auth, or tenancy — the host (`@suaveplan/api`) injects full request-context construction. There is exactly one HTTP model (the WHATWG `Request`/`Response`) and one error serialisation (RFC 9457 problem+json).
## Architecture
| Component | Source | Role |
|---|---|---|
| `createDispatcher` | `src/dispatcher/dispatcher.ts` | Route table → `ApiRuntime` (fetch handler) |
| `createRouter` | `src/router/router.ts` | Radix-trie route matcher (static / `:param` / `*`) |
| `compose` | `src/compose/compose.ts` | Onion middleware composition |
| `json` / `text` / `noContent` / `redirect` | `src/responses/responses.ts` | WHATWG Response builders |
| `problemResponse` / `toProblemDocument` | `src/problem/problem.ts` | RFC 9457 problem+json serialisation |
| `API_CORE_CODES` / `ApiError` | `src/error-codes/error-codes.ts` | Error code registry + typed error class |
## File Structure
```
src/
index.ts # Barrel: dispatcher, router, compose, responses, problem, error codes
dispatcher/
dispatcher.ts # createDispatcher
dispatcher.md
dispatcher.test.ts
index.ts
router/
router.ts # createRouter (radix trie)
router.md
router.test.ts
index.ts
compose/
compose.ts # onion middleware composition
compose.md
compose.test.ts
index.ts
responses/
responses.ts # Response builders
responses.md
responses.test.ts
index.ts
problem/
problem.ts # RFC 9457 serialisation
problem.md
problem.test.ts
index.ts
error-codes/
error-codes.ts # API_CORE_CODES + ApiError
error-codes.md
error-codes.test.ts
index.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-core` | `createDispatcher`, `createRouter`, `compose`, `json`, `text`, `noContent`, `redirect`, `problemResponse`, `toProblemDocument`, `API_CORE_CODES`, `ApiError`, all types |
| `@suaveplan/api-core/router` | `createRouter`, `Router`, `MatchResult` |
| `@suaveplan/api-core/compose` | `compose`, `Composed` |
| `@suaveplan/api-core/responses` | `json`, `text`, `noContent`, `redirect`, `RedirectStatus` |
| `@suaveplan/api-core/problem` | `problemResponse`, `toProblemDocument`, `ProblemDocument`, `PROBLEM_CONTENT_TYPE` |
| `@suaveplan/api-core/dispatcher` | `createDispatcher`, `DispatcherConfig`, `BaseRequestFields` |
| `@suaveplan/api-core/error-codes` | `API_CORE_CODES`, `ApiError`, `ApiCoreCode`, `DEFAULT_STATUS_BY_CODE` |
## Patterns
### Standalone dispatcher
```ts
import { createDispatcher, json } from "@suaveplan/api-core";
const api = createDispatcher({
routes: [
{ method: "GET", path: "/widgets/:id", handler: (ctx) => json({ id: ctx.params.id }) },
],
createContext: (base) => buildHostContext(base),
});
// Standalone on any WinterCG runtime:
Bun.serve({ fetch: api.fetch });
// or mounted as a guest:
// hono.mount("/api", api.fetch);
```
### Radix router
```ts
import { createRouter } from "@suaveplan/api-core/router";
const router = createRouter();
router.add("GET", "/users/:id", handler);
router.add("GET", "/users/:id/posts", handler);
const match = router.match("GET", "/users/42"); // { params: { id: "42" }, handler }
```
### Onion middleware
```ts
import { compose } from "@suaveplan/api-core/compose";
const stack = compose([
async (ctx, next) => {
const start = Date.now();
await next();
ctx.response.headers.set("x-response-time", String(Date.now() - start));
},
routeHandler,
]);
```
### RFC 9457 problem+json
```ts
import { problemResponse } from "@suaveplan/api-core/problem";
return problemResponse(404, {
title: "Widget not found",
detail: `No widget with id ${id}`,
});
```
### Response builders
```ts
import { json, text, noContent, redirect } from "@suaveplan/api-core/responses";
json({ ok: true });
text("Hello");
noContent();
redirect("/new-path", 301);
```
## Constraints
- **Plugin-unaware** — knows nothing about plugins, auth, capabilities, or tenancy. The host injects context.
- **One HTTP model** — only WHATWG `Request`/`Response`.
- **One error format** — only RFC 9457 problem+json.
- **100% coverage** — unit tests co-located, coverage gated at 100% on every metric.
- **No mocks** — tests use real WHATYG `Request`/`Response` objects.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `API_CORE.*` | HTTP engine error registry (exact codes in `src/error-codes/error-codes.ts`) |
## Checklist
- [ ] Read `src/dispatcher/dispatcher.ts` before modifying request dispatch
- [ ] Read `src/router/router.ts` before changing route matching
- [ ] Read `src/compose/compose.ts` before modifying middleware composition
- [ ] Preserve plugin-unaware design — never import plugin/auth/capability concepts
- [ ] Preserve the single HTTP model (WHATYG Request/Response) and single error format (RFC 9457)

View File

@@ -0,0 +1,100 @@
---
name: sp-api-schemas
description: Shared Zod schemas describing Suaveplan API request/response shapes — single source of truth for server plugins, React data hooks, and generated OpenAPI documents. Covers auth, admin, common (pagination, sort, filter, timestamps), events, and helpers.
---
# @suaveplan/api-schemas
## Purpose
Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI documents.
## Architecture
| Module | Source | Role |
|---|---|---|
| `common/pagination` | `src/common/pagination.ts` | Cursor-based and offset-based pagination input/output |
| `common/sort` | `src/common/sort.ts` | Sort criteria (field + direction, multi-column) |
| `common/filter` | `src/common/filter.ts` | Filter predicates with 13 comparison operators |
| `common/id-param` | `src/common/id-param.ts` | URL path parameters for single and nested resources |
| `common/timestamps` | `src/common/timestamps.ts` | Audit timestamps (createdAt/updatedAt, soft-delete) |
| `common/error-envelope` | `src/common/error-envelope.ts` | Structured error responses with field-level details |
| `auth/login` | `src/auth/login.ts` | Login request/response (credentials → token + user) |
| `auth/register` | `src/auth/register.ts` | Registration request/response |
| `auth/refresh` | `src/auth/refresh.ts` | Token refresh with rotation |
| `auth/logout` | `src/auth/logout.ts` | Session revocation |
| `auth/context` | `src/auth/context.ts` | Decoded auth context (userId, roles, tenantId) |
| `admin/ping` | `src/admin/ping.ts` | Sentinel liveness check |
| `admin/health` | `src/admin/health.ts` | Aggregate + per-subsystem health status |
| `admin/status` | `src/admin/status.ts` | Deployment metadata (version, uptime, environment) |
| `events/metadata` | `src/events/metadata.ts` | Event correlation and actor context |
| `events/domain-event` | `src/events/domain-event.ts` | Immutable domain event record |
| `events/envelope` | `src/events/envelope.ts` | Transport wrapper with sequence number |
| `helpers/to-suaveplan-error` | `src/helpers/to-suaveplan-error.ts` | Convert ZodError → ErrorEnvelope |
## File Structure
```
src/
index.ts # Re-exports z from zod + all subpaths
common/
pagination.ts, sort.ts, filter.ts, id-param.ts, timestamps.ts, error-envelope.ts
auth/
login.ts, register.ts, refresh.ts, logout.ts, context.ts
admin/
ping.ts, health.ts, status.ts
events/
metadata.ts, domain-event.ts, envelope.ts
helpers/
to-suaveplan-error.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-schemas` | `z` (re-exported from zod) + all schemas |
| `@suaveplan/api-schemas/common` | Pagination, sort, filter, id-param, timestamps, error-envelope schemas |
| `@suaveplan/api-schemas/auth` | Login, register, refresh, logout, context schemas |
| `@suaveplan/api-schemas/admin` | Ping, health, status schemas |
| `@suaveplan/api-schemas/events` | Metadata, domain-event, envelope schemas |
| `@suaveplan/api-schemas/helpers` | `toSuaveplanError` helper |
## Patterns
### Importing schemas
```ts
import { LoginInput, LoginOutput } from "@suaveplan/api-schemas/auth";
import { PaginationInput, ErrorEnvelope } from "@suaveplan/api-schemas/common";
import { AdminHealthOutput } from "@suaveplan/api-schemas/admin";
import { EventEnvelope } from "@suaveplan/api-schemas/events";
import { toSuaveplanError } from "@suaveplan/api-schemas/helpers";
import z from "@suaveplan/api-schemas"; // re-exported for convenience
```
### Converting ZodError
```ts
import { toSuaveplanError } from "@suaveplan/api-schemas/helpers";
try {
const result = schema.parse(input);
} catch (err) {
if (err instanceof z.ZodError) {
const envelope = toSuaveplanError(err);
return json(envelope, { status: 400 });
}
}
```
## Constraints
- **Leaf package** — no `@suaveplan/*` runtime dependencies; only `zod`, `tslib`, `@opentelemetry/api`.
- **Single source of truth** — every API shape is defined here and consumed by plugins, React hooks, and OpenAPI generation.
- **Re-exports z** — `import z from "@suaveplan/api-schemas"` works for convenience.
## Checklist
- [ ] New API shapes must be added here first before being used in plugins or UI
- [ ] Keep schemas in sync with `@suaveplan/api-types` type contracts

View File

@@ -0,0 +1,122 @@
---
name: sp-api-types
description: Types-only contract package for the Suaveplan API plugin foundation. Defines Principal, TenantId, PluginContext, RequestContext, Router, Middleware, CapabilityMap, PluginManifest, lifecycle states, worker-RPC proxy transform, rollback journal, and 20+ subpath contracts. Zero runtime, zero dependencies. Emits only .d.ts files.
---
# @suaveplan/api-types
## Purpose
Types-only contract package for the Suaveplan API plugin foundation. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-host`, every adapter) share.
**Zero runtime. Zero dependencies.** The build emits only `.d.ts` files; every subpath's `dist/<sub>/index.js` is an empty module marker. Consumers declare this package as a regular `dependency` (not `peerDependency`) — installing it adds no runtime weight.
## Architecture
| Subpath | Contract |
|---|---|
| `principal` | `Principal`, `PrincipalId`, `Scope`, `Claims` |
| `tenant` | `TenantId`, `TenantContext` |
| `manifest` | `PluginManifest`, `TrustLevel`, `IsolationTier` |
| `router` | `Router`, `RouteHandler`, `Middleware`, `HttpMethod` |
| `runtime` | `ApiRuntime`, `CapabilityResolver`, `RouteDescriptor`, `RouteSchema` |
| `pipeline-events` | `PipelineStageEvent`, `PipelineEventsBus`, `PipelineStage` |
| `capabilities` | `CapabilityId`, `CapabilityMap`, `CapabilityRequirements` |
| `plugin-context` | `PluginContext`, `RequestContext`, `ConnectionContext`, `JobContext` |
| `lifecycle` | `PluginState`, `PluginTransition` |
| `errors` | `ErrorCode`, `ErrorEnvelope` |
| `audit` | `AuditEvent`, `AuditOutcome`, `AuditSink` |
| `auth` | `SessionResolver`, `AuthAdapter`, `AuthProviderAdapter` |
| `tenant-resolver` | `TenantResolver`, `TenantResolverChain` |
| `secrets` | `SecretsLike` |
| `db` | `AdminDatabaseLike`, `TenantScopedDatabaseLike`, `DbTransaction`, `Migration` |
| `graphql` | `GraphqlRegistry`, `ResolverMap` |
| `ws` | `WsConnection`, `WsConnectionRegistry`, `WsBroadcast` |
| `sse` | `SseConnection`, `SseBroker` |
| `saga` | `SagaOrchestrator`, `SagaParticipant` |
| `twopc` | `TwoPhaseCommitParticipant` |
| `worker-rpc` | `WorkerRpcMessage`, `WorkerProxyOf`, `NotCloneable` |
| `instruments` | `Counter`, `Histogram`, `Gauge`, `InstrumentRegistry` |
| `rollback-journal` | `RollbackJournal`, `RollbackJournalEntry` |
| `sandbox-bridge` | `SerializedRequest`, `SerializedResponse`, `WASI_STDIO_PROTOCOL_VERSION` |
## File Structure
```
src/
index.ts # Barrel: namespace re-exports for every subpath
principal/
tenant/
manifest/
router/
runtime/
pipeline-events/
capabilities/
plugin-context/
lifecycle/
errors/
audit/
auth/
tenant-resolver/
secrets/
db/
graphql/
ws/
sse/
saga/
twopc/
worker-rpc/
instruments/
rollback-journal/
sandbox-bridge/
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-types` | Namespace re-exports for every subpath + explicit named re-exports for common types |
| `@suaveplan/api-types/principal` | `Principal`, `PrincipalId`, `Scope`, `Claims` |
| `@suaveplan/api-types/tenant` | `TenantId`, `TenantContext` |
| `@suaveplan/api-types/router` | `Router`, `RouteHandler`, `Middleware`, `HttpMethod` |
| `@suaveplan/api-types/runtime` | `ApiRuntime`, `CapabilityResolver`, `RouteDescriptor` |
| `@suaveplan/api-types/auth` | `SessionResolver`, `AuthAdapter`, `AuthProviderAdapter` |
| `@suaveplan/api-types/db` | `AdminDatabaseLike`, `TenantScopedDatabaseLike`, `DbTransaction`, `Migration` |
| `@suaveplan/api-types/capabilities` | `CapabilityId`, `CapabilityMap`, `CapabilityRequirements` |
| ... | (20+ subpaths, see README) |
## Patterns
### Importing types
```ts
import type { Principal, PrincipalId, Scope } from "@suaveplan/api-types/principal";
import type { TenantId } from "@suaveplan/api-types/tenant";
import type { PluginContext, RequestContext } from "@suaveplan/api-types/plugin-context";
import type { AdminDatabaseLike } from "@suaveplan/api-types/db";
import type { SessionResolver } from "@suaveplan/api-types/auth";
```
### Barrel namespace imports
```ts
import { principal, tenant, db } from "@suaveplan/api-types";
// Use via namespace
type MyPrincipal = principal.Principal;
```
## Constraints
- **Types-only** — zero runtime code; `.js` files are empty module markers.
- **Zero dependencies** — no runtime or dev dependencies in `dependencies`.
- **No `export *` at root** — explicit named re-exports per CLAUDE.md rule 10.
- **Declare as regular dependency** — not `peerDependency`; types-only packages have no shared-singleton concern.
- **Type-level conformance** — `expectTypeOf` tests in `src/_typetests/*.spec-conformance.test.ts`.
## Checklist
- [ ] New cross-cutting types must be added to the appropriate subpath, not duplicated in consuming packages
- [ ] Root barrel must use explicit named re-exports, never `export *`
- [ ] Verify `.js` emitted files stay within empty-module budget
- [ ] Keep in sync with `@suaveplan/api-schemas` Zod schemas

View File

@@ -0,0 +1,149 @@
---
name: sp-api
description: Framework-free Fetch-native plugin runtime for the Suaveplan API platform. Registers plugins, orders them by dependsOn (Kahn topological sort with cycle detection), validates capabilities, drives PluginState lifecycle FSM, assembles RequestContext, emits PipelineStageEvents, compiles routes into an api-core dispatcher, and performs zero-downtime blue-green reload. Server-tier (Node + Bun + Deno + Workers).
---
# @suaveplan/api
## Purpose
Framework-free Fetch-native plugin runtime for the Suaveplan API platform.
`createApi(config)` registers plugins, orders them by `dependsOn` (Kahn topological sort with cycle detection), validates each plugin's required capabilities against a shared `CapabilityRegistry`, drives every plugin through the `PluginState` lifecycle FSM, assembles the per-request `RequestContext` via an injected context factory, emits `PipelineStageEvent`s, compiles the active route set into an `@suaveplan/api-core` dispatcher, and performs zero-downtime blue-green reload (atomic dispatcher swap + bounded drain + rollback) through an injectable `ModuleResolver`.
The runtime is a WinterCG `fetch(Request) => Promise<Response>` handler, so host frameworks (Hono, Fastify, Express) are optional guests and runtimes (Bun, Deno, Workers) accept it directly. No framework is imported.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createApi` | `src/app/create-api.ts` | Keystone entry point — assembles the whole runtime |
| `CapabilityRegistry` | `src/capabilities/capability-registry.ts` | Runtime `CapabilityResolver`; validates plugin required capabilities |
| `PluginRegistry` | `src/plugins/plugin-registry.ts` | Plugin registration, dependency ordering, capability validation |
| `LifecycleDriver` | `src/lifecycle/lifecycle.ts` | PluginState FSM + blue-green reload choreography |
| `VersionedDispatcher` | `src/lifecycle/versioned-dispatcher.ts` | Dispatcher wrapper with per-version in-flight counter |
| `drainVersion` | `src/lifecycle/reload.ts` | Bounded drain of old dispatcher version |
| `createContextFactory` | `src/pipeline/pipeline.ts` | Per-request context factory |
| `EventBusPipelineEventsBus` | `src/pipeline/pipeline-events-bus.ts` | Pipeline stage observability events |
| `NodeModuleResolver` / `InMemoryModuleResolver` | `src/module-resolver/module-resolver.ts` | Plugin loading at reload time |
| `HOST_CODES` / `HostError` | `src/error-codes/error-codes.ts` | Runtime error registry |
## File Structure
```
src/
index.ts # Barrel: createApi, registries, lifecycle, pipeline, error codes
app/
create-api.ts # createApi + admin surface
create-api.types.ts # CreateApiConfig, ApiRuntimeAdmin, CapabilityRegistration
capabilities/
capability-registry.ts # CapabilityRegistry
capability-registry.md
error-codes/
error-codes.ts # HOST_CODES + HostError
error-codes.md
host-defaults/
host-defaults.ts # Zero-cost default context implementations
host-defaults.md
lifecycle/
lifecycle.ts # LifecycleDriver FSM
lifecycle.types.ts # LifecycleHost, ReloadOptions
reload.ts # drainVersion (bounded drain)
versioned-dispatcher.ts # VersionedDispatcher
lifecycle.md, reload.md, versioned-dispatcher.md
module-resolver/
module-resolver.ts # NodeModuleResolver, InMemoryModuleResolver
module-resolver.types.ts # ModuleResolver, DynamicImport
module-resolver.md
pipeline/
pipeline.ts # createContextFactory
pipeline.types.ts # ContextFactory, PipelineConfig
pipeline-events-bus.ts # EventBusPipelineEventsBus
pipeline.md, pipeline-events-bus.md
plugins/
plugin-registry.ts # PluginRegistry
plugin-registry.types.ts # Plugin, PluginModule, RegisteredPlugin, RouteRegistrar
plugin-registry.md
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api` | `createApi`, `CapabilityRegistry`, `PluginRegistry`, `LifecycleDriver`, `VersionedDispatcher`, `drainVersion`, `createContextFactory`, `EventBusPipelineEventsBus`, `NodeModuleResolver`, `InMemoryModuleResolver`, `HOST_CODES`, `HostError`, all types |
| `@suaveplan/api/capabilities` | `CapabilityRegistry`, `CapabilityFactory` |
| `@suaveplan/api/plugins` | `PluginRegistry`, plugin types |
| `@suaveplan/api/lifecycle` | `LifecycleDriver`, `VersionedDispatcher`, `drainVersion`, lifecycle types |
| `@suaveplan/api/module-resolver` | `NodeModuleResolver`, `InMemoryModuleResolver`, resolver types |
| `@suaveplan/api/pipeline` | `createContextFactory`, `EventBusPipelineEventsBus`, pipeline types |
| `@suaveplan/api/error-codes` | `HOST_CODES`, `HostError`, `HostCode` |
## Patterns
### Basic runtime
```ts
import { createApi } from "@suaveplan/api";
const runtime = createApi({ plugins: [pingPlugin] });
await runtime.start("ping");
const res = await runtime.fetch(new Request("http://host/ping"));
```
### Blue-green reload
```ts
// Build new dispatcher from updated route set
const next = await runtime.reload({ moduleResolver });
// Atomic swap: runtime.active = next
// Old version drains in-flight requests with timeout
```
### Plugin with capabilities
```ts
const plugin = {
id: "billing",
dependsOn: ["database"],
requiredCapabilities: ["database", "secrets"],
async register(registrar) {
registrar.route("GET", "/invoices", async (ctx) => {
const db = ctx.capabilities.database;
// ...
});
},
};
```
### Custom module resolver for hot reload
```ts
import { NodeModuleResolver } from "@suaveplan/api/module-resolver";
const runtime = createApi({
plugins: [/* ... */],
moduleResolver: new NodeModuleResolver(),
});
```
## Constraints
- **Framework-free** — no framework imported; the runtime IS a WinterCG `fetch` handler.
- **Plugin-unaware dispatcher** — `@suaveplan/api-core` handles HTTP; this package handles plugins.
- **Zero-downtime reload** — atomic dispatcher swap + bounded drain + rollback on failure.
- **Kahn topological sort** — plugin ordering with cycle detection.
- **Dependencies**: `@suaveplan/api-core`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `HOST.*` | Runtime error registry (exact codes in `src/error-codes/error-codes.ts`) |
## Checklist
- [ ] Read `src/app/create-api.ts` before modifying runtime assembly
- [ ] Read `src/lifecycle/lifecycle.ts` before changing FSM or reload behaviour
- [ ] Read `src/plugins/plugin-registry.ts` before modifying plugin registration
- [ ] Preserve framework-free design — never import Hono/Fastify/Express
- [ ] Blue-green reload must remain atomic with bounded drain

View File

@@ -0,0 +1,39 @@
---
name: api-stack-api-db-navigator
description: Navigator for api-stack/api-db. Pick a library to get detailed API guidance.
---
# Api db (api-stack)
Packages in `api-stack/api-db`.
## Libraries
### @suaveplan/api-db-kit
Shared adapter-family kit for the Suaveplan API database capability — the DbError typed error + DB_CODES (12) error vocabulary, Zod boundary schemas (dbParamsSchema / migrationManifestSchema), and the...
→ Skill: `sp-api-db-kit/SKILL.md`
### @suaveplan/api-db-postgres
PostgreSQL Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike over the porsager `postgres` driver that passes the api-capabilities runDatabaseConformance harness against ...
→ Skill: `sp-api-db-postgres/SKILL.md`
### @suaveplan/api-db-sqlite-memory
SQLite Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike + TenantScopedDatabaseLike over @suaveplan/db-sqlite (bun:sqlite) that passes the api-capabilities runDatabaseCo...
→ Skill: `sp-api-db-sqlite-memory/SKILL.md`
### @suaveplan/api-db-surreal
SurrealDB Database capability for the Suaveplan API platform. A faithful AdminDatabaseLike over @suaveplan/surrealdb that passes the api-capabilities runDatabaseConformance harness against a real Surr...
→ Skill: `sp-api-db-surreal/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,93 @@
---
name: sp-api-db-kit
description: Shared adapter-family kit for the Suaveplan API database capability. DbCapError typed error + DB_CAP_CODES (12) error vocabulary, Zod boundary schemas, and the 17-invariant runDbAdapterConformance cross-adapter conformance harness parameterized over a caller-supplied fixture. Imports no concrete adapter. Universal tier.
---
# @suaveplan/api-db-kit
## Purpose
Shared adapter-family kit for the Suaveplan API database capability. Extracts the cross-adapter contract glue — a typed `DbCapError` over a stable `DB_CAP.*` error vocabulary, Zod boundary schemas, and the 17-invariant `runDbAdapterConformance` cross-adapter conformance harness — into one optional, separately-installable package so a third party can ship a new DB adapter depending only on this kit and prove conformance without touching core.
The base DB capability contract (`AdminDatabaseLike`, `TenantScopedDatabaseLike`, `DbParams`, `PreparedStatement`, `DbTransaction`, `MigrationCursor`, `Migration`, `DatabaseEngine`/`DatabaseEngineRegistry`) stays frozen in `@suaveplan/api-types/db`; this kit imports it and contributes only the shared error vocabulary, schemas, and harness.
## Architecture
| Component | Source | Role |
|---|---|---|
| `DB_CAP_CODES` | `src/error-codes/error-codes.ts` | 12-code `DB_CAP.*` registry |
| `DbCapError` | `src/error/db-error.ts` | Typed error extending `SuaveplanError<DbCapCode>` |
| `dbParamsSchema` | `src/schemas/schemas.ts` | Zod validator for DB params input |
| `migrationManifestSchema` | `src/schemas/schemas.ts` | Zod validator for migration manifests |
| `runDbAdapterConformance` | `src/testing/conformance.ts` | 17-invariant cross-adapter conformance harness |
## File Structure
```
src/
index.ts # Barrel: codes, error, schemas, harness types
error-codes/
error-codes.ts # DB_CAP_CODES registry
error/
db-error.ts # DbCapError + options + redaction
schemas/
schemas.ts # Zod boundary schemas
testing/
conformance.ts # runDbAdapterComposition harness
helpers.ts # withFixture, captureThrown, expectDbCapCode
admin-vs-scoped.ts # Admin DDL vs scoped DDL-refusal invariants
tenant-injection.ts # Cross-tenant isolation invariants
migration.ts # Migration drift + idempotency invariants
unsafe-query.ts # Production-mode unsafe-query gating
transaction.ts # Transaction commit/rollback/nested-refusal
prepared.ts # Prepared-statement lifecycle invariants
runtime-limits.ts # Abort / oversized-statement / connect-refused
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-db-kit` | `DB_CAP_CODES`, `DbCapError`, `dbParamsSchema`, `migrationManifestSchema`, `runDbAdapterConformance`, all types |
| `@suaveplan/api-db-kit/error-codes` | `DB_CAP_CODES`, `DbCapCode`, `DB_CAP_ERROR_NAME` |
| `@suaveplan/api-db-kit/error` | `DbCapError`, `DbCapErrorOptions`, `DB_CAP_REDACTED_LABEL` |
| `@suaveplan/api-db-kit/schemas` | `dbParamsSchema`, `migrationManifestSchema`, `migrationManifestListSchema` |
| `@suaveplan/api-db-kit/testing` | `runDbAdapterConformance`, fixture types, helpers |
## Patterns
### Running conformance against an adapter
```ts
import { runDbAdapterConformance } from "@suaveplan/api-db-kit/testing";
import { buildFixture } from "./fixture.js";
runDbAdapterConformance("my-adapter", buildFixture);
```
### Typed DB error handling
```ts
import { DB_CAP_CODES, DbCapError } from "@suaveplan/api-db-kit";
try {
await db.query("SELECT * FROM users");
} catch (err) {
if (err instanceof DbCapError && err.code === DB_CAP_CODES.CONNECTION_REFUSED) {
// handle connection failure
}
}
```
## Constraints
- **Imports no concrete adapter** — the kit depends only on `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`, `tslib`.
- **17 invariants** — admin-vs-scoped, tenant-injection, migration, unsafe-query, transaction, prepared, runtime-limits.
- **Parameterized over fixtures** — `runDbAdapterConformance(label, factory)` takes a caller-supplied fixture.
- **Universal tier** — no `node:*` imports at runtime.
## Checklist
- [ ] Read `src/testing/conformance.ts` before modifying harness behaviour
- [ ] Preserve the kit→variant boundary — never import a concrete adapter
- [ ] New invariants must be added to the harness and proven by at least one adapter

View File

@@ -0,0 +1,111 @@
---
name: sp-api-db-postgres
description: PostgreSQL Database capability for the Suaveplan API platform. Faithful AdminDatabaseLike over the porsager postgres driver with engine-neutral SQL→PostgreSQL translation ($N params, jsonb for typeless columns), positional + named params, prepared statements, transactions, tenant scoping, forward-only migrations runner, and schema-on-install. Passes runDatabaseConformance against real PostgreSQL 16 via testcontainers.
---
# @suaveplan/api-db-postgres
## Purpose
The PostgreSQL `Database` capability for the Suaveplan API platform. `PostgresDatabase` is a faithful `AdminDatabaseLike` over the `postgres` (porsager) driver.
The `Database` capability contract speaks an engine-neutral dialect that strict PostgreSQL does not accept verbatim, so this package carries a real translator that rewrites the dialect (CREATE TABLE / INSERT / SELECT / UPDATE / DELETE, positional `?` + named `:name` params) into PostgreSQL with `$N` parameters. Typeless contract columns are stored as `jsonb` and round-trip arbitrary JSON values losslessly; typed migration columns pass through to native PostgreSQL columns.
It passes the exact `runDatabaseConformance` harness from `@suaveplan/api-capability-kit` that the in-memory `MemoryDatabase` reference passes — proven against a real PostgreSQL 16 container via testcontainers, no mocks.
## Architecture
| Component | Source | Role |
|---|---|---|
| `PostgresDatabase` | `src/database/postgres-database.ts` | `AdminDatabaseLike` implementation |
| `createPostgresDatabase` | `src/database/connect.ts` | Production entry point |
| `translate` | `src/database/sql-translate.ts` | SQL→PostgreSQL dialect translator |
| `sql-parse` | `src/database/sql-parse.ts` | Parsing primitives for translator |
| `runTransaction` | `src/database/transaction.ts` | Reserved-connection transaction runner |
| `createTenantScoped` | `src/database/tenant-scoping.ts` | Admin → tenant-scoped wrapper |
| `PostgresMigrationRunner` | `src/migrations/migration-runner.ts` | Forward-only migration runner |
| `DB_POSTGRES_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: database, migrations, error codes, testing
database/
connect.ts # createPostgresDatabase
postgres-database.ts # PostgresDatabase class
sql-parse.ts # Parsing primitives
sql-translate.ts # SQL→PostgreSQL translator
tenant-scoping.ts # Tenant-scoped wrapper
transaction.ts # Transaction runner
migrations/
migration-runner.ts # PostgresMigrationRunner
error-codes/
error-codes.ts # DB_POSTGRES_CODES + PostgresDatabaseError
testing/
connect.ts # Live-fixture connect helper
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-db-postgres` | `createPostgresDatabase`, `PostgresDatabase`, `PostgresMigrationRunner`, `runPostgresMigrations`, `DB_POSTGRES_CODES`, `PostgresDatabaseError`, testing helpers |
| `@suaveplan/api-db-postgres/database` | `PostgresDatabase`, `createPostgresDatabase`, translator, tenant scoping, transactions |
| `@suaveplan/api-db-postgres/migrations` | `PostgresMigrationRunner`, `runPostgresMigrations` |
| `@suaveplan/api-db-postgres/error-codes` | `DB_POSTGRES_CODES`, `PostgresDatabaseError` |
| `@suaveplan/api-db-postgres/testing` | `connectTestDatabase`, `randomTestSchema` |
## Patterns
### Basic CRUD
```ts
import { createPostgresDatabase } from "@suaveplan/api-db-postgres";
const db = createPostgresDatabase({ url: "postgresql://app:secret@localhost:5432/app" });
await db.execute("CREATE TABLE users (id, name)");
await db.execute("INSERT INTO users (id, name) VALUES (?, ?)", [1, "Ada"]);
const rows = await db.query("SELECT * FROM users WHERE id = ?", [1]);
await db.close();
```
### Schema-on-install
```ts
import { runPluginMigrations } from "@suaveplan/api-migrations/plugin-migrations/plugin-migrations";
await runPluginMigrations({ db }, {
name: "store",
migrations: [
{ name: "001_products", up: "CREATE TABLE products (id text PRIMARY KEY, name text NOT NULL)" },
],
});
```
### Tenant scoping
```ts
const scoped = createTenantScoped(db, "tenant_abc");
```
## Constraints
- **Real PostgreSQL only** — integration tests against PostgreSQL 16 via testcontainers; `describe.skipIf` when Docker unavailable — never mocked.
- **Engine-neutral dialect** — SQL is translated to PostgreSQL; consumers write neutral SQL.
- **jsonb for typeless columns** — contract columns without explicit types store as `jsonb`.
- **Passes same harness as in-memory** — `runDatabaseConformance` from `@suaveplan/api-capability-kit`.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `postgres`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `DB_POSTGRES.*` | PostgreSQL-specific error registry (exact codes in `src/error-codes/error-codes.ts`) |
## Checklist
- [ ] Read `src/database/sql-translate.ts` before modifying SQL translation
- [ ] Read `src/database/postgres-database.ts` before changing DB behaviour
- [ ] Preserve engine-neutral dialect — consumers must not write PostgreSQL-specific SQL
- [ ] Integration tests require Docker; they skip cleanly when unavailable

View File

@@ -0,0 +1,119 @@
---
name: sp-api-db-sqlite-memory
description: SQLite Database capability for the Suaveplan API platform. Faithful AdminDatabaseLike + TenantScopedDatabaseLike over @suaveplan/db-sqlite (bun:sqlite) with native SQL pass-through, positional + named params, prepared statements, real BEGIN IMMEDIATE transactions, WAL + foreign_keys pragmas, in-memory and file-backed modes, tenant scoping, and a lexicographic migrations runner. Passes runDatabaseConformance against real SQLite.
---
# @suaveplan/api-db-sqlite-memory
## Purpose
The SQLite `Database` capability for the Suaveplan API platform. `SqliteDatabase` is a faithful `AdminDatabaseLike` over `@suaveplan/db-sqlite` (`bun:sqlite` on the Bun server). The capability contract speaks engine-neutral SQL, and SQLite accepts it natively — no dialect translation needed.
It passes the exact `runDatabaseConformance` harness from `@suaveplan/api-capability-kit` that the in-memory `MemoryDatabase` reference passes — proven against a real SQLite engine (`:memory:`), no mocks, no container.
## Architecture
| Component | Source | Role |
|---|---|---|
| `SqliteDatabase` | `src/database/sqlite-database.ts` | `AdminDatabaseLike` implementation |
| `connectSqliteDatabase` | `src/database/connect.ts` | Production entry point |
| `createSqliteDatabase` | `src/factory/create-sqlite-database.ts` | Top-level factory returning `{ admin, tenant, close }` |
| `runTransaction` | `src/database/transaction.ts` | `BEGIN IMMEDIATE` transaction runner |
| `createTenantScopedSqliteWrapper` | `src/tenant-scoping/tenant-scoping.ts` | Admin → tenant-scoped wrapper |
| `runSqliteMemoryMigrations` | `src/migrations/migrations.ts` | Lexicographic forward-only migration runner |
| `DB_SQLITE_MEMORY_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: database, factory, migrations, options, telemetry, tenant-scoping, error codes, testing
database/
connect.ts # connectSqliteDatabase
sqlite-database.ts # SqliteDatabase class
transaction.ts # Transaction runner
factory/
create-sqlite-database.ts # createSqliteDatabase
migrations/
migrations.ts # runSqliteMemoryMigrations
options/
sqlite-memory-db-options.ts # Zod schema + pragma/filename resolution
tenant-scoping/
tenant-scoping.ts # Tenant-scoped wrapper + SQL rewriters
telemetry/
telemetry.ts # OTel adapter + migration instruments
error-codes/
error-codes.ts # DB_SQLITE_MEMORY_CODES + DbSqliteMemoryError
testing/
harness.ts # createInMemorySqliteHarness
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-db-sqlite-memory` | `createSqliteDatabase`, `connectSqliteDatabase`, `SqliteDatabase`, `runSqliteMemoryMigrations`, `DB_SQLITE_MEMORY_CODES`, `DbSqliteMemoryError`, testing helpers |
| `@suaveplan/api-db-sqlite-memory/database` | `SqliteDatabase`, `connectSqliteDatabase`, `runTransaction`, `toPositional` |
| `@suaveplan/api-db-sqlite-memory/factory` | `createSqliteDatabase`, `SqliteDatabaseHandle` |
| `@suaveplan/api-db-sqlite-memory/migrations` | `runSqliteMemoryMigrations`, `SqliteMigrationsFsReader` |
| `@suaveplan/api-db-sqlite-memory/options` | Zod schema, `parseSqliteDatabaseOptions`, `DEFAULT_PRAGMAS` |
| `@suaveplan/api-db-sqlite-memory/tenant-scoping` | `createTenantScopedSqliteWrapper`, `sqliteRewriters` |
| `@suaveplan/api-db-sqlite-memory/error-codes` | `DB_SQLITE_MEMORY_CODES`, `DbSqliteMemoryError` |
| `@suaveplan/api-db-sqlite-memory/testing` | `createInMemorySqliteHarness` |
## Patterns
### In-memory database
```ts
import { createSqliteDatabase } from "@suaveplan/api-db-sqlite-memory";
const db = await createSqliteDatabase(); // in-memory, WAL + foreign_keys
await db.admin.execute("CREATE TABLE users (id, tenant_id, name)");
const acme = db.tenant("acme" as TenantId);
await acme.execute("INSERT INTO users (id, name) VALUES (?, ?)", [1, "Ada"]);
const rows = await db.admin.query("SELECT * FROM users");
await db.close();
```
### File-backed database
```ts
const db = await createSqliteDatabase({
inMemory: false,
filePath: "/data/app.sqlite",
});
```
### Custom pragmas
```ts
const db = await createSqliteDatabase({
pragmas: { foreign_keys: "OFF", journal_mode: "DELETE" },
});
```
## Constraints
- **Native SQL pass-through** — no dialect translation needed for SQLite.
- **Default pragmas** — `journal_mode = WAL`, `foreign_keys = ON` at connect time.
- **Real transactions** — `BEGIN IMMEDIATE` with rollback on throw.
- **No container needed** — `:memory:` runs unconditionally under `bun test`.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/db-sqlite`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Error Codes
| Code | Meaning |
|---|---|
| `DB_SQLITE_MEMORY.NOT_CONNECTED` | Operation ran before connect or after close |
| `DB_SQLITE_MEMORY.OPEN_FAILED` | SQLite engine could not open the database |
| `DB_SQLITE_MEMORY.INVALID_OPTIONS` | Zod validation failed |
| `DB_SQLITE_MEMORY.MIGRATION_FAILED` | Migration file threw; transaction rolled back |
| `DB_SQLITE_MEMORY.TENANT_SCOPE_VIOLATION` | Cross-tenant write or unsupported SQL shape |
## Checklist
- [ ] Read `src/database/sqlite-database.ts` before modifying DB behaviour
- [ ] Read `src/tenant-scoping/tenant-scoping.ts` before changing tenant scoping
- [ ] Preserve default pragmas (WAL, foreign_keys=ON) unless explicitly overridden
- [ ] Ensure `runDatabaseConformance` continues to pass

View File

@@ -0,0 +1,94 @@
---
name: sp-api-db-surreal
description: SurrealDB Database capability for the Suaveplan API platform. Faithful AdminDatabaseLike over @suaveplan/surrealdb with engine-neutral SQL→SurrealQL translation, positional + named params, prepared statements, transactions with rollback, tenant scoping, and a forward-only migrations runner. Passes runDatabaseConformance against real SurrealDB v3 via testcontainers.
---
# @suaveplan/api-db-surreal
## Purpose
The SurrealDB `Database` capability for the Suaveplan API platform. `SurrealDatabase` is a faithful `AdminDatabaseLike` over `@suaveplan/surrealdb`. The capability contract speaks engine-neutral SQL, so this package carries a real translator that turns the dialect into SurrealQL.
It passes the exact `runDatabaseConformance` harness from `@suaveplan/api-capability-kit` that the in-memory `MemoryDatabase` reference passes — proven against a real SurrealDB v3 container via testcontainers, no mocks.
## Architecture
| Component | Source | Role |
|---|---|---|
| `SurrealDatabase` | `src/database/surreal-database.ts` | `AdminDatabaseLike` implementation |
| `connectSurrealDatabase` | `src/database/connect.ts` | Production entry point |
| `translate` | `src/database/sql-translate.ts` | SQL→SurrealQL translator |
| `runTransaction` | `src/database/transaction.ts` | Buffered atomic transaction runner |
| `createTenantScoped` | `src/database/tenant-scoping.ts` | Admin → tenant-scoped wrapper |
| `SurrealMigrationRunner` | `src/migrations/migration-runner.ts` | Forward-only migration runner |
| `DB_SURREAL_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: database, migrations, error codes, testing
database/
connect.ts # connectSurrealDatabase
surreal-database.ts # SurrealDatabase class
sql-translate.ts # SQL→SurrealQL translator
tenant-scoping.ts # Tenant-scoped wrapper
transaction.ts # Transaction runner
migrations/
migration-runner.ts # SurrealMigrationRunner
error-codes/
error-codes.ts # DB_SURREAL_CODES + SurrealDatabaseError
testing/
connect.ts # Live-fixture connect helper
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-db-surreal` | `connectSurrealDatabase`, `SurrealDatabase`, `SurrealMigrationRunner`, `runSurrealMigrations`, `DB_SURREAL_CODES`, `SurrealDatabaseError`, testing helpers |
| `@suaveplan/api-db-surreal/database` | `SurrealDatabase`, `connectSurrealDatabase`, translator, tenant scoping, transactions |
| `@suaveplan/api-db-surreal/migrations` | `SurrealMigrationRunner`, `runSurrealMigrations` |
| `@suaveplan/api-db-surreal/error-codes` | `DB_SURREAL_CODES`, `SurrealDatabaseError` |
| `@suaveplan/api-db-surreal/testing` | `connectTestDatabase`, `randomTestNamespace` |
## Patterns
### Basic CRUD
```ts
import { connectSurrealDatabase } from "@suaveplan/api-db-surreal";
const db = await connectSurrealDatabase({
url: "ws://localhost:8000",
namespace: "app",
database: "app",
username: "root",
password: "root",
});
await db.execute("CREATE TABLE users (id, name)");
await db.execute("INSERT INTO users (id, name) VALUES (?, ?)", [1, "Ada"]);
const rows = await db.query("SELECT * FROM users WHERE id = ?", [1]);
await db.close();
```
## Constraints
- **Real SurrealDB only** — integration tests against SurrealDB v3 via testcontainers; `describe.skipIf` when Docker unavailable — never mocked.
- **Engine-neutral dialect** — SQL is translated to SurrealQL; consumers write neutral SQL.
- **Passes same harness as in-memory** — `runDatabaseConformance` from `@suaveplan/api-capability-kit`.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `DB_SURREAL.*` | SurrealDB-specific error registry (exact codes in `src/error-codes/error-codes.ts`) |
## Checklist
- [ ] Read `src/database/sql-translate.ts` before modifying SQL translation
- [ ] Read `src/database/surreal-database.ts` before changing DB behaviour
- [ ] Preserve engine-neutral dialect — consumers must not write SurrealQL-specific SQL
- [ ] Integration tests require Docker; they skip cleanly when unavailable

View File

@@ -0,0 +1,51 @@
---
name: api-stack-api-gateway-navigator
description: Navigator for api-stack/api-gateway. Pick a library to get detailed API guidance.
---
# Api gateway (api-stack)
Packages in `api-stack/api-gateway`.
## Libraries
### @suaveplan/api-gateway
Transport-agnostic, Fetch-native edge gateway core for composing multiple backend targets behind one entry point. Ships a pattern-based Router, a pluggable BalancerRegistry seam (the load-balancer str...
→ Skill: `sp-api-gateway/SKILL.md`
### @suaveplan/api-gateway-balancer-consistent-hash
Consistent-hash load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: select(targets, key) deterministically maps a key to a target via a dependency-free FNV-1...
→ Skill: `sp-api-gateway-balancer-consistent-hash/SKILL.md`
### @suaveplan/api-gateway-balancer-random
Uniform-random load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each select() returns a target chosen uniformly at random from the healthy set via an inje...
→ Skill: `sp-api-gateway-balancer-random/SKILL.md`
### @suaveplan/api-gateway-balancer-round-robin
Round-robin load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each select() returns the next healthy target in registration order via a per-instance counte...
→ Skill: `sp-api-gateway-balancer-round-robin/SKILL.md`
### @suaveplan/api-gateway-balancer-weighted
Weight-proportional load-balancer strategy for @suaveplan/api-gateway. Implements the core ILoadBalancer contract: each select() returns a target with probability proportional to its weight (default 1...
→ Skill: `sp-api-gateway-balancer-weighted/SKILL.md`
### @suaveplan/api-gateway-hono
Optional Hono binding for @suaveplan/api-gateway. gatewayMiddleware(gateway) returns a Hono middleware that reads the raw WHATWG Request off the context and delegates it to the gateway's handle(reques...
→ Skill: `sp-api-gateway-hono/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,74 @@
---
name: sp-api-gateway-balancer-consistent-hash
description: Consistent-hash load-balancer strategy for @suaveplan/api-gateway. select(targets, key) deterministically maps a key to a target via FNV-1a hash modulo the healthy count for sticky affinity. Drop-in variant package — register under "consistent-hash" in a BalancerRegistry.
---
# @suaveplan/api-gateway-balancer-consistent-hash
## Purpose
Consistent-hash load-balancer strategy for `@suaveplan/api-gateway`. `select(targets, key)` deterministically maps `key` to a target via FNV-1a modulo the healthy count, so the same key always selects the same target (sticky affinity).
One package per strategy: the core gateway depends on no concrete strategy and resolves one per route through its `BalancerRegistry`, so you register exactly the strategies you use.
## Architecture
| Component | Source | Role |
|---|---|---|
| `consistentHashBalancer` | `src/consistent-hash/consistent-hash.ts` | `LoadBalancerFactory` implementing the `ILoadBalancer` contract |
## File Structure
```
src/
index.ts # Exports consistentHashBalancer
consistent-hash/
consistent-hash.ts # Strategy implementation
consistent-hash.md # Module docs
consistent-hash.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway-balancer-consistent-hash` | `consistentHashBalancer` |
| `@suaveplan/api-gateway-balancer-consistent-hash/consistent-hash/consistent-hash` | Same from feature subpath |
## Patterns
### Register and use
```ts
import { createBalancerRegistry, createGateway } from "@suaveplan/api-gateway";
import { consistentHashBalancer } from "@suaveplan/api-gateway-balancer-consistent-hash";
const balancers = createBalancerRegistry();
balancers.register("consistent-hash", consistentHashBalancer);
const gateway = createGateway({
balancers,
strategy: "consistent-hash",
routes: [
{
pattern: "/api/*",
targets: [
{ id: "a", url: "https://a.svc" },
{ id: "b", url: "https://b.svc" },
],
},
],
});
```
## Constraints
- **Drop-in variant** — depends only on `@suaveplan/api-gateway` types and `tslib`.
- **Deterministic** — same key always selects the same target.
- **FNV-1a hash** — dependency-free hashing.
- **Dependencies**: `@suaveplan/api-gateway`, `tslib`.
## Checklist
- [ ] Read `src/consistent-hash/consistent-hash.ts` before modifying strategy
- [ ] Preserve deterministic mapping — same key must always return same target

View File

@@ -0,0 +1,75 @@
---
name: sp-api-gateway-balancer-random
description: Uniform-random load-balancer strategy for @suaveplan/api-gateway. select() returns a target chosen uniformly at random from the healthy set via an injectable Rng (defaulting to Math.random). Drop-in variant package — register under "random" in a BalancerRegistry.
---
# @suaveplan/api-gateway-balancer-random
## Purpose
Uniform-random load-balancer strategy for `@suaveplan/api-gateway`. `select()` returns a target chosen uniformly at random from the healthy set, using an injectable `Rng` (defaulting to `Math.random`).
One package per strategy: the core gateway depends on no concrete strategy and resolves one per route through its `BalancerRegistry`, so you register exactly the strategies you use.
## Architecture
| Component | Source | Role |
|---|---|---|
| `randomBalancer` | `src/random/random.ts` | `LoadBalancerFactory` implementing the `ILoadBalancer` contract |
## File Structure
```
src/
index.ts # Exports randomBalancer
random/
random.ts # Strategy implementation
random.md # Module docs
random.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway-balancer-random` | `randomBalancer` |
| `@suaveplan/api-gateway-balancer-random/random/random` | Same from feature subpath |
## Patterns
### Register and use
```ts
import { createBalancerRegistry, createGateway } from "@suaveplan/api-gateway";
import { randomBalancer } from "@suaveplan/api-gateway-balancer-random";
const balancers = createBalancerRegistry();
balancers.register("random", randomBalancer);
const gateway = createGateway({
balancers,
strategy: "random",
routes: [
{
pattern: "/api/*",
targets: [
{ id: "a", url: "https://a.svc" },
{ id: "b", url: "https://b.svc" },
],
},
],
});
```
## Constraints
- **Drop-in variant** — depends only on `@suaveplan/api-gateway` types, `@suaveplan/types`, and `tslib`.
- **Injectable RNG** — accepts a custom `Rng` for deterministic tests.
- **Uniform distribution** — every healthy target has equal selection probability.
- **Dependencies**: `@suaveplan/api-gateway`, `@suaveplan/types`, `tslib`.
## Checklist
- [ ] Read `src/random/random.ts` before modifying strategy
- [ ] Preserve uniform distribution across healthy targets
- [ ] Injectable RNG must remain configurable for test determinism

View File

@@ -0,0 +1,74 @@
---
name: sp-api-gateway-balancer-round-robin
description: Round-robin load-balancer strategy for @suaveplan/api-gateway. select() returns the next healthy target in registration order via a per-instance counter, distributing evenly (within ±1). Drop-in variant package — register under "round-robin" in a BalancerRegistry.
---
# @suaveplan/api-gateway-balancer-round-robin
## Purpose
Round-robin load-balancer strategy for `@suaveplan/api-gateway`. `select()` returns the next selectable target in registration order via a per-instance counter, distributing evenly (within ±1) across the healthy set.
One package per strategy: the core gateway depends on no concrete strategy and resolves one per route through its `BalancerRegistry`, so you register exactly the strategies you use.
## Architecture
| Component | Source | Role |
|---|---|---|
| `roundRobinBalancer` | `src/round-robin/round-robin.ts` | `LoadBalancerFactory` implementing the `ILoadBalancer` contract |
## File Structure
```
src/
index.ts # Exports roundRobinBalancer
round-robin/
round-robin.ts # Strategy implementation
round-robin.md # Module docs
round-robin.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway-balancer-round-robin` | `roundRobinBalancer` |
| `@suaveplan/api-gateway-balancer-round-robin/round-robin/round-robin` | Same from feature subpath |
## Patterns
### Register and use
```ts
import { createBalancerRegistry, createGateway } from "@suaveplan/api-gateway";
import { roundRobinBalancer } from "@suaveplan/api-gateway-balancer-round-robin";
const balancers = createBalancerRegistry();
balancers.register("round-robin", roundRobinBalancer);
const gateway = createGateway({
balancers,
routes: [
{
pattern: "/api/*",
targets: [
{ id: "a", url: "https://a.svc" },
{ id: "b", url: "https://b.svc" },
],
},
],
});
```
## Constraints
- **Drop-in variant** — depends only on `@suaveplan/api-gateway` types and `tslib`.
- **Even distribution** — targets receive requests within ±1 of each other.
- **Per-instance counter** — each balancer instance maintains its own position.
- **Dependencies**: `@suaveplan/api-gateway`, `tslib`.
## Checklist
- [ ] Read `src/round-robin/round-robin.ts` before modifying strategy
- [ ] Preserve even distribution semantics
- [ ] Ensure counter wraps correctly across the healthy set

View File

@@ -0,0 +1,75 @@
---
name: sp-api-gateway-balancer-weighted
description: Weight-proportional load-balancer strategy for @suaveplan/api-gateway. select() returns a target with probability proportional to its weight (default 1) via a cumulative-weight walk over an injectable Rng. Drop-in variant package — register under "weighted" in a BalancerRegistry.
---
# @suaveplan/api-gateway-balancer-weighted
## Purpose
Weight-proportional load-balancer strategy for `@suaveplan/api-gateway`. `select()` returns a target with probability proportional to its `weight` (default `1`), using an injectable `Rng` (defaulting to `Math.random`).
One package per strategy: the core gateway depends on no concrete strategy and resolves one per route through its `BalancerRegistry`, so you register exactly the strategies you use.
## Architecture
| Component | Source | Role |
|---|---|---|
| `weightedBalancer` | `src/weighted/weighted.ts` | `LoadBalancerFactory` implementing the `ILoadBalancer` contract |
## File Structure
```
src/
index.ts # Exports weightedBalancer
weighted/
weighted.ts # Strategy implementation
weighted.md # Module docs
weighted.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway-balancer-weighted` | `weightedBalancer` |
| `@suaveplan/api-gateway-balancer-weighted/weighted/weighted` | Same from feature subpath |
## Patterns
### Register and use
```ts
import { createBalancerRegistry, createGateway } from "@suaveplan/api-gateway";
import { weightedBalancer } from "@suaveplan/api-gateway-balancer-weighted";
const balancers = createBalancerRegistry();
balancers.register("weighted", weightedBalancer);
const gateway = createGateway({
balancers,
strategy: "weighted",
routes: [
{
pattern: "/api/*",
targets: [
{ id: "a", url: "https://a.svc", weight: 3 },
{ id: "b", url: "https://b.svc", weight: 1 },
],
},
],
});
```
## Constraints
- **Drop-in variant** — depends only on `@suaveplan/api-gateway` types, `@suaveplan/types`, and `tslib`.
- **Cumulative-weight walk** — probability proportional to target weight.
- **Injectable RNG** — accepts a custom `Rng` for deterministic tests.
- **Dependencies**: `@suaveplan/api-gateway`, `@suaveplan/types`, `tslib`.
## Checklist
- [ ] Read `src/weighted/weighted.ts` before modifying strategy
- [ ] Preserve weight-proportional selection
- [ ] Injectable RNG must remain configurable for test determinism

View File

@@ -0,0 +1,65 @@
---
name: sp-api-gateway-hono
description: Optional Hono binding for @suaveplan/api-gateway. gatewayMiddleware(gateway) returns a terminal Hono middleware that delegates the raw WHATWG Request to the gateway's handle(request) and returns the upstream Response directly. hono is an optional peer dependency typed structurally — never imported — keeping the core gateway framework-free.
---
# @suaveplan/api-gateway-hono
## Purpose
Optional Hono binding for `@suaveplan/api-gateway`. `gatewayMiddleware(gateway)` returns a Hono middleware that reads the raw WHATYG `Request` off the context and delegates it to the gateway's `handle(request)`, returning the upstream `Response` directly — a terminal, translation-free pass-through.
`hono` is an OPTIONAL peer dependency and is never imported by this package (the context is typed structurally), so the core gateway graph stays framework-free — mirroring how `@suaveplan/api-adapters` treats frameworks as guests, not hosts.
## Architecture
| Component | Source | Role |
|---|---|---|
| `gatewayMiddleware` | `src/hono/hono.ts` | Factory returning a terminal Hono middleware |
| `HonoContextLike` | `src/hono/hono.types.ts` | Structural Hono context type |
| `GatewayMiddlewareHandler` | `src/hono/hono.types.ts` | Middleware handler type |
## File Structure
```
src/
index.ts # Exports gatewayMiddleware + types
hono/
hono.ts # gatewayMiddleware implementation
hono.types.ts # Structural context types
hono.md # Module docs
hono.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway-hono` | `gatewayMiddleware`, `HonoContextLike`, `GatewayMiddlewareHandler` |
| `@suaveplan/api-gateway-hono/hono/hono` | Same from feature subpath |
## Patterns
### Mount gateway in Hono
```ts
import { Hono } from "hono";
import { gatewayMiddleware } from "@suaveplan/api-gateway-hono";
const app = new Hono();
app.use("/api/*", gatewayMiddleware(gateway));
```
## Constraints
- **Terminal middleware** — returns the gateway response without calling `next()`, so later handlers for that route never run.
- **Structural typing** — never imports `hono`; the context is typed structurally.
- **Optional peer** — `hono` is optional; the package works only when Hono is present.
- **Framework-free core** — the gateway itself knows nothing about Hono; this is a thin binding layer.
- **Dependencies**: `@suaveplan/api-gateway`, `tslib`. Peer: `hono` (optional).
## Checklist
- [ ] Read `src/hono/hono.ts` before modifying the middleware
- [ ] Preserve structural typing — never import `hono` directly
- [ ] Preserve terminal middleware semantics

View File

@@ -0,0 +1,140 @@
---
name: sp-api-gateway
description: Transport-agnostic, Fetch-native edge gateway core for composing multiple backend targets behind one entry point. Pattern-based router, pluggable BalancerRegistry seam, request/response transformer pipeline, circuit breaker, active health checker, per-route retry via @suaveplan/retry, edge rate-limit guard via @suaveplan/rate-limiter, and OTel instrumentation. Frameworks and load-balancer strategies are guests, not hosts.
---
# @suaveplan/api-gateway
## Purpose
A transport-agnostic, Fetch-native edge gateway core for composing multiple backend targets behind one entry point. Ships a pattern-based router, a pluggable load-balancer **registry** seam, a request/response transformer pipeline, a closed/open/half-open circuit breaker, an active health checker, per-route retry via `@suaveplan/retry`, and an edge rate-limit guard built on the platform `@suaveplan/rate-limiter` primitives — all behind one `Gateway.handle(request: Request): Promise<Response>` call.
The core imports no framework and no concrete load-balancer strategy, mirroring the Suaveplan API platform's "frameworks are guests, not hosts" model.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createGateway` / `Gateway` | `src/gateway/gateway.ts` | Orchestrator composing router, balancer, transformer, circuit breaker, health, fetcher, rate-limit |
| `Router` | `src/router/router.ts` | Pattern-based request matcher |
| `createBalancerRegistry` | `src/balancer-registry/balancer-registry.ts` | Pluggable load-balancer registry seam |
| `TransformerPipeline` | `src/transformer/transformer.ts` | Request/response rewrite pipeline |
| `GatewayCircuitBreaker` | `src/circuit-breaker/circuit-breaker.ts` | Closed → open → half-open → closed failure-isolation FSM |
| `IntervalHealthChecker` | `src/health/health.ts` | Active upstream-health probe |
| `createRateLimitGuard` | `src/rate-limit/rate-limit.ts` | Edge rate-limit guard |
| `createFetcher` | `src/fetcher/fetcher.ts` | Outbound HTTP port |
| `GATEWAY_CODES` / `GatewayError` | `src/error-codes/error-codes.ts` | Error registry |
## File Structure
```
src/
index.ts # Barrel: all gateway exports
gateway/
gateway.ts # createGateway + Gateway orchestrator
route-runtime.ts # compileRoute: RouteDefinition → per-route runtime
retry-policy.ts # resolveRetry: RetryPolicy → executable pieces
router/
router.ts # Pattern-based request router
balancer-registry/
balancer-registry.ts # Pluggable load-balancer registry
transformer/
transformer.ts # TransformerPipeline + built-in transformers
circuit-breaker/
circuit-breaker.ts # GatewayCircuitBreaker FSM
health/
health.ts # IntervalHealthChecker
rate-limit/
rate-limit.ts # createRateLimitGuard
fetcher/
fetcher.ts # createFetcher (outbound HTTP)
error-codes/
error-codes.ts # GATEWAY_CODES + GatewayError
schemas/
schemas.ts # Zod boundary schemas for GatewayConfig
otel/
instruments.ts # OTel counters + histogram
types/
common.ts # Shared type contracts
constants/
index.ts # Default thresholds and status tokens
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-gateway` | `createGateway`, `Gateway`, `Router`, `createBalancerRegistry`, `TransformerPipeline`, `GatewayCircuitBreaker`, `IntervalHealthChecker`, `createRateLimitGuard`, `createFetcher`, `GATEWAY_CODES`, `GatewayError`, all types |
| `@suaveplan/api-gateway/gateway/gateway` | `createGateway`, `Gateway`, `compileRoute`, `resolveRetry` |
| `@suaveplan/api-gateway/router/router` | `Router` |
| `@suaveplan/api-gateway/balancer-registry/balancer-registry` | `createBalancerRegistry`, `BalancerRegistry`, `LoadBalancerFactory` |
| `@suaveplan/api-gateway/transformer/transformer` | `TransformerPipeline`, `addHeaders`, `removeHeaders`, `rewritePath`, `stripPathPrefix`, etc. |
| `@suaveplan/api-gateway/circuit-breaker/circuit-breaker` | `GatewayCircuitBreaker`, `CircuitBreakerOptions` |
| `@suaveplan/api-gateway/health/health` | `IntervalHealthChecker`, `HealthCheckerOptions` |
| `@suaveplan/api-gateway/rate-limit/rate-limit` | `createRateLimitGuard`, `RateLimitGuardOptions` |
| `@suaveplan/api-gateway/fetcher/fetcher` | `createFetcher`, `FetcherOptions` |
| `@suaveplan/api-gateway/error-codes` | `GATEWAY_CODES`, `GatewayError` |
| `@suaveplan/api-gateway/schemas` | `GatewayConfigSchema`, `RouteDefinitionSchema`, etc. |
| `@suaveplan/api-gateway/otel/instruments` | `getGatewayInstruments`, `resetGatewayInstruments` |
## Patterns
### Standalone gateway
```ts
import { createBalancerRegistry, createGateway, createRateLimitGuard } from "@suaveplan/api-gateway";
import { roundRobinBalancer } from "@suaveplan/api-gateway-balancer-round-robin";
const balancers = createBalancerRegistry();
balancers.register("round-robin", roundRobinBalancer);
const gateway = createGateway({
balancers,
strategy: "round-robin",
circuitBreaker: { failureThreshold: 3, resetTimeout: 10_000 },
rateLimit: createRateLimitGuard({ limit: 1000, windowMs: 60_000 }),
routes: [
{
pattern: "/api/*",
targets: [
{ id: "a", url: "https://a.svc", weight: 3 },
{ id: "b", url: "https://b.svc", weight: 1 },
],
retry: { attempts: 3, mode: "exponential", jitter: true },
},
],
});
export default { fetch: (req: Request) => gateway.handle(req) };
```
### Hono binding (separate package)
```ts
import { Hono } from "hono";
import { gatewayMiddleware } from "@suaveplan/api-gateway-hono";
const app = new Hono();
app.use("/api/*", gatewayMiddleware(gateway));
```
## Constraints
- **Framework-free** — no framework imported; speaks only WHATYG `fetch(Request) => Promise<Response>`.
- **Strategy-agnostic** — core depends on no concrete load-balancer; strategies are sibling packages wired via `BalancerRegistry`.
- **Reuses platform primitives** — `@suaveplan/retry` for per-route retry, `@suaveplan/rate-limiter` for edge rate-limiting.
- **Dependencies**: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/rate-limiter`, `@suaveplan/retry`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Error Codes
| Code | Meaning |
|---|---|
| `GATEWAY.*` | Gateway error registry (exact codes in `src/error-codes/error-codes.ts`) |
## Checklist
- [ ] Read `src/gateway/gateway.ts` before modifying orchestration logic
- [ ] Read `src/router/router.ts` before changing route matching
- [ ] Read `src/circuit-breaker/circuit-breaker.ts` before modifying circuit breaker FSM
- [ ] Preserve framework-free design — never import Hono/Express/Fastify
- [ ] Preserve strategy-agnostic design — never depend on concrete balancer

View File

@@ -0,0 +1,51 @@
---
name: api-stack-api-graphql-navigator
description: Navigator for api-stack/api-graphql. Pick a library to get detailed API guidance.
---
# Api graphql (api-stack)
Packages in `api-stack/api-graphql`.
## Libraries
### @suaveplan/api-graphql
GraphQL-over-HTTP meta-package for the Fetch-native Suaveplan API platform. Builds an executable schema from a GraphqlRegistry (SDL + resolvers) or a pre-built GraphQLSchema and emits RouteDescriptor[...
→ Skill: `sp-api-graphql/SKILL.md`
### @suaveplan/api-graphql-apollo
Optional Apollo-Server-backed GraphQL engine for the Fetch-native Suaveplan API platform. createApolloRoutes(source, opts?) exposes the same route-producing API as the default Yoga engine but each req...
→ Skill: `sp-api-graphql-apollo/SKILL.md`
### @suaveplan/api-graphql-core
Cycle-breaking foundation shared by the Fetch-native Suaveplan GraphQL engine and IDE packages. Owns the registry runtime implementing the frozen api-types GraphqlRegistry contract (MutableGraphqlRegi...
→ Skill: `sp-api-graphql-core/SKILL.md`
### @suaveplan/api-graphql-ui-altair
Altair GraphQL IDE renderer for the Fetch-native Suaveplan API platform. createAltairRoutes(opts) returns a single GET RouteDescriptor serving an Altair HTML shell loaded from a pinned CDN version (AL...
→ Skill: `sp-api-graphql-ui-altair/SKILL.md`
### @suaveplan/api-graphql-ui-graphiql
GraphiQL IDE renderer for the Fetch-native Suaveplan API platform. createGraphiqlRoutes(opts) returns a single GET RouteDescriptor serving a GraphiQL HTML shell loaded from a pinned CDN version (GRAPH...
→ Skill: `sp-api-graphql-ui-graphiql/SKILL.md`
### @suaveplan/api-graphql-yoga
Default GraphQL engine for the Fetch-native Suaveplan API platform. createYogaRoutes(source, opts?) builds an executable schema from a GraphqlRegistry (SDL + resolvers) or a pre-built GraphQLSchema an...
→ Skill: `sp-api-graphql-yoga/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,93 @@
---
name: sp-api-graphql-apollo
description: Optional Apollo-Server-backed GraphQL engine for the Fetch-native Suaveplan API platform. createApolloRoutes(source, opts?) produces GET+POST RouteDescriptors served by a real @apollo/server ApolloServer via executeHTTPGraphQLRequest. @apollo/server is an optional peer imported lazily; missing peer raises API_GRAPHQL.APOLLO_PEER_MISSING. Never depends on the sibling Yoga engine.
---
# @suaveplan/api-graphql-apollo
## Purpose
Optional Apollo-Server-backed GraphQL engine for the Fetch-native Suaveplan API platform. `createApolloRoutes(source, opts?)` exposes the exact same route-producing API as the default Yoga engine — it builds an executable schema from a `GraphqlRegistry` or a `GraphQLSchema` and returns GET + POST `RouteDescriptor`s — but each request is served by a real `@apollo/server` `ApolloServer` via its `executeHTTPGraphQLRequest` entry point.
`@apollo/server` is an **optional peer dependency**: imported lazily on the first request so an app that uses only the default Yoga path carries none of Apollo's weight. A missing peer raises `API_GRAPHQL.APOLLO_PEER_MISSING` (HTTP 501).
## Architecture
| Component | Source | Role |
|---|---|---|
| `createApolloRoutes` | `src/apollo/apollo.ts` | Route factory producing GET+POST RouteDescriptors |
| `loadApollo` | `src/apollo/apollo.ts` | Lazy Apollo module loader |
| `apollo-bridge` | `src/apollo-bridge/apollo-bridge.ts` | WHATWG Request ⇄ Apollo HTTPGraphQLRequest translation |
## File Structure
```
src/
index.ts # Exports createApolloRoutes, loadApollo, types, bridge
apollo/
apollo.ts # createApolloRoutes + loadApollo
apollo.md # Module docs
apollo.test.ts
apollo-bridge/
apollo-bridge.ts # Request/Response translation layer
apollo-bridge.md
apollo-bridge.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql-apollo` | `createApolloRoutes`, `loadApollo`, `ApolloRouteOptions`, `ApolloImporter`, bridge types |
| `@suaveplan/api-graphql-apollo/apollo` | Same from feature subpath |
## Patterns
### Using Apollo engine
```ts
import { collectRegistry } from "@suaveplan/api-graphql-core";
import { createApolloRoutes } from "@suaveplan/api-graphql-apollo";
import { definePlugin } from "@suaveplan/api-plugin-kit";
const source = collectRegistry((registry) => {
registry.addTypeDefs("type Query { hello(name: String): String }");
registry.addResolvers({
Query: { hello: (_p, a: { name?: string }) => `hi ${a.name ?? "world"}` },
});
});
const plugin = definePlugin({
id: "graphql-apollo-app",
version: "0.1.0",
routes(router) {
for (const route of createApolloRoutes(source, { path: "/graphql" })) {
router[route.method.toLowerCase() as "get" | "post"](route.path, route.handler);
}
},
});
```
### Custom Apollo importer (testing)
```ts
const routes = createApolloRoutes(source, {
path: "/graphql",
importer: async () => import("@apollo/server"),
});
```
## Constraints
- **Optional peer** — `@apollo/server` is optional; missing peer raises `API_GRAPHQL.APOLLO_PEER_MISSING`.
- **Lazy-loaded** — Apollo is imported on the first request, not at module load.
- **Never depends on Yoga** — depends only on `@suaveplan/api-graphql-core` and `@suaveplan/api-types`.
- **Same signature as Yoga** — `createApolloRoutes` has the same API as `createYogaRoutes`.
- **Dependencies**: `@suaveplan/api-graphql-core`, `@suaveplan/api-types`, `graphql`, `tslib`. Peer: `@apollo/server` (optional).
## Checklist
- [ ] Read `src/apollo/apollo.ts` before modifying route factory
- [ ] Read `src/apollo-bridge/apollo-bridge.ts` before changing request translation
- [ ] Preserve lazy-loading — never eagerly import `@apollo/server`
- [ ] Preserve optional peer semantics — missing peer must raise `API_GRAPHQL.APOLLO_PEER_MISSING`

View File

@@ -0,0 +1,109 @@
---
name: sp-api-graphql-core
description: Cycle-breaking foundation shared by the Fetch-native Suaveplan GraphQL engine and IDE packages. Owns the registry runtime (MutableGraphqlRegistry / collectRegistry / buildSchema), the API_GRAPHQL.* error-code registry + GraphqlError, the shared engine route-factory contract, and the shared IDE-shell route plumbing. Engines and IDE renderers depend on this neutral foundation instead of each other.
---
# @suaveplan/api-graphql-core
## Purpose
Cycle-breaking foundation shared by the Fetch-native Suaveplan GraphQL engine and IDE packages. It owns four things they need but none can own without depending on the others:
1. **Registry runtime** — `MutableGraphqlRegistry` / `collectRegistry` / `buildSchema` — implementing the frozen `@suaveplan/api-types` `GraphqlRegistry` contract and turning SDL-plus-resolvers into an executable `GraphQLSchema`.
2. **`API_GRAPHQL.*` error registry** — `GRAPHQL_CODES`, `GraphqlError`, `DEFAULT_STATUS_BY_CODE` — every engine throws these on schema/peer/execution failure.
3. **Shared engine contract** — `EngineRouteOptions`, `GraphqlExecutionContext`, `DEFAULT_GRAPHQL_PATH` — the option bundle and types every engine route factory speaks.
4. **Shared IDE-shell route plumbing** — `UiRouteOptions`, `escapeJsString`, `htmlRoute` on the `./ui-route` subpath — the option bundle and helpers every GraphQL IDE renderer consumes.
`@suaveplan/api-graphql-yoga` and `@suaveplan/api-graphql-apollo` (the engines) plus `@suaveplan/api-graphql-ui-graphiql` and `@suaveplan/api-graphql-ui-altair` (the IDE renderers) depend on this neutral package instead of on each other, so no dependency cycle forms.
## Architecture
| Component | Source | Role |
|---|---|---|
| `collectRegistry` / `MutableGraphqlRegistry` | `src/registry/registry.ts` | Registry runtime implementing the `GraphqlRegistry` contract |
| `buildSchema` | `src/registry/build-schema.ts` | Source/registry/schema → executable `GraphQLSchema` |
| `GRAPHQL_CODES` / `GraphqlError` | `src/error-codes/error-codes.ts` | Error registry + typed error class |
| `EngineRouteOptions` / `GraphqlExecutionContext` | `src/engine-types/engine-types.ts` | Shared engine contract |
| `htmlRoute` / `escapeJsString` | `src/ui-route/ui-route.ts` | Shared IDE-shell route plumbing |
## File Structure
```
src/
index.ts # Barrel: registry, error codes, engine types, UI route
registry/
registry.ts # MutableGraphqlRegistry + collectRegistry
build-schema.ts # buildSchema
registry.types.ts # Resolver, ResolverMap, LoaderFactory, etc.
registry.md, build-schema.md
error-codes/
error-codes.ts # GRAPHQL_CODES + GraphqlError
error-codes.md
engine-types/
engine-types.ts # EngineRouteOptions, GraphqlExecutionContext, DEFAULT_GRAPHQL_PATH
engine-types.md
ui-route/
ui-route.ts # htmlRoute, escapeJsString, UiRouteOptions
ui-route.md
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql-core` | `collectRegistry`, `buildSchema`, `MutableGraphqlRegistry`, `GRAPHQL_CODES`, `GraphqlError`, `DEFAULT_GRAPHQL_PATH`, `EngineRouteOptions`, `htmlRoute`, `escapeJsString`, all types |
| `@suaveplan/api-graphql-core/registry` | `collectRegistry`, `buildSchema`, `MutableGraphqlRegistry`, registry types |
| `@suaveplan/api-graphql-core/engine-types` | `EngineRouteOptions`, `GraphqlExecutionContext`, `DEFAULT_GRAPHQL_PATH` |
| `@suaveplan/api-graphql-core/ui-route` | `htmlRoute`, `escapeJsString`, `UiRouteOptions` |
| `@suaveplan/api-graphql-core/error-codes` | `GRAPHQL_CODES`, `GraphqlError`, `GraphqlErrorCode`, `DEFAULT_STATUS_BY_CODE` |
## Patterns
### Building a schema from SDL
```ts
import { collectRegistry, buildSchema } from "@suaveplan/api-graphql-core";
const source = collectRegistry((registry) => {
registry.addTypeDefs("type Query { hello(name: String): String }");
registry.addResolvers({
Query: { hello: (_p, a: { name?: string }) => `hi ${a.name ?? "world"}` },
});
});
const schema = buildSchema(source);
```
### Error handling
```ts
import { GRAPHQL_CODES, GraphqlError } from "@suaveplan/api-graphql-core";
throw new GraphqlError(GRAPHQL_CODES.SCHEMA_INVALID, "Type Query is missing");
```
### IDE route plumbing
```ts
import { htmlRoute, escapeJsString } from "@suaveplan/api-graphql-core/ui-route";
const route = htmlRoute({
path: "/graphql/ide",
title: "GraphiQL",
// ...
});
```
## Constraints
- **Cycle-breaker** — no dependency on any engine or IDE renderer package.
- **Real GraphQL schema construction** — uses `@graphql-tools/schema`; no mocks in tests.
- **100% coverage** — unit tests co-located, coverage gated at 100%.
- **Dependencies**: `@graphql-tools/schema`, `@suaveplan/api-core`, `@suaveplan/api-types`, `@suaveplan/error`, `graphql`, `tslib`.
## Checklist
- [ ] Read `src/registry/registry.ts` before modifying registry behaviour
- [ ] Read `src/registry/build-schema.ts` before changing schema construction
- [ ] Preserve cycle-breaking — never import an engine or IDE renderer package
- [ ] Keep in sync with `@suaveplan/api-types/graphql` contract

View File

@@ -0,0 +1,71 @@
---
name: sp-api-graphql-ui-altair
description: Altair GraphQL IDE renderer for the Fetch-native Suaveplan API platform. createAltairRoutes(opts) returns a single GET RouteDescriptor serving an Altair HTML shell loaded from a pinned CDN version and initialised against the configured GraphQL endpoint. Depends on @suaveplan/api-graphql-core/ui-route for shared plumbing; never on the api-graphql meta-package.
---
# @suaveplan/api-graphql-ui-altair
## Purpose
Altair GraphQL IDE renderer for the Fetch-native Suaveplan API platform. `createAltairRoutes(opts)` returns a single GET `RouteDescriptor` whose handler responds with an HTML document that loads Altair from a pinned CDN version (`ALTAIR_VERSION`) and initialises it against the configured GraphQL endpoint.
The IDE shell is independent of the engine and the data route: the host decides whether to expose Altair, GraphiQL, both, or neither, and at which paths and under which auth policy. The shared `UiRouteOptions` contract plus the `escapeJsString` / `htmlRoute` route plumbing live in `@suaveplan/api-graphql-core` on the `./ui-route` subpath; this renderer depends on that neutral foundation and never on the `@suaveplan/api-graphql` meta-package, so no dependency cycle forms.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAltairRoutes` | `src/altair/altair.ts` | Factory returning a GET RouteDescriptor for the Altair HTML shell |
| `ALTAIR_VERSION` | `src/altair/altair.ts` | Pinned CDN version constant |
## File Structure
```
src/
index.ts # Exports createAltairRoutes, ALTAIR_VERSION, UiRouteOptions
altair/
altair.ts # Altair HTML shell renderer
altair.md # Module docs
altair.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql-ui-altair` | `createAltairRoutes`, `ALTAIR_VERSION`, `UiRouteOptions` |
| `@suaveplan/api-graphql-ui-altair/altair` | Same from feature subpath |
## Patterns
### Serve Altair IDE
```ts
import { createApi } from "@suaveplan/api";
import { createAltairRoutes } from "@suaveplan/api-graphql-ui-altair";
import { definePlugin } from "@suaveplan/api-plugin-kit";
const plugin = definePlugin({
id: "graphql-ide",
version: "0.1.0",
routes(router) {
for (const route of createAltairRoutes({ path: "/altair", endpoint: "/graphql" })) {
router[route.method.toLowerCase() as "get"](route.path, route.handler);
}
},
});
```
## Constraints
- **CDN-loaded** — Altair is loaded from a pinned CDN version; no client assets bundled.
- **Independent of engine** — works with Yoga, Apollo, or any other GraphQL engine.
- **Cycle-free** — depends only on `@suaveplan/api-graphql-core/ui-route` and `@suaveplan/api-types`, never on the meta-package.
- **100% coverage** — served document asserted byte-by-byte (endpoint escaping, pinned version, custom title).
- **Dependencies**: `@suaveplan/api-graphql-core`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/altair/altair.ts` before modifying the HTML shell
- [ ] Preserve CDN version pinning — update `ALTAIR_VERSION` when bumping
- [ ] Ensure endpoint escaping is safe against `</script>` injection

View File

@@ -0,0 +1,71 @@
---
name: sp-api-graphql-ui-graphiql
description: GraphiQL IDE renderer for the Fetch-native Suaveplan API platform. createGraphiqlRoutes(opts) returns a single GET RouteDescriptor serving a GraphiQL HTML shell loaded from a pinned CDN version whose fetcher is wired to the configured GraphQL endpoint. Depends on @suaveplan/api-graphql-core/ui-route for shared plumbing; never on the api-graphql meta-package.
---
# @suaveplan/api-graphql-ui-graphiql
## Purpose
GraphiQL IDE renderer for the Fetch-native Suaveplan API platform. `createGraphiqlRoutes(opts)` returns a single GET `RouteDescriptor` whose handler responds with an HTML document that loads GraphiQL from a pinned CDN version (`GRAPHIQL_VERSION`) and wires its fetcher to the configured GraphQL endpoint.
The IDE shell is independent of the engine and the data route: the host decides whether to expose GraphiQL, Altair, both, or neither, and at which paths and under which auth policy. The shared `UiRouteOptions` contract plus the `escapeJsString` / `htmlRoute` route plumbing live in `@suaveplan/api-graphql-core` on the `./ui-route` subpath; this renderer depends on that neutral foundation and never on the `@suaveplan/api-graphql` meta-package, so no dependency cycle forms.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createGraphiqlRoutes` | `src/graphiql/graphiql.ts` | Factory returning a GET RouteDescriptor for the GraphiQL HTML shell |
| `GRAPHIQL_VERSION` | `src/graphiql/graphiql.ts` | Pinned CDN version constant |
## File Structure
```
src/
index.ts # Exports createGraphiqlRoutes, GRAPHIQL_VERSION, UiRouteOptions
graphiql/
graphiql.ts # GraphiQL HTML shell renderer
graphiql.md # Module docs
graphiql.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql-ui-graphiql` | `createGraphiqlRoutes`, `GRAPHIQL_VERSION`, `UiRouteOptions` |
| `@suaveplan/api-graphql-ui-graphiql/graphiql` | Same from feature subpath |
## Patterns
### Serve GraphiQL IDE
```ts
import { createApi } from "@suaveplan/api";
import { createGraphiqlRoutes } from "@suaveplan/api-graphql-ui-graphiql";
import { definePlugin } from "@suaveplan/api-plugin-kit";
const plugin = definePlugin({
id: "graphql-ide",
version: "0.1.0",
routes(router) {
for (const route of createGraphiqlRoutes({ path: "/graphiql", endpoint: "/graphql" })) {
router[route.method.toLowerCase() as "get"](route.path, route.handler);
}
},
});
```
## Constraints
- **CDN-loaded** — GraphiQL is loaded from a pinned CDN version; no client assets bundled.
- **Independent of engine** — works with Yoga, Apollo, or any other GraphQL engine.
- **Cycle-free** — depends only on `@suaveplan/api-graphql-core/ui-route` and `@suaveplan/api-types`, never on the meta-package.
- **100% coverage** — served document asserted byte-by-byte (endpoint escaping against `</script>` payload, pinned version, custom title).
- **Dependencies**: `@suaveplan/api-graphql-core`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/graphiql/graphiql.ts` before modifying the HTML shell
- [ ] Preserve CDN version pinning — update `GRAPHIQL_VERSION` when bumping
- [ ] Ensure endpoint escaping is safe against `</script>` injection

View File

@@ -0,0 +1,78 @@
---
name: sp-api-graphql-yoga
description: Default GraphQL engine for the Fetch-native Suaveplan API platform. createYogaRoutes(source, opts?) builds an executable schema from a GraphqlRegistry or pre-built GraphQLSchema and returns a GET + POST RouteDescriptor pair whose handlers delegate to a real graphql-yoga fetch handler. Yoga IS a WinterCG (Request) => Response handler, so GraphQL mounts as plain Fetch routes. Never depends on the sibling Apollo engine.
---
# @suaveplan/api-graphql-yoga
## Purpose
Default GraphQL engine for the Fetch-native Suaveplan API platform. `createYogaRoutes(source, opts?)` builds an executable schema from a `GraphqlRegistry` (SDL + resolvers) or a pre-built `GraphQLSchema` and returns a GET + POST `RouteDescriptor` pair whose handlers delegate to a real `graphql-yoga` instance.
A `graphql-yoga` instance **is** a `(Request) => Promise<Response>` handler, so GraphQL mounts as plain `RouteDescriptor`s with no bespoke transport — the platform `RequestContext` is threaded into Yoga as the server context, so resolvers receive the same `ctx` the host built.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createYogaRoutes` | `src/yoga/yoga.ts` | Route factory producing GET+POST RouteDescriptors |
## File Structure
```
src/
index.ts # Exports createYogaRoutes + shared types
yoga/
yoga.ts # createYogaRoutes implementation
yoga.md # Module docs
yoga.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql-yoga` | `createYogaRoutes`, `EngineRouteOptions`, `GraphqlExecutionContext`, `DEFAULT_GRAPHQL_PATH` |
| `@suaveplan/api-graphql-yoga/yoga` | Same from feature subpath |
## Patterns
### Basic Yoga GraphQL endpoint
```ts
import { collectRegistry } from "@suaveplan/api-graphql-core";
import { createYogaRoutes } from "@suaveplan/api-graphql-yoga";
import { definePlugin } from "@suaveplan/api-plugin-kit";
const source = collectRegistry((registry) => {
registry.addTypeDefs("type Query { hello(name: String): String }");
registry.addResolvers({
Query: { hello: (_p, a: { name?: string }) => `hi ${a.name ?? "world"}` },
});
});
const plugin = definePlugin({
id: "graphql-app",
version: "0.1.0",
routes(router) {
for (const route of createYogaRoutes(source, { path: "/graphql" })) {
router[route.method.toLowerCase() as "get" | "post"](route.path, route.handler);
}
},
});
```
## Constraints
- **Default engine** — this is the default GraphQL engine; Apollo is optional.
- **WinterCG compatible** — Yoga is a `(Request) => Promise<Response>` handler.
- **Never depends on Apollo** — depends only on `@suaveplan/api-graphql-core` and `@suaveplan/api-types`.
- **Real GraphQL execution** — tests execute real GraphQL queries through Yoga over real `Request` objects; no mocks.
- **100% coverage** — coverage gated at 100% on every metric.
- **Dependencies**: `@suaveplan/api-graphql-core`, `@suaveplan/api-types`, `graphql`, `graphql-yoga`, `tslib`.
## Checklist
- [ ] Read `src/yoga/yoga.ts` before modifying route factory
- [ ] Preserve WinterCG compatibility — Yoga routes must work as plain fetch handlers
- [ ] Never add Apollo-specific code — keep engines strictly separated

View File

@@ -0,0 +1,99 @@
---
name: sp-api-graphql
description: GraphQL-over-HTTP meta-package for the Fetch-native Suaveplan API platform. Re-exports the full public surface of five extracted packages — api-graphql-core (registry + errors), api-graphql-yoga (default engine), api-graphql-apollo (optional engine), api-graphql-ui-graphiql, and api-graphql-ui-altair — so existing imports keep working unchanged.
---
# @suaveplan/api-graphql
## Purpose
GraphQL-over-HTTP meta-package for the Fetch-native Suaveplan API platform. Builds an executable schema from a `GraphqlRegistry` (SDL + resolvers) or a pre-built `GraphQLSchema` and emits `RouteDescriptor[]` whose handlers delegate to a real engine fetch handler.
Yoga is the default engine; Apollo Server is an optional-peer engine on a subpath; GraphiQL and Altair IDE shells ship as HTML-route factories on subpaths.
This package re-exports the full public surface of five extracted packages so existing imports keep working unchanged.
## Architecture
| Source Package | Re-exported Surface | Role |
|---|---|---|
| `@suaveplan/api-graphql-core` | `collectRegistry`, `buildSchema`, `MutableGraphqlRegistry`, `GRAPHQL_CODES`, `GraphqlError`, `EngineRouteOptions`, etc. | Registry runtime + error codes + engine contract |
| `@suaveplan/api-graphql-yoga` | `createYogaRoutes` | Default engine (Yoga is a WinterCG fetch handler) |
| `@suaveplan/api-graphql-apollo` | `createApolloRoutes`, `loadApollo` | Optional Apollo-Server engine |
| `@suaveplan/api-graphql-ui-graphiql` | `createGraphiqlRoutes` | GraphiQL IDE renderer |
| `@suaveplan/api-graphql-ui-altair` | `createAltairRoutes` | Altair IDE renderer |
## File Structure
```
src/
index.ts # Re-exports from all five extracted packages
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-graphql` | All from core + Yoga + Apollo + GraphiQL + Altair |
| `@suaveplan/api-graphql/registry` | `collectRegistry`, `buildSchema`, `MutableGraphqlRegistry`, etc. |
| `@suaveplan/api-graphql/engine/yoga` | `createYogaRoutes` |
| `@suaveplan/api-graphql/engine/apollo` | `createApolloRoutes`, `loadApollo` |
| `@suaveplan/api-graphql/ui/graphiql` | `createGraphiqlRoutes` |
| `@suaveplan/api-graphql/ui/altair` | `createAltairRoutes` |
| `@suaveplan/api-graphql/error-codes` | `GRAPHQL_CODES`, `GraphqlError` |
## Patterns
### Basic Yoga GraphQL endpoint
```ts
import { createApi } from "@suaveplan/api";
import { definePlugin } from "@suaveplan/api-plugin-kit";
import { collectRegistry, createYogaRoutes } from "@suaveplan/api-graphql";
const source = collectRegistry((registry) => {
registry.addTypeDefs("type Query { hello(name: String): String }");
registry.addResolvers({
Query: { hello: (_p, a: { name?: string }) => `hi ${a.name ?? "world"}` },
});
});
const plugin = definePlugin({
id: "graphql-app",
version: "0.1.0",
routes(router) {
for (const route of createYogaRoutes(source, { path: "/graphql" })) {
router[route.method.toLowerCase() as "get" | "post"](route.path, route.handler);
}
},
});
```
### Apollo engine (optional peer)
```ts
import { createApolloRoutes } from "@suaveplan/api-graphql/engine/apollo";
const routes = createApolloRoutes(source, { path: "/graphql" });
```
### IDE shells
```ts
import { createGraphiqlRoutes } from "@suaveplan/api-graphql/ui/graphiql";
import { createAltairRoutes } from "@suaveplan/api-graphql/ui/altair";
```
## Constraints
- **Meta-package** — zero own code; all re-exports from extracted sibling packages.
- **Back-compat** — preserves the exact pre-extraction public surface.
- **Yoga default** — `createYogaRoutes` is the default engine; Apollo is optional.
- **Apollo lazy-loaded** — `@apollo/server` imported on first request; missing peer raises `API_GRAPHQL.APOLLO_PEER_MISSING` (HTTP 501).
- **Dependencies**: `@suaveplan/api-graphql-apollo`, `@suaveplan/api-graphql-core`, `@suaveplan/api-graphql-ui-altair`, `@suaveplan/api-graphql-ui-graphiql`, `@suaveplan/api-graphql-yoga`, `tslib`.
## Checklist
- [ ] When adding new subpaths, update both this meta-package and the source package
- [ ] Preserve back-compat — existing imports must keep working
- [ ] Do not add runtime code here — this is purely a re-export barrel

View File

@@ -0,0 +1,33 @@
---
name: api-stack-api-jobs-navigator
description: Navigator for api-stack/api-jobs. Pick a library to get detailed API guidance.
---
# Api jobs (api-stack)
Packages in `api-stack/api-jobs`.
## Libraries
### @suaveplan/api-jobs-kit
Shared contract glue for the @suaveplan jobs.scheduler capability tier: the stable JOBS.* error-code registry + JobsError, the Zod boundary schemas (jobDefinitionSchema/retryPolicySchema/scheduleOptsS...
→ Skill: `sp-api-jobs-kit/SKILL.md`
### @suaveplan/api-jobs-scheduler
Real Jobs capability backend for the Suaveplan API platform: SchedulerJobs is a JobRegistrationHandle backed by @suaveplan/job-scheduler (real cron + queued scheduling, retries, timeouts, concurrency)...
→ Skill: `sp-api-jobs-scheduler/SKILL.md`
### @suaveplan/api-jobs-scheduler-memory
In-memory reference implementation of the jobs.scheduler capability (JobSchedulerLike from @suaveplan/api-types). Delegates the retry/timeout/abort execution loop to the foundation @suaveplan/job-sche...
→ Skill: `sp-api-jobs-scheduler-memory/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,103 @@
---
name: sp-api-jobs-kit
description: Shared contract glue for the jobs.scheduler capability tier. JOBS.* error-code registry + JobsError, Zod boundary schemas + pure isValidCron validator, and the 11-invariant runner-agnostic runJobSchedulerConformance harness parameterized over a caller-supplied JobSchedulerLike factory. Imports NO implementation package.
---
# @suaveplan/api-jobs-kit
## Purpose
Shared contract glue for the `jobs.scheduler` capability tier — the rich, optional job-scheduler layer that sits alongside the minimal `jobs` (`JobRegistrationHandle`) capability without widening it.
The kit owns three things every `jobs.scheduler` backend reuses:
1. **`JOBS.*` error-code registry + `JobsError`** — eight stable codes callers branch on.
2. **Zod boundary schemas + `isValidCron`** — `jobDefinitionSchema`, `retryPolicySchema`, `scheduleOptsSchema`, `runScheduledOptsSchema`, and the pure dependency-free 5/6-field cron validator.
3. **`runJobSchedulerConformance`** — the runner-agnostic 11-invariant harness every backend runs against its own implementation.
## Architecture
| Component | Source | Role |
|---|---|---|
| `JOBS_CODES` | `src/error-codes/error-codes.ts` | 8-code `JOBS.*` registry |
| `JobsError` | `src/error/jobs-error.ts` | Typed error class |
| `jobDefinitionSchema` | `src/schemas/schemas.ts` | Zod boundary for job definitions |
| `retryPolicySchema` | `src/schemas/schemas.ts` | Zod boundary for retry policies |
| `scheduleOptsSchema` | `src/schemas/schemas.ts` | Zod boundary for schedule options |
| `runScheduledOptsSchema` | `src/schemas/schemas.ts` | Zod boundary for run-scheduled options |
| `isValidCron` | `src/schemas/schemas.ts` | Pure 5/6-field cron validator |
| `runJobSchedulerConformance` | `src/testing/conformance.ts` | 11-invariant conformance harness |
## File Structure
```
src/
index.ts # Barrel: error codes, error class, schemas, harness
error-codes/
error-codes.ts # JOBS_CODES registry
error/
jobs-error.ts # JobsError class
schemas/
schemas.ts # Zod schemas + isValidCron
testing/
conformance.ts # runJobSchedulerConformance harness
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-jobs-kit` | `JOBS_CODES`, `JobsError`, `jobDefinitionSchema`, `retryPolicySchema`, `scheduleOptsSchema`, `runScheduledOptsSchema`, `isValidCron`, `runJobSchedulerConformance` |
| `@suaveplan/api-jobs-kit/error-codes` | `JOBS_CODES`, `JobsCode`, `JOBS_ERROR_NAME`, `CONTRACT_VERSION` |
| `@suaveplan/api-jobs-kit/error` | `JobsError` |
| `@suaveplan/api-jobs-kit/schemas` | All schemas + `isValidCron` |
| `@suaveplan/api-jobs-kit/testing` | `runJobSchedulerConformance` |
## Patterns
### Validate a cron expression
```ts
import { isValidCron, JOBS_CODES, JobsError } from "@suaveplan/api-jobs-kit";
if (!isValidCron("*/15 * * * *")) {
throw new JobsError(JOBS_CODES.CRON_INVALID, "error.jobs.cron_invalid");
}
```
### Running conformance against a backend
```ts
import { runJobSchedulerConformance } from "@suaveplan/api-jobs-kit/testing";
runJobSchedulerConformance("MyScheduler", ({ tenantId, clock }) => ({
scheduler: createMyScheduler({ tenantId, clock }),
advance: (ms) => myClock.advance(ms),
}));
```
## Constraints
- **Imports NO implementation package** — depends only on `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/testing`, `@suaveplan/types`, `tslib`, `zod`.
- **11 invariants** — the harness covers scheduling, execution, retry, timeout, concurrency, and disposal.
- **Universal tier** — no browser-only or Node-only API.
- **Pure cron validator** — `isValidCron` is dependency-free and pure.
## Error Codes
| Code | Meaning |
|---|---|
| `JOBS.JOB_NOT_FOUND` | Referenced job does not exist |
| `JOBS.JOB_ALREADY_COMPLETED` | Job already finished |
| `JOBS.RETRY_BUDGET_EXHAUSTED` | Max retries reached |
| `JOBS.TIMEOUT_EXCEEDED` | Job exceeded timeout |
| `JOBS.CRON_INVALID` | Cron expression is invalid |
| `JOBS.INVALID_RUN_AT_PAST` | Scheduled run-at is in the past |
| `JOBS.TENANT_MISMATCH` | Job tenant does not match caller |
| `JOBS.SCHEDULER_DISPOSED` | Scheduler has been disposed |
## Checklist
- [ ] Read `src/testing/conformance.ts` before modifying harness
- [ ] Preserve kit→variant boundary — never import a concrete scheduler backend
- [ ] `isValidCron` must remain pure and dependency-free

View File

@@ -0,0 +1,117 @@
---
name: sp-api-jobs-scheduler-memory
description: In-memory reference implementation of the jobs.scheduler capability (JobSchedulerLike from @suaveplan/api-types). Delegates retry/timeout/abort execution loop to @suaveplan/job-scheduler executeJob primitive and owns tenant guard, schedule-time validation, due-time queue + runScheduled drain, dispose FSM, abortSignal cancellation, and frozen JobHandle snapshots. Passes all 11 invariants of the kit's runJobSchedulerConformance harness. Universal tier.
---
# @suaveplan/api-jobs-scheduler-memory
## Purpose
In-memory reference implementation of the `jobs.scheduler` capability tier (`JobSchedulerLike` from `@suaveplan/api-types`). It is the canonical green proof that the contract is implementable — it passes all 11 invariants of `@suaveplan/api-jobs-kit`'s `runJobSchedulerConformance` harness.
The scheduler **delegates** the retry / per-attempt-timeout / abort execution loop to the foundation `@suaveplan/job-scheduler` `executeJob` primitive, and owns only what the foundation does not express:
- tenant binding + guard (`TENANT_MISMATCH`, `JOB_NOT_FOUND`)
- synchronous schedule-time validation (`CRON_INVALID` via the kit's `isValidCron`, `INVALID_RUN_AT_PAST`)
- the due-time queue + `runScheduled` drain (registration order, batch summary, `until` / `maxBatch` / `abortSignal`)
- the dispose FSM (`SCHEDULER_DISPOSED`, idempotent)
- `ScheduleOpts.abortSignal` cancellation + `JOB_ALREADY_COMPLETED`
- frozen `JobHandle` snapshots
## Architecture
| Component | Source | Role |
|---|---|---|
| `MemoryJobScheduler` | `src/scheduler/scheduler.ts` | In-memory `JobSchedulerLike` implementation |
| `createMemoryJobScheduler` | `src/scheduler/scheduler.ts` | Factory mirroring harness signature |
| `snapshot` | `src/scheduler/snapshot.ts` | Projects mutable internal record into immutable `JobHandle` |
| `runJobToOutcome` | `src/execution/execution.ts` | Delegation seam to foundation `executeJob` primitive |
| `AttemptTrackingBus` | `src/execution/execution.ts` | Minimal event sink tracking per-attempt count |
## File Structure
```
src/
index.ts # Exports createMemoryJobScheduler, MemoryJobScheduler, MemoryJobSchedulerOptions
scheduler/
scheduler.ts # MemoryJobScheduler class
scheduler.types.ts # InternalRecord, MemoryJobSchedulerOptions
snapshot.ts # Immutable JobHandle projection
execution/
execution.ts # runJobToOutcome delegation + AttemptTrackingBus + NOOP_LOGGER
execution.types.ts # RunJobInput, RunJobOutcome
error-codes/
error-codes.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-jobs-scheduler-memory` | `MemoryJobScheduler`, `createMemoryJobScheduler`, `MemoryJobSchedulerOptions` |
| `@suaveplan/api-jobs-scheduler-memory/scheduler` | Same from feature subpath |
## Patterns
### Basic usage
```ts
import { createMemoryJobScheduler } from "@suaveplan/api-jobs-scheduler-memory";
const scheduler = createMemoryJobScheduler({
tenantId: "acme",
clock: Date.now,
});
const handle = scheduler.schedule({
name: "send-digest",
tenantId: "acme",
retryPolicy: { maxAttempts: 3 },
timeoutMs: 5_000,
run: (input, signal) => sendDigest(input, signal),
});
const summary = await scheduler.runScheduled();
console.log(summary); // { drained, succeeded, failed, cancelled }
console.log(scheduler.getStatus(handle.id).status);
```
### Conformance proof
```ts
import { runJobSchedulerConformance } from "@suaveplan/api-jobs-kit/testing";
import { createMemoryJobScheduler } from "@suaveplan/api-jobs-scheduler-memory";
runJobSchedulerConformance("MemoryJobScheduler", ({ tenantId, clock }) => ({
scheduler: createMemoryJobScheduler({ tenantId, clock }),
advance: () => {},
}));
```
## Constraints
- **Universal tier** — no browser-only or Node-only API.
- **Deterministic tests** — inject `clock` for zero-real-time tests; omit for `Date.now`.
- **11 invariant harness** — passes `runJobSchedulerConformance` from `@suaveplan/api-jobs-kit`.
- **Foundation delegation** — retry/timeout/abort loop lives in `@suaveplan/job-scheduler`, not here.
- **Frozen snapshots** — `JobHandle` is `Object.freeze`-d and never carries `undefined` placeholder slots.
- **Dependencies**: `@suaveplan/api-jobs-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/job-scheduler`, `@suaveplan/types`, `tslib`.
## Error Codes (from JOBS_CODES)
| Code | Raised when |
|---|---|
| `JOBS.TENANT_MISMATCH` | `def.tenantId` does not match scheduler tenant |
| `JOBS.CRON_INVALID` | `opts.cron` fails `isValidCron` |
| `JOBS.INVALID_RUN_AT_PAST` | `opts.runAt` is in the past |
| `JOBS.JOB_NOT_FOUND` | `getStatus` / `cancelScheduled` references unknown id |
| `JOBS.JOB_ALREADY_COMPLETED` | `cancelScheduled` on non-pending job |
| `JOBS.SCHEDULER_DISPOSED` | Any operation after `dispose()` |
| `JOBS.TIMEOUT_EXCEEDED` | Foundation timeout mapped at boundary |
| `JOBS.RETRY_BUDGET_EXHAUSTED` | Foundation retry exhaustion mapped at boundary |
## Checklist
- [ ] Read `src/scheduler/scheduler.ts` before modifying scheduling behaviour
- [ ] Read `src/execution/execution.ts` before modifying foundation delegation
- [ ] Ensure `runJobSchedulerConformance` continues to pass
- [ ] Preserve frozen snapshot semantics — never expose mutable internal state

View File

@@ -0,0 +1,100 @@
---
name: sp-api-jobs-scheduler
description: Real Jobs capability backend for the Suaveplan API platform. SchedulerJobs is a JobRegistrationHandle backed by @suaveplan/job-scheduler (real cron + queued scheduling, retries, timeouts, concurrency), passing the same runJobsConformance harness the MemoryJobs reference impl passes. Universal tier.
---
# @suaveplan/api-jobs-scheduler
## Purpose
Real `Jobs` capability backend for the Suaveplan API platform. `SchedulerJobs` is a `JobRegistrationHandle` (the `jobs` capability shape frozen in `@suaveplan/api-types`) backed by a real `@suaveplan/job-scheduler` `JobScheduler`.
Where the reference `MemoryJobs` (`@suaveplan/api-capability-jobs-memory`) triggers handlers on demand and owns no scheduling, `SchedulerJobs` actually schedules: cron handlers fire on their cron expression and queued handlers run when a payload is enqueued, both flowing through the scheduler's retry, timeout, and concurrency machinery. It passes the same `runJobsConformance` harness `MemoryJobs` passes, so the capability runtime treats the two identically — the difference is that this one really schedules.
## Architecture
| Component | Source | Role |
|---|---|---|
| `SchedulerJobs` | `src/scheduler-jobs/scheduler-jobs.ts` | `JobRegistrationHandle` backed by a real scheduler |
| `createSchedulerJobs` | `src/scheduler-jobs/scheduler-jobs.ts` | Factory for SchedulerJobs |
| `VirtualTimers` | `src/scheduler-jobs/virtual-timers.ts` | Deterministic timer + clock pump for zero-real-time tests |
| `JOBS_SCHEDULER_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Exports SchedulerJobs, createSchedulerJobs, VirtualTimers, error codes
scheduler-jobs/
scheduler-jobs.ts # SchedulerJobs implementation
scheduler-jobs.types.ts # Options and handler types
virtual-timers.ts # Deterministic timer backbone
scheduler-jobs.md, virtual-timers.md
error-codes/
error-codes.ts # JOBS_SCHEDULER_CODES + SchedulerJobsError
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-jobs-scheduler` | `SchedulerJobs`, `createSchedulerJobs`, `VirtualTimers`, `JOBS_SCHEDULER_CODES`, `SchedulerJobsError` |
| `@suaveplan/api-jobs-scheduler/scheduler-jobs` | `SchedulerJobs`, `VirtualTimers` |
| `@suaveplan/api-jobs-scheduler/error-codes` | `JOBS_SCHEDULER_CODES`, `SchedulerJobsError` |
## Patterns
### Basic usage
```ts
import { SchedulerJobs } from "@suaveplan/api-jobs-scheduler";
const jobs = new SchedulerJobs();
jobs.registerCron("nightly-rollup", async () => {
await computeRollups();
});
jobs.registerQueued("send-email", async (payload) => {
await deliver(payload);
});
// On-demand trigger — deterministic, no real wall-clock wait.
await jobs.runQueued("send-email", { to: "a@b.test" });
await jobs.runCron("nightly-rollup");
// Autonomous firing on the host's loop.
jobs.start();
// ... later, on graceful shutdown:
await jobs.stop();
```
### Conformance testing
```ts
import { runJobsConformance } from "@suaveplan/api-capability-kit";
import { SchedulerJobs } from "@suaveplan/api-jobs-scheduler";
runJobsConformance("SchedulerJobs", () => {
const jobs = new SchedulerJobs();
return {
handle: jobs,
runCron: (name) => jobs.runCron(name),
runQueued: (name, payload) => jobs.runQueued(name, payload),
};
});
```
## Constraints
- **Universal tier** — needs only an injectable timer pair (defaulting to `globalThis.setTimeout`); no `node:*` import required.
- **Deterministic tests** — inject `clock` / `timers` for deterministic tests; omit to use built-in `VirtualTimers`.
- **Same conformance as MemoryJobs** — passes `runJobsConformance` from `@suaveplan/api-capability-kit`.
- **Real scheduling** — cron handlers fire on their expression; queued handlers run on enqueue.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/job-scheduler`, `@suaveplan/types`, `tslib`.
## Checklist
- [ ] Read `src/scheduler-jobs/scheduler-jobs.ts` before modifying job behaviour
- [ ] Read `src/scheduler-jobs/virtual-timers.ts` before changing timer semantics
- [ ] Ensure `runJobsConformance` continues to pass
- [ ] Preserve zero-real-time test capability via VirtualTimers

View File

@@ -0,0 +1,57 @@
---
name: api-stack-api-middleware-navigator
description: Navigator for api-stack/api-middleware. Pick a library to get detailed API guidance.
---
# Api middleware (api-stack)
Packages in `api-stack/api-middleware`.
## Libraries
### @suaveplan/api-middleware
Shared contracts for the Suaveplan API middleware tier: ApiMiddlewareError base class and API_MIDDLEWARE_CODES registry (RATE_LIMITED, UNAUTHENTICATED, FORBIDDEN, PAYLOAD_TOO_LARGE, AUDIT_SINK_FAILED)...
→ Skill: `sp-api-middleware/SKILL.md`
### @suaveplan/api-middleware-audit
Install-and-go audit plugin for the Fetch-native @suaveplan/api platform. createAuditPlugin(options) returns a @suaveplan/api Plugin whose declarative globalMiddleware is the platform @suaveplan/api-m...
→ Skill: `sp-api-middleware-audit/SKILL.md`
### @suaveplan/api-middleware-body-limit
Install-and-go request-body-size-limit plugin for the Fetch-native @suaveplan/api platform. createBodyLimitPlugin(options) returns a @suaveplan/api Plugin whose declarative globalMiddleware is the pla...
→ Skill: `sp-api-middleware-body-limit/SKILL.md`
### @suaveplan/api-middleware-cors
Install-and-go CORS plugin for the Fetch-native @suaveplan/api platform. createCorsPlugin(options) returns a @suaveplan/api Plugin whose declarative globalMiddleware is the platform @suaveplan/api-mid...
→ Skill: `sp-api-middleware-cors/SKILL.md`
### @suaveplan/api-middleware-rate-limit
Install-and-go rate-limit plugin for the Fetch-native @suaveplan/api platform. createRateLimitPlugin(options) returns a @suaveplan/api Plugin whose declarative globalMiddleware is the platform @suavep...
→ Skill: `sp-api-middleware-rate-limit/SKILL.md`
### @suaveplan/api-middleware-rbac
Install-and-go RBAC plugin for the Fetch-native @suaveplan/api platform. createRbacPlugin(options) returns a @suaveplan/api Plugin whose declarative globalMiddleware is the platform @suaveplan/api-mid...
→ Skill: `sp-api-middleware-rbac/SKILL.md`
### @suaveplan/api-middleware-rules
Platform-native rule-engine middleware for @suaveplan/api: ruleMiddleware and accessControlMiddleware typed as Middleware<Caps> against RequestContext. Replaces the Hono-specific @suaveplan/rules-hono...
→ Skill: `sp-api-middleware-rules/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,106 @@
---
name: sp-api-middleware-audit
description: Install-and-go audit plugin for the Fetch-native @suaveplan/api platform. createAuditPlugin(options) returns a Plugin whose declarative globalMiddleware is the platform auditMiddleware, so dropping it into createApi({ plugins }) audits the whole app with no imperative middleware wiring. Emits structured AuditEvent per request to configurable sinks; swallows but counts sink failures so auditing never breaks a request.
---
# @suaveplan/api-middleware-audit
## Purpose
Install-and-go audit plugin for the Fetch-native `@suaveplan/api` platform. `createAuditPlugin(options)` returns a `@suaveplan/api` `Plugin` whose declarative `globalMiddleware` is the platform `@suaveplan/api-middleware` `auditMiddleware`.
The host collects that declaration across every registered plugin and applies it **pipeline-wide** — to ALL routes, including other plugins' routes — so dropping the plugin into `createApi({ plugins })` audits the whole app with no imperative `createApi({ middleware })` wiring. This is the declarative global-middleware seam in action: a cross-cutting plugin observes the whole surface without mutating a host.
The wrapped middleware emits a structured `AuditEvent` to every configured sink after each request (method, path, status, outcome, principal id, tenant id, duration), redacts request metadata through `@suaveplan/data-masking`, swallows (but counts) a failing sink so auditing never breaks a request, and emits the `api_middleware.audit.*` OTel instruments.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAuditPlugin` | `src/plugin/plugin.ts` | Plugin factory — wraps `auditMiddleware` as `globalMiddleware` |
| `auditMiddleware` | `src/middleware/audit.ts` | Emits `AuditEvent` to every sink after each request |
| `stdoutAuditSink` | `src/middleware/stdout-sink.ts` | NDJSON to an injected line writer |
| `fileAuditSink` | `src/middleware/file-sink.ts` | NDJSON appended to a path, optional HMAC |
| `AUDIT_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | `AUDIT_PLUGIN.INVALID_INPUT` registry |
## File Structure
```
src/
index.ts # Barrel: createAuditPlugin, auditMiddleware, sinks, error codes, options
plugin/
plugin.ts # createAuditPlugin factory
plugin.test.ts
middleware/
audit.ts # auditMiddleware
audit.types.ts # AuditEvent, AuditSink, AuditOptions
stdout-sink.ts # stdoutAuditSink
file-sink.ts # fileAuditSink
options/
options.ts # Zod boundary + resolveAuditPluginConfig
error-codes/
error-codes.ts # AUDIT_PLUGIN_CODES + AuditPluginError
otel/
instruments.ts # api_middleware.audit.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-audit` | `createAuditPlugin`, `auditMiddleware`, `stdoutAuditSink`, `fileAuditSink`, `AUDIT_PLUGIN_CODES`, `AuditPluginError`, options types |
| `@suaveplan/api-middleware-audit/plugin` | `createAuditPlugin` |
| `@suaveplan/api-middleware-audit/options` | `AuditPluginOptionsSchema`, `resolveAuditPluginConfig`, types |
| `@suaveplan/api-middleware-audit/error-codes` | `AUDIT_PLUGIN_CODES`, `AuditPluginError` |
| `@suaveplan/api-middleware-audit/middleware` | `auditMiddleware`, sink types |
| `@suaveplan/api-middleware-audit/otel` | OTel instruments |
## Patterns
### Audit the whole app
```ts
import { createApi } from "@suaveplan/api";
import { stdoutAuditSink } from "@suaveplan/api-middleware-audit";
import { createAuditPlugin } from "@suaveplan/api-middleware-audit";
const runtime = createApi({
plugins: [
createAuditPlugin({ sinks: [stdoutAuditSink()] }),
myFeaturePlugin, // ← its routes are now audited too
],
});
```
### Custom SIEM sink
```ts
import type { AuditEvent, AuditSink } from "@suaveplan/api-middleware-audit";
const siemSink: AuditSink = {
async write(event: AuditEvent): Promise<void> {
await fetch("https://siem.internal/ingest", {
method: "POST",
body: JSON.stringify(event),
});
},
};
createAuditPlugin({ sinks: [siemSink] });
```
## Constraints
- **Sink-agnostic** — drive platform sinks or any `AuditSink` with `write(event)`.
- **At least one sink required** — empty/missing `sinks` throws `AUDIT_PLUGIN.INVALID_INPUT` at construction.
- **Never breaks requests** — failing sinks are swallowed and counted, never thrown.
- **Declarative global middleware** — contributes `globalMiddleware`, not routes.
- **Per-request metadata redaction** — default full-masks `authorization`; configurable via `maskRules`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-middleware`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/data-masking`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/middleware/audit.ts` before modifying audit event shape
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve sink-failure swallow semantics — auditing must never break a request
- [ ] Ensure at least one sink is enforced at the Zod boundary

View File

@@ -0,0 +1,96 @@
---
name: sp-api-middleware-body-limit
description: Install-and-go request-body-size-limit plugin for the Fetch-native @suaveplan/api platform. createBodyLimitPlugin(options) returns a Plugin whose declarative globalMiddleware is the platform bodyLimitMiddleware (Content-Length + streamed-byte cap, 413 RFC 9457 problem+json, api_middleware.body_limit.rejected_total OTel counter). Dropping it into createApi({ plugins }) caps every request body with no imperative middleware wiring.
---
# @suaveplan/api-middleware-body-limit
## Purpose
Install-and-go request-body-size-limit plugin for the Fetch-native `@suaveplan/api` platform. `createBodyLimitPlugin(options)` returns a `@suaveplan/api` `Plugin` whose declarative `globalMiddleware` is the platform `@suaveplan/api-middleware` `bodyLimitMiddleware`.
The host collects that declaration across every registered plugin and applies it **pipeline-wide** — to ALL routes, including other plugins' routes — so dropping the plugin into `createApi({ plugins })` caps the body size of every request to the whole app with no imperative `createApi({ middleware })` wiring.
The wrapped middleware checks a present `Content-Length` first (cheap, no read) and otherwise drains a **clone** of the body to count bytes — leaving the original stream intact for the handler — rejecting an over-cap request with a `413` RFC 9457 `application/problem+json` response and emitting the `api_middleware.body_limit.rejected_total` OTel counter. Bodyless methods (`GET`/`HEAD`/`OPTIONS`) pass straight through.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createBodyLimitPlugin` | `src/plugin/plugin.ts` | Plugin factory — wraps `bodyLimitMiddleware` as `globalMiddleware` |
| `bodyLimitMiddleware` | `src/middleware/body-limit.ts` | Rejects bodies over `maxBytes` with `413` problem+json |
| `BODY_LIMIT_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | `BODY_LIMIT_PLUGIN.INVALID_INPUT` registry |
## File Structure
```
src/
index.ts # Barrel: createBodyLimitPlugin, error codes, options
plugin/
plugin.ts # createBodyLimitPlugin factory
plugin.test.ts
middleware/
body-limit.ts # bodyLimitMiddleware implementation
body-limit.types.ts # BodyLimitOptions
options/
options.ts # Zod boundary + resolveBodyLimitPluginConfig
error-codes/
error-codes.ts # BODY_LIMIT_PLUGIN_CODES + BodyLimitPluginError
otel/
instruments.ts # api_middleware.body_limit.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-body-limit` | `createBodyLimitPlugin`, `BODY_LIMIT_PLUGIN_CODES`, `BodyLimitPluginError`, options types |
| `@suaveplan/api-middleware-body-limit/plugin` | `createBodyLimitPlugin` |
| `@suaveplan/api-middleware-body-limit/options` | `BodyLimitPluginOptionsSchema`, `resolveBodyLimitPluginConfig`, types |
| `@suaveplan/api-middleware-body-limit/error-codes` | `BODY_LIMIT_PLUGIN_CODES`, `BodyLimitPluginError` |
## Patterns
### Cap every request body
```ts
import { createApi } from "@suaveplan/api";
import { createBodyLimitPlugin } from "@suaveplan/api-middleware-body-limit";
const runtime = createApi({
plugins: [
createBodyLimitPlugin({ maxBytes: 1_048_576 }), // 1 MiB cap
myFeaturePlugin, // ← its routes now reject bodies over 1 MiB
],
});
```
### Per-route cap (use middleware directly)
```ts
import { bodyLimitMiddleware } from "@suaveplan/api-middleware/body-limit";
// In a route descriptor:
{
method: "POST",
path: "/upload",
handler: uploadHandler,
middleware: [bodyLimitMiddleware({ maxBytes: 10 * 1024 * 1024 })],
}
```
## Constraints
- **`maxBytes` required** — must be a positive integer; throws `BODY_LIMIT_PLUGIN.INVALID_INPUT` at construction.
- **Content-Length first** — cheap header check before any stream read.
- **Clone-before-count** — original body stream stays intact for the handler.
- **Bodyless passthrough** — `GET`/`HEAD`/`OPTIONS` skip the cap entirely.
- **Declarative global middleware** — contributes `globalMiddleware`, not routes.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-core`, `@suaveplan/api-middleware`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/middleware/body-limit.ts` before modifying byte-count logic
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve clone semantics — never consume the original request body stream
- [ ] Ensure `maxBytes` validation remains strict (positive integer)

View File

@@ -0,0 +1,119 @@
---
name: sp-api-middleware-cors
description: Install-and-go CORS plugin for the Fetch-native @suaveplan/api platform. createCorsPlugin(options) returns a Plugin whose declarative globalMiddleware is the platform corsMiddleware (origin allowlist/predicate, preflight answering, Access-Control-Allow-* decoration, api_middleware.cors.preflight_total OTel counter). Permissive defaults mean createCorsPlugin() with no config is valid. Dropping it into createApi({ plugins }) decorates the whole app with no imperative middleware wiring.
---
# @suaveplan/api-middleware-cors
## Purpose
Install-and-go CORS plugin for the Fetch-native `@suaveplan/api` platform. `createCorsPlugin(options)` returns a `@suaveplan/api` `Plugin` whose declarative `globalMiddleware` is the platform `@suaveplan/api-middleware` `corsMiddleware`.
The host collects that declaration across every registered plugin and applies it **pipeline-wide** — to ALL routes, including other plugins' routes — so dropping the plugin into `createApi({ plugins })` answers CORS preflights and decorates responses across the whole app with no imperative `createApi({ middleware })` wiring.
Because `corsMiddleware` takes permissive defaults (`origin: "*"`, the common verb set, a `204` preflight), **`createCorsPlugin()` with no config is valid** and yields a permissive CORS policy — unlike the RBAC plugin, no field is required.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createCorsPlugin` | `src/plugin/plugin.ts` | Plugin factory — wraps `corsMiddleware` as `globalMiddleware` |
| `corsMiddleware` | `src/middleware/cors.ts` | Answers `OPTIONS` preflight, decorates responses with `Access-Control-Allow-*` |
| `CORS_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | `CORS_PLUGIN.INVALID_INPUT` registry |
## File Structure
```
src/
index.ts # Barrel: createCorsPlugin, corsMiddleware, CORS options, error codes
plugin/
plugin.ts # createCorsPlugin factory
plugin.test.ts
middleware/
cors.ts # corsMiddleware implementation
cors.types.ts # CorsOptions, CorsOrigin
options/
options.ts # Zod boundary + resolveCorsPluginConfig
error-codes/
error-codes.ts # CORS_PLUGIN_CODES + CorsPluginError
otel/
instruments.ts # api_middleware.cors.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-cors` | `createCorsPlugin`, `corsMiddleware`, `CorsOptions`, `CorsOrigin`, `CORS_PLUGIN_CODES`, `CorsPluginError` |
| `@suaveplan/api-middleware-cors/plugin` | `createCorsPlugin` |
| `@suaveplan/api-middleware-cors/options` | `CorsPluginOptionsSchema`, `resolveCorsPluginConfig`, types |
| `@suaveplan/api-middleware-cors/error-codes` | `CORS_PLUGIN_CODES`, `CorsPluginError` |
| `@suaveplan/api-middleware-cors/middleware` | `corsMiddleware`, `CorsOptions`, `CorsOrigin` |
| `@suaveplan/api-middleware-cors/otel` | OTel instruments |
## Patterns
### Global CORS policy
```ts
import { createApi } from "@suaveplan/api";
import { createCorsPlugin } from "@suaveplan/api-middleware-cors";
const runtime = createApi({
plugins: [
createCorsPlugin({ origin: ["https://app.example.com"] }),
myFeaturePlugin, // ← its responses are now CORS-decorated
],
});
```
### Permissive default (no config)
```ts
// Valid — corsMiddleware takes permissive defaults
const plugin = createCorsPlugin();
// or
const plugin = createCorsPlugin({});
```
### Per-route CORS (use middleware directly)
```ts
import { corsMiddleware } from "@suaveplan/api-middleware/cors";
// In a route descriptor:
{
method: "GET",
path: "/public",
handler: publicHandler,
middleware: [corsMiddleware({ origin: ["https://app.example.com"] })],
}
```
## Constraints
- **No required fields** — `createCorsPlugin()` with no args is valid (permissive CORS).
- **Preflight short-circuit** — allowed `OPTIONS` preflight answers with `204` and does NOT call `next()`.
- **Disallowed origin = no header** — browser blocks the read; server body unchanged.
- **Vary: Origin** — always set when `Allow-Origin` is emitted.
- **Declarative global middleware** — contributes `globalMiddleware`, not routes.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-middleware`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Options
| Option | Type | Default |
|---|---|---|
| `origin` | `"*" \| string[] \| (origin) => boolean` | `"*"` |
| `methods` | `string[]` | common verb set |
| `allowedHeaders` | `string[]` | reflects request |
| `exposedHeaders` | `string[]` | — |
| `credentials` | `boolean` | `false` |
| `maxAgeSeconds` | `number` | — |
| `preflightStatus` | `number` | `204` |
## Checklist
- [ ] Read `src/middleware/cors.ts` before modifying CORS behaviour
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve permissive-default semantics — empty config must remain valid
- [ ] Ensure preflight short-circuit never calls `next()`

View File

@@ -0,0 +1,109 @@
---
name: sp-api-middleware-rate-limit
description: Install-and-go rate-limit plugin for the Fetch-native @suaveplan/api platform. createRateLimitPlugin(options) returns a Plugin whose declarative globalMiddleware is the platform rateLimitMiddleware (token-bucket or fixed-window algorithm, 429 + Retry-After + X-RateLimit-* headers, RFC 9457 problem+json, api_middleware.rate_limit.* OTel counters). Dropping it into createApi({ plugins }) throttles the whole app with no imperative middleware wiring.
---
# @suaveplan/api-middleware-rate-limit
## Purpose
Install-and-go rate-limit plugin for the Fetch-native `@suaveplan/api` platform. `createRateLimitPlugin(options)` returns a `@suaveplan/api` `Plugin` whose declarative `globalMiddleware` is the platform `@suaveplan/api-middleware` `rateLimitMiddleware`.
The host collects that declaration across every registered plugin and applies it **pipeline-wide** — to ALL routes, including other plugins' routes — so dropping the plugin into `createApi({ plugins })` throttles the whole app with no imperative `createApi({ middleware })` wiring.
The wrapped middleware renders `429` + `Retry-After` + `X-RateLimit-Limit` / `-Remaining` / `-Reset` headers with an RFC 9457 `application/problem+json` body, and emits the `api_middleware.rate_limit.*` OTel counters.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createRateLimitPlugin` | `src/plugin/plugin.ts` | Plugin factory — wraps `rateLimitMiddleware` as `globalMiddleware` |
| `rateLimitMiddleware` | `src/middleware/rate-limit.ts` | Token-bucket or fixed-window limiter → `429` |
| `RATE_LIMIT_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | `RATE_LIMIT_PLUGIN.INVALID_INPUT` registry |
## File Structure
```
src/
index.ts # Barrel: createRateLimitPlugin, rateLimitMiddleware, error codes, options
plugin/
plugin.ts # createRateLimitPlugin factory
plugin.test.ts
middleware/
rate-limit.ts # rateLimitMiddleware implementation
rate-limit.types.ts # RateLimitOptions, RateLimitKeyFn
options/
options.ts # Zod boundary + resolveRateLimitPluginConfig
error-codes/
error-codes.ts # RATE_LIMIT_PLUGIN_CODES + RateLimitPluginError
otel/
instruments.ts # api_middleware.rate_limit.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-rate-limit` | `createRateLimitPlugin`, `rateLimitMiddleware`, `RATE_LIMIT_PLUGIN_CODES`, `RateLimitPluginError`, options types |
| `@suaveplan/api-middleware-rate-limit/plugin` | `createRateLimitPlugin` |
| `@suaveplan/api-middleware-rate-limit/middleware` | `rateLimitMiddleware`, `RateLimitOptions`, `RateLimitKeyFn` |
| `@suaveplan/api-middleware-rate-limit/options` | `RateLimitPluginOptionsSchema`, `resolveRateLimitPluginConfig`, types |
| `@suaveplan/api-middleware-rate-limit/error-codes` | `RATE_LIMIT_PLUGIN_CODES`, `RateLimitPluginError` |
| `@suaveplan/api-middleware-rate-limit/otel` | OTel instruments |
## Patterns
### Throttle the whole app
```ts
import { createApi } from "@suaveplan/api";
import { createRateLimitPlugin } from "@suaveplan/api-middleware-rate-limit";
const runtime = createApi({
plugins: [
createRateLimitPlugin({ limit: 100, windowMs: 60_000 }),
myFeaturePlugin, // ← its routes are throttled too
],
});
```
### Per-route throttling (use middleware directly)
```ts
import { rateLimitMiddleware } from "@suaveplan/api-middleware/rate-limit";
// In a route descriptor:
{
method: "POST",
path: "/api/resource",
handler: resourceHandler,
middleware: [rateLimitMiddleware({ limit: 10, windowMs: 60_000 })],
}
```
## Constraints
- **`limit` and `windowMs` required** — positive integers; throws `RATE_LIMIT_PLUGIN.INVALID_INPUT` at construction.
- **Two algorithms** — `"token-bucket"` (default) or `"fixed-window"`.
- **Injectable key function** — defaults to forwarding-header / principal; configurable via `keyFn`.
- **Injectable clock** — `Date.now` by default; override for deterministic tests.
- **Declarative global middleware** — contributes `globalMiddleware`, not routes.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-core`, `@suaveplan/api-middleware`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/rate-limiter`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Options
| Option | Type | Default | Notes |
|---|---|---|---|
| `limit` | `number` | (required) | Requests per window per key |
| `windowMs` | `number` | (required) | Window length in ms |
| `algorithm` | `"token-bucket" \| "fixed-window"` | `"token-bucket"` | Limiting strategy |
| `keyFn` | `(ctx) => string` | forwarding-header / principal | Bucket key per request |
| `clock` | `() => number` | `Date.now` | Injectable epoch-ms clock |
| `ipHeaders` | `string[]` | `["x-forwarded-for", "x-real-ip"]` | Headers probed for client IP |
## Checklist
- [ ] Read `src/middleware/rate-limit.ts` before modifying limiter logic
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve algorithm selection — token-bucket must remain the default
- [ ] Ensure `429` responses carry `Retry-After` and `X-RateLimit-*` headers

View File

@@ -0,0 +1,122 @@
---
name: sp-api-middleware-rbac
description: Install-and-go RBAC plugin for the Fetch-native @suaveplan/api platform. createRbacPlugin(options) returns a Plugin whose declarative globalMiddleware is the platform rbacMiddleware (scope / anyOf / scopeFor enforcement, 401 + 403 RFC 9457 problem+json, api_middleware.rbac.denied_total OTel counter). Dropping it into createApi({ plugins }) guards the whole app with no imperative middleware wiring. At least one of scope/anyOf/scopeFor is required.
---
# @suaveplan/api-middleware-rbac
## Purpose
Install-and-go RBAC plugin for the Fetch-native `@suaveplan/api` platform. `createRbacPlugin(options)` returns a `@suaveplan/api` `Plugin` whose declarative `globalMiddleware` is the platform `@suaveplan/api-middleware` `rbacMiddleware`.
The host collects that declaration across every registered plugin and applies it **pipeline-wide** — to ALL routes, including other plugins' routes — so dropping the plugin into `createApi({ plugins })` guards the whole app with no imperative `createApi({ middleware })` wiring.
The wrapped middleware renders `401` (no principal) and `403` (insufficient scope) RFC 9457 `application/problem+json` responses and emits the `api_middleware.rbac.denied_total` OTel counter.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createRbacPlugin` | `src/plugin/plugin.ts` | Plugin factory — wraps `rbacMiddleware` as `globalMiddleware` |
| `rbacMiddleware` | `src/middleware/rbac.ts` | Scope-based access control → `401`/`403` |
| `RBAC_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | `RBAC_PLUGIN.INVALID_INPUT` registry |
## File Structure
```
src/
index.ts # Barrel: createRbacPlugin, rbacMiddleware, error codes, options
plugin/
plugin.ts # createRbacPlugin factory
plugin.test.ts
middleware/
rbac.ts # rbacMiddleware implementation
rbac.types.ts # RbacOptions, ScopeForFn
options/
options.ts # Zod boundary + resolveRbacPluginConfig
error-codes/
error-codes.ts # RBAC_PLUGIN_CODES + RbacPluginError
otel/
instruments.ts # api_middleware.rbac.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-rbac` | `createRbacPlugin`, `rbacMiddleware`, `RBAC_PLUGIN_CODES`, `RbacPluginError`, options types |
| `@suaveplan/api-middleware-rbac/plugin` | `createRbacPlugin` |
| `@suaveplan/api-middleware-rbac/middleware` | `rbacMiddleware`, `RbacOptions`, `ScopeForFn` |
| `@suaveplan/api-middleware-rbac/options` | `RbacPluginOptionsSchema`, `resolveRbacPluginConfig`, types |
| `@suaveplan/api-middleware-rbac/error-codes` | `RBAC_PLUGIN_CODES`, `RbacPluginError` |
| `@suaveplan/api-middleware-rbac/otel` | OTel instruments |
## Patterns
### Guard the whole app
```ts
import { createApi } from "@suaveplan/api";
import { createRbacPlugin } from "@suaveplan/api-middleware-rbac";
const runtime = createApi({
plugins: [
createRbacPlugin({ scope: "admin" }),
myFeaturePlugin, // ← its routes now require the `admin` scope
],
sessionResolver, // populates ctx.principal the guard checks
});
```
### Mixed-surface app with scopeFor
```ts
import { createRbacPlugin } from "@suaveplan/api-middleware-rbac";
createRbacPlugin({
scopeFor: (ctx) => {
const path = new URL(ctx.request.url).pathname;
if (path === "/health") return null; // public
if (path.startsWith("/admin")) return "admin";
return "user";
},
});
```
### Per-route guarding (use middleware directly)
```ts
import { rbacMiddleware } from "@suaveplan/api-middleware/rbac";
// In a route descriptor:
{
method: "GET",
path: "/admin",
handler: adminHandler,
middleware: [rbacMiddleware({ scope: "admin" })],
}
```
## Constraints
- **At least one constraint required** — `scope`, `anyOf`, or `scopeFor`; throws `RBAC_PLUGIN.INVALID_INPUT` if none provided.
- **Precedence** — `scopeFor` overrides `anyOf` overrides `scope`.
- **`null` = public** — `scopeFor` returning `null` allows the request through.
- **Global guard caveat** — also covers health checks and login routes; use `scopeFor` for mixed surfaces.
- **Declarative global middleware** — contributes `globalMiddleware`, not routes.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-core`, `@suaveplan/api-middleware`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Options
| Option | Type | Default | Notes |
|---|---|---|---|
| `scope` | `string` | — | Single required scope |
| `anyOf` | `string[]` | — | Admit principal holding any one |
| `scopeFor` | `(ctx) => string \| string[] \| null` | — | Per-request requirement; `null` ⇒ public |
## Checklist
- [ ] Read `src/middleware/rbac.ts` before modifying access-control logic
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve `scopeFor` > `anyOf` > `scope` precedence
- [ ] Ensure `null` from `scopeFor` remains a public-passthrough signal

View File

@@ -0,0 +1,127 @@
---
name: sp-api-middleware-rules
description: Platform-native rule-engine middleware for @suaveplan/api. ruleMiddleware(rule, options?) evaluates a single @suaveplan/rules Rule per request and short-circuits with 403 problem+json on denial. accessControlMiddleware(options) evaluates a Rule or Ruleset with optional result caching and a pluggable AccessControlCache interface. Both are typed as Middleware<Caps> against RequestContext — no Hono dependency. Replaces @suaveplan/rules-hono.
---
# @suaveplan/api-middleware-rules
## Purpose
Platform-native rule-engine middleware for `@suaveplan/api`. Bridges `@suaveplan/rules` into the Fetch-native API platform middleware model — no Hono or Express dependency.
- `ruleMiddleware(rule, options?)` evaluates a single `@suaveplan/rules` `Rule` per request. Pass → stores `EvaluationResult` in `ctx.state`, calls `next()`. Fail → `403 application/problem+json`.
- `accessControlMiddleware(options)` evaluates a `Rule` or `Ruleset` with optional result caching and a pluggable `AccessControlCache` interface.
Both are typed against the platform `RequestContext` and `Middleware` contracts.
## Architecture
| Component | Source | Role |
|---|---|---|
| `ruleMiddleware` | `src/rule-middleware/rule-middleware.ts` | Evaluates one `Rule` per request |
| `accessControlMiddleware` | `src/access-control/access-control.ts` | Evaluates `Rule`/`Ruleset` with optional caching |
| `MemoryAccessControlCache` | `src/access-control/access-control.ts` | In-process Map cache with per-entry TTL |
| `getDefaultEngine` | `src/engine/engine.ts` | Lazy per-process singleton `RuleEngine` |
| `RULES_MIDDLEWARE_CODES` | `src/error-codes/error-codes.ts` | `RULES_MW.*` registry |
## File Structure
```
src/
index.ts # Exports ruleMiddleware, accessControlMiddleware, caches, engine, codes, otel
rule-middleware/
rule-middleware.ts # ruleMiddleware implementation
rule-middleware.md
access-control/
access-control.ts # accessControlMiddleware + MemoryAccessControlCache
access-control.md
engine/
engine.ts # Lazy RuleEngine singleton
engine.md
error-codes/
error-codes.ts # RULES_MIDDLEWARE_CODES + RulesMiddlewareError
otel/
instruments.ts # api_middleware_rules.* OTel bundle
testing/
reset.ts # resetDefaultEngine for tests
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware-rules` | `ruleMiddleware`, `accessControlMiddleware`, `MemoryAccessControlCache`, `RULE_RESULT_KEY`, `ACCESS_RESULT_KEY`, `RULES_MIDDLEWARE_CODES`, `RulesMiddlewareError`, `getDefaultEngine`, OTel instruments |
| `@suaveplan/api-middleware-rules/testing` | `resetDefaultEngine` |
## Patterns
### Single rule guard
```ts
import { definePlugin } from "@suaveplan/api-plugin-kit/define-plugin";
import { ruleMiddleware } from "@suaveplan/api-middleware-rules";
import { rule } from "@suaveplan/rules";
const ageRule = rule("age").greaterThanOrEquals(18).build();
const plugin = definePlugin({
id: "example",
version: "1.0.0",
routes(router) {
router.post("/age-restricted", () => new Response("ok"), {
middleware: [ruleMiddleware(ageRule)],
});
},
});
```
### Access control with custom context
```ts
import { accessControlMiddleware } from "@suaveplan/api-middleware-rules";
import { rule, context } from "@suaveplan/rules";
const adminRule = rule("role").equals("admin").build();
router.get("/admin", () => new Response("admin"), {
middleware: [
accessControlMiddleware({
rules: adminRule,
extractContext: (ctx) =>
context({ role: ctx.principal?.claims["role"] ?? "user" }),
}),
],
});
```
### Caching
```ts
import { accessControlMiddleware, MemoryAccessControlCache } from "@suaveplan/api-middleware-rules";
const cache = new MemoryAccessControlCache();
const guard = accessControlMiddleware({
rules: myRule,
cache,
cacheKey: (ctx) => ctx.principal?.id ?? "anon",
cacheTtlMs: 30_000,
});
```
## Constraints
- **No Hono dependency** — pure platform `Middleware<Caps>` against `RequestContext`.
- **403 on denial** — both middlewares return `application/problem+json` on fail/error.
- **State keys** — `RULE_RESULT_KEY` and `ACCESS_RESULT_KEY` symbols store `EvaluationResult` in `ctx.state`.
- **Cache requires key** — providing `cache` without `cacheKey` throws `RulesMiddlewareError(RULES_MW.INVALID_OPTIONS)` at construction.
- **Lazy engine** — `RuleEngine` is a per-process singleton created on first use.
- **OTel metrics** — `api_middleware_rules.rule.duration_ms`, `rule_total`, `access_control.duration_ms`, `access_control_total` with `outcome` attribute.
- **Dependencies**: `@suaveplan/api-core`, `@suaveplan/api-middleware`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/rules`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`.
## Checklist
- [ ] Read `src/rule-middleware/rule-middleware.ts` before modifying rule evaluation
- [ ] Read `src/access-control/access-control.ts` before modifying caching behaviour
- [ ] Preserve Hono-free boundary — never add framework-specific imports
- [ ] Ensure `cache` without `cacheKey` remains a construction-time error

View File

@@ -0,0 +1,89 @@
---
name: sp-api-middleware
description: Shared contracts for the Suaveplan API middleware tier. Exports ApiMiddlewareError base class and API_MIDDLEWARE_CODES registry (RATE_LIMITED, UNAUTHENTICATED, FORBIDDEN, PAYLOAD_TOO_LARGE, AUDIT_SINK_FAILED) with default HTTP status mapping. Concrete middleware factories live in their own packages (api-middleware-rate-limit, rbac, cors, audit, body-limit, rules).
---
# @suaveplan/api-middleware
## Purpose
Shared contracts for the Suaveplan API middleware tier. Exports the stable `ApiMiddlewareError` base class and `API_MIDDLEWARE_CODES` registry used across all `api-middleware-*` packages.
This is a **contract-only** package — concrete middleware factories (`rateLimitMiddleware`, `rbacMiddleware`, `corsMiddleware`, `auditMiddleware`, `bodyLimitMiddleware`, `rulesMiddleware`) live in their own named packages. The redesigned pipeline order is `tenant → auth → rate-limit → rbac → audit → handler`; this package supplies the shared error vocabulary and status mappings that every middleware in that pipeline references.
## Architecture
| Component | Source | Role |
|---|---|---|
| `API_MIDDLEWARE_CODES` | `src/error-codes/error-codes.ts` | 5-code `API_MW.*` registry |
| `ApiMiddlewareError` | `src/error-codes/error-codes.ts` | Typed error class with HTTP status |
| `DEFAULT_STATUS_BY_CODE` | `src/error-codes/error-codes.ts` | Default HTTP status map (429, 401, 403, 413, 500) |
## File Structure
```
src/
index.ts # Exports API_MIDDLEWARE_CODES, ApiMiddlewareError, DEFAULT_STATUS_BY_CODE
error-codes/
error-codes.ts # Registry + error class + status map
error-codes.md
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-middleware` | `API_MIDDLEWARE_CODES`, `ApiMiddlewareError`, `ApiMiddlewareCode`, `ApiMiddlewareErrorOptions`, `DEFAULT_STATUS_BY_CODE` |
## Patterns
### Throw a middleware error
```ts
import {
API_MIDDLEWARE_CODES,
ApiMiddlewareError,
} from "@suaveplan/api-middleware";
throw new ApiMiddlewareError(
API_MIDDLEWARE_CODES.RATE_LIMITED,
"too many requests"
);
// status defaults to 429 via DEFAULT_STATUS_BY_CODE
```
### Branch on code
```ts
import { API_MIDDLEWARE_CODES } from "@suaveplan/api-middleware";
if (err.code === API_MIDDLEWARE_CODES.UNAUTHENTICATED) {
// 401
} else if (err.code === API_MIDDLEWARE_CODES.FORBIDDEN) {
// 403
}
```
## Constraints
- **Contract-only** — no middleware factories here; they live in named child packages.
- **Stable namespace** — `API_MW.*` codes never collide with plugin-level namespaces like `AUDIT_PLUGIN.*` or `CORS_PLUGIN.*`.
- **Default status mapping** — every code maps to a default HTTP status consumed by `@suaveplan/api-core` problem+json serialiser.
- **AUDIT_SINK_FAILED never thrown to client** — surfaced only on the audit sink-failure counter so a broken sink can never break a request.
- **Dependencies**: `@suaveplan/error`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Default Status | Usage |
|---|---|---|
| `API_MW.RATE_LIMITED` | 429 | Thrown by rate-limit middleware |
| `API_MW.UNAUTHENTICATED` | 401 | Thrown by RBAC middleware |
| `API_MW.FORBIDDEN` | 403 | Thrown by RBAC middleware |
| `API_MW.PAYLOAD_TOO_LARGE` | 413 | Thrown by body-limit middleware |
| `API_MW.AUDIT_SINK_FAILED` | 500 | Counted only, never thrown to client |
## Checklist
- [ ] Read `src/error-codes/error-codes.ts` before adding new codes
- [ ] Preserve contract-only boundary — never add middleware factories to this package
- [ ] Update `DEFAULT_STATUS_BY_CODE` when adding a new code

View File

@@ -0,0 +1,21 @@
---
name: api-stack-api-migrations-navigator
description: Navigator for api-stack/api-migrations. Pick a library to get detailed API guidance.
---
# Api migrations (api-stack)
Packages in `api-stack/api-migrations`.
## Libraries
### @suaveplan/api-migrations
Shared, declarative, plug-and-play migration runner for the Suaveplan API platform. A database-agnostic MigrationRunner over a narrow MigrationAdapter surface with real SHA-256 checksum validation aga...
→ Skill: `sp-api-migrations/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,155 @@
---
name: sp-api-migrations
description: Shared, declarative, plug-and-play migration runner for the Suaveplan API platform. Database-agnostic MigrationRunner over a narrow MigrationAdapter surface with SHA-256 checksum validation, AbortSignal cancellation, OTel counter + histogram per apply, and MigrationError with MIGRATION.* codes. Ships four adapters (surrealql, postgres, sqlite, database-capability) plus runPluginMigrations — the schema-on-install seam a plugin calls in start(ctx). Server-tier — Bun + Node.
---
# @suaveplan/api-migrations
## Purpose
Shared, declarative, plug-and-play migration runner for the Suaveplan API platform. A database-agnostic `MigrationRunner` over a narrow `MigrationAdapter` surface, with real SHA-256 checksum validation, `AbortSignal` cancellation between migrations, OTel counter + histogram per apply, and `MigrationError` with a stable `MIGRATION.*` code registry.
Ships four adapters (`surrealql`, `postgres`, `sqlite`, and a `database-capability` adapter over the platform's `AdminDatabaseLike`) plus `runPluginMigrations(ctx, set)` — the canonical schema-on-install seam a plugin calls in `start(ctx)` to apply its declared tables idempotently under an advisory lock.
## Architecture
| Component | Source | Role |
|---|---|---|
| `MigrationRunner` | `src/runner/runner.ts` | Dialect-agnostic core — sources pending migrations, applies in order |
| `applyMigration` | `src/runner/apply.ts` | Single-migration apply path + `computeChecksum` |
| `runPluginMigrations` | `src/plugin-migrations/plugin-migrations.ts` | Schema-on-install seam for plugins |
| `acquireLock` / `releaseLock` | `src/plugin-migrations/lock.ts` | Advisory lock serialising plugin schema-on-install |
| `createPostgresAdapter` | `src/adapters/postgres/postgres.ts` | PostgreSQL adapter |
| `createSqliteAdapter` | `src/adapters/sqlite/sqlite.ts` | SQLite adapter |
| `createSurrealqlAdapter` | `src/adapters/surrealql/surrealql.ts` | SurrealDB adapter |
| `createDatabaseCapabilityAdapter` | `src/adapters/database-capability/database-capability.ts` | Platform `AdminDatabaseLike` adapter |
| `MIGRATION_CODES` | `src/error-codes/error-codes.ts` | 7-code `MIGRATION.*` registry |
## File Structure
```
src/
index.ts # Barrel: runner, adapters, plugin-migrations, codes, otel, types
runner/
runner.ts # MigrationRunner + createMigrationRunner
apply.ts # applyMigration + computeChecksum
plugin-migrations/
plugin-migrations.ts # runPluginMigrations
lock.ts # Advisory lock
adapters/
postgres/postgres.ts
sqlite/sqlite.ts
surrealql/surrealql.ts
database-capability/database-capability.ts
error-codes/
error-codes.ts # MIGRATION_CODES + MigrationError
otel/
instruments.ts # Migration counter + histogram
types/
migration.ts # Public type surface
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-migrations` | `MigrationRunner`, `createMigrationRunner`, all adapters, `runPluginMigrations`, `MIGRATION_CODES`, `MigrationError`, types |
| `@suaveplan/api-migrations/runner/runner` | `MigrationRunner`, `createMigrationRunner`, `computeChecksum` |
| `@suaveplan/api-migrations/adapters/postgres/postgres` | `createPostgresAdapter`, `PostgresClient` |
| `@suaveplan/api-migrations/adapters/sqlite/sqlite` | `createSqliteAdapter`, `SqliteClient` |
| `@suaveplan/api-migrations/adapters/surrealql/surrealql` | `createSurrealqlAdapter`, `SurrealqlClient` |
| `@suaveplan/api-migrations/adapters/database-capability/database-capability` | `createDatabaseCapabilityAdapter` |
| `@suaveplan/api-migrations/plugin-migrations/plugin-migrations` | `runPluginMigrations` |
| `@suaveplan/api-migrations/error-codes/error-codes` | `MIGRATION_CODES`, `MigrationError` |
| `@suaveplan/api-migrations/otel/instruments` | OTel instruments |
| `@suaveplan/api-migrations/types/migration` | All public types |
## Patterns
### Generic runner from directory
```ts
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
const runner = new MigrationRunner(createSqliteAdapter(client), {
fileExtension: ".sql",
});
const results = await runner.runFromDirectory("./db/migrations");
for (const r of results) {
if (!r.success) throw new Error(`Apply halted at ${r.name}: ${r.error}`);
}
```
### Schema-on-install in a plugin
```ts
import { definePlugin } from "@suaveplan/api-plugin-kit/define-plugin";
import { runPluginMigrations } from "@suaveplan/api-migrations/plugin-migrations/plugin-migrations";
export const storePlugin = definePlugin({
id: "store",
version: "0.1.0",
requires: ["db"] as const,
routes() {},
async start(ctx) {
await runPluginMigrations(
{ db: ctx.capabilities.db },
{
name: "store",
migrations: [
{
name: "001_products",
up: "CREATE TABLE products (id text PRIMARY KEY, name text NOT NULL)",
},
],
}
);
},
});
```
### Checksum-drift recovery
```ts
import { MIGRATION_CODES, MigrationError } from "@suaveplan/api-migrations/error-codes/error-codes";
try {
await runner.runFromDirectory("./db/migrations");
} catch (err) {
if (err instanceof MigrationError && err.code === MIGRATION_CODES.CHECKSUM_DRIFT) {
// err.context = { name, expected, actual }
// Resolution: restore migration OR ship a NEW additive migration. Never rewrite history.
}
}
```
## Constraints
- **Server-tier** — uses `node:crypto` for SHA-256 and `node:fs/promises` for directory scanning.
- **Checksum validation** — `validateChecksums: true` by default; halts on drift.
- **AbortSignal support** — cancellation between migrations via `signal` option.
- **Advisory lock** — `runPluginMigrations` serialises across concurrent processes.
- **Idempotent** — second `start` is a no-op when checksums match.
- **Peer dependencies** — `surrealdb`, `postgres`, `@sqlite.org/sqlite-wasm` are optional peers.
- **Dependencies**: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `MIGRATION.TABLE_BOOTSTRAP_FAILED` | Migration tracking table creation failed |
| `MIGRATION.APPLY_FAILED` | Single migration apply failed |
| `MIGRATION.CHECKSUM_DRIFT` | Previously applied migration content changed |
| `MIGRATION.DIRECTORY_NOT_FOUND` | Migration directory does not exist |
| `MIGRATION.INVALID_FILE_NAME` | File missing numeric order prefix |
| `MIGRATION.ROLLBACK_NOT_SUPPORTED` | Rollback attempted on adapter without support |
| `MIGRATION.LOCK_CONTENDED` | Advisory lock could not be acquired |
## Checklist
- [ ] Read `src/runner/runner.ts` before modifying orchestration logic
- [ ] Read `src/runner/apply.ts` before modifying checksum/apply behaviour
- [ ] Preserve checksum validation as the default — never silently skip it
- [ ] Ensure `runPluginMigrations` advisory lock prevents race conditions

View File

@@ -0,0 +1,45 @@
---
name: api-stack-api-openapi-navigator
description: Navigator for api-stack/api-openapi. Pick a library to get detailed API guidance.
---
# Api openapi (api-stack)
Packages in `api-stack/api-openapi`.
## Libraries
### @suaveplan/api-openapi
Fetch-native OpenAPI 3.1 document generation and self-contained doc UIs (Swagger UI, Redoc, Scalar) for the Suaveplan API platform. Maps a plugin's framework-neutral RouteDescriptor[] into a determini...
→ Skill: `sp-api-openapi/SKILL.md`
### @suaveplan/api-openapi-core
Cycle-breaking foundation shared by the Fetch-native Suaveplan OpenAPI doc-UI renderer packages and the api-openapi meta-package. Owns the doc-UI shared contract (DocUiOptions / DocUiRouteOptions) plu...
→ Skill: `sp-api-openapi-core/SKILL.md`
### @suaveplan/api-openapi-ui-redoc
Self-contained Redoc doc-UI shell for the Fetch-native Suaveplan API platform. Exposes redocUiHtml / redocUiRoute / REDOC_VERSION: a complete HTML document using the <redoc spec-url> custom element pl...
→ Skill: `sp-api-openapi-ui-redoc/SKILL.md`
### @suaveplan/api-openapi-ui-scalar
Self-contained Scalar API Reference doc-UI shell for the Fetch-native Suaveplan API platform. Exposes scalarUiHtml / scalarUiRoute / SCALAR_VERSION: a complete HTML document using the <script id="api-...
→ Skill: `sp-api-openapi-ui-scalar/SKILL.md`
### @suaveplan/api-openapi-ui-swagger
Self-contained Swagger UI doc-UI shell for the Fetch-native Suaveplan API platform. Exposes swaggerUiHtml / swaggerUiRoute / SWAGGER_UI_VERSION: a complete HTML document that loads the pinned swagger-...
→ Skill: `sp-api-openapi-ui-swagger/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,87 @@
---
name: sp-api-openapi-core
description: Cycle-breaking foundation shared by the Fetch-native Suaveplan OpenAPI doc-UI renderer packages and the api-openapi meta-package. Owns the doc-UI shared contract (DocUiOptions / DocUiRouteOptions) plus resolveSpecUrl / escapeHtml / htmlRoute helpers and DEFAULT_UI_PATH / DEFAULT_SPEC_URL constants, and the API_OPENAPI.* error-code registry + ApiOpenApiError. The renderer packages and the meta-package depend on this neutral foundation instead of on each other, so no dependency cycle forms.
---
# @suaveplan/api-openapi-core
## Purpose
Cycle-breaking foundation shared by the Fetch-native Suaveplan OpenAPI doc-UI renderer packages (`@suaveplan/api-openapi-ui-{redoc,scalar,swagger}`) and the `@suaveplan/api-openapi` meta-package.
It owns two things the renderers need but neither can own without forcing a dependency cycle:
- the **doc-UI shared contract + helpers** — `DocUiOptions` / `DocUiRouteOptions`, `resolveSpecUrl`, `escapeHtml`, `htmlRoute`, and the `DEFAULT_UI_PATH` / `DEFAULT_SPEC_URL` constants;
- the **`API_OPENAPI.*` error registry** — `API_OPENAPI_CODES`, `ApiOpenApiError` — including `INVALID_SPEC_URL`, which `resolveSpecUrl` throws, and the four document-builder codes (`INVALID_INFO`, `INVALID_ROUTE_PATH`, `DUPLICATE_OPERATION`, `UNSUPPORTED_METHOD`) the `@suaveplan/api-openapi` document builder raises.
The renderer packages and the meta-package depend on this neutral package instead of on each other, so no dependency cycle forms.
## Architecture
| Component | Source | Role |
|---|---|---|
| `resolveSpecUrl` | `src/ui-shared/ui-shared.ts` | Validates and trims spec URL; throws `INVALID_SPEC_URL` |
| `escapeHtml` | `src/ui-shared/ui-shared.ts` | HTML text/attribute escaping |
| `htmlRoute` | `src/ui-shared/ui-shared.ts` | Wraps HTML shell in a public GET `text/html` RouteDescriptor |
| `API_OPENAPI_CODES` | `src/error-codes/error-codes.ts` | 5-code `API_OPENAPI.*` registry |
| `ApiOpenApiError` | `src/error-codes/error-codes.ts` | Typed error class |
## File Structure
```
src/
index.ts # Exports error codes + ui-shared helpers
ui-shared/
ui-shared.ts # DocUiOptions, DocUiRouteOptions, resolveSpecUrl, escapeHtml, htmlRoute
ui-shared.md
error-codes/
error-codes.ts # API_OPENAPI_CODES + ApiOpenApiError
error-codes.md
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-openapi-core` | `API_OPENAPI_CODES`, `ApiOpenApiError`, `resolveSpecUrl`, `escapeHtml`, `htmlRoute`, `DEFAULT_UI_PATH`, `DEFAULT_SPEC_URL`, `DocUiOptions`, `DocUiRouteOptions` |
| `@suaveplan/api-openapi-core/ui-shared` | `resolveSpecUrl`, `escapeHtml`, `htmlRoute`, `DEFAULT_UI_PATH`, `DEFAULT_SPEC_URL`, `DocUiOptions`, `DocUiRouteOptions` |
| `@suaveplan/api-openapi-core/error-codes` | `API_OPENAPI_CODES`, `ApiOpenApiError` |
## Patterns
### Build a custom doc-UI renderer
```ts
import {
escapeHtml,
htmlRoute,
resolveSpecUrl,
API_OPENAPI_CODES,
ApiOpenApiError,
} from "@suaveplan/api-openapi-core";
const specUrl = escapeHtml(resolveSpecUrl("/openapi.json"));
const route = htmlRoute("/docs", `<iframe src="${specUrl}"></iframe>`);
```
## Constraints
- **Cycle-breaker** — this package exists ONLY to prevent dependency cycles between renderers and the meta-package.
- **No CDN assets** — this package owns only contracts and helpers; HTML templates and pinned CDN versions live in the renderer packages.
- **100% coverage** — route helper exercised through real `Request`/`Response` objects.
- **Dependencies**: `@suaveplan/api-core`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `tslib`.
## Error Codes
| Code | Meaning |
|---|---|
| `API_OPENAPI.INVALID_INFO` | Document info object is invalid |
| `API_OPENAPI.INVALID_ROUTE_PATH` | Route path cannot be templatized |
| `API_OPENAPI.DUPLICATE_OPERATION` | Duplicate operationId in document |
| `API_OPENAPI.UNSUPPORTED_METHOD` | HTTP method not in OpenAPI verb set |
| `API_OPENAPI.INVALID_SPEC_URL` | Empty or whitespace spec URL |
## Checklist
- [ ] Read `src/ui-shared/ui-shared.ts` before modifying shared contract
- [ ] Read `src/error-codes/error-codes.ts` before adding new codes
- [ ] Preserve cycle-breaking boundary — never import a renderer or the meta-package

View File

@@ -0,0 +1,84 @@
---
name: sp-api-openapi-ui-redoc
description: Self-contained Redoc doc-UI shell for the Fetch-native Suaveplan API platform. Exposes redocUiHtml / redocUiRoute / REDOC_VERSION — a complete HTML document using the <redoc spec-url> custom element plus the pinned redoc standalone bundle from a CDN, wrapped in a public GET text/html RouteDescriptor. Spec-URL validation, HTML escaping, and route wrapper come from @suaveplan/api-openapi-core; this package owns only the Redoc-specific HTML template.
---
# @suaveplan/api-openapi-ui-redoc
## Purpose
Self-contained Redoc doc-UI shell for the Fetch-native Suaveplan API platform. Exposes `redocUiHtml` / `redocUiRoute` / `REDOC_VERSION`: a complete `<!doctype html>` document using the `<redoc spec-url="...">` custom element plus the pinned `redoc@2.1.5` standalone bundle from a CDN, wrapped in a public `GET` `text/html` `RouteDescriptor`.
Spec-URL validation (`resolveSpecUrl`), HTML escaping (`escapeHtml`), and the route wrapper (`htmlRoute`) come from `@suaveplan/api-openapi-core`; this package owns only the Redoc-specific HTML template and pinned asset version.
It is re-exported from `@suaveplan/api-openapi` at the `./ui/redoc` subpath, so existing `@suaveplan/api-openapi/ui/redoc` imports keep working unchanged.
## Architecture
| Component | Source | Role |
|---|---|---|
| `redocUiHtml` | `src/redoc/redoc.ts` | Builds the Redoc HTML shell |
| `redocUiRoute` | `src/redoc/redoc.ts` | Returns a public GET `text/html` RouteDescriptor |
| `REDOC_VERSION` | `src/redoc/redoc.ts` | Pinned CDN version constant (`"2.1.5"`) |
## File Structure
```
src/
index.ts # Exports REDOC_VERSION, redocUiHtml, redocUiRoute
redoc/
redoc.ts # Redoc HTML shell + route factory
redoc.md
redoc.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-openapi-ui-redoc` | `redocUiHtml`, `redocUiRoute`, `REDOC_VERSION` |
## Patterns
### Serve Redoc at /docs
```ts
import { redocUiRoute } from "@suaveplan/api-openapi-ui-redoc";
const route = redocUiRoute({ specUrl: "/openapi.json", path: "/docs" });
// route.method === "GET", serves Redoc against /openapi.json
```
### Combined with OpenAPI document route
```ts
import { buildOpenApiDocument } from "@suaveplan/api-openapi/document";
import { openApiRoute } from "@suaveplan/api-openapi/openapi-route";
import { redocUiRoute } from "@suaveplan/api-openapi-ui-redoc";
const doc = buildOpenApiDocument(routes, { title: "API", version: "1.0.0" });
const docsPlugin = {
manifest: { name: "api-docs", version: "1.0.0", apiHost: "^0.1.0", isolation: "trusted", trustLevel: "first-party", requiredCapabilities: [] },
register(reg) {
reg.addRoute(openApiRoute(doc)); // GET /openapi.json
reg.addRoute(redocUiRoute({ specUrl: "/openapi.json" })); // GET /docs
},
};
```
## Constraints
- **CDN-loaded** — Redoc bundle loaded from jsdelivr CDN; no client assets bundled.
- **Pinned version** — `REDOC_VERSION` is pinned; update when bumping.
- **HTML-escaped** — both spec URL and title are escaped before interpolation.
- **INVALID_SPEC_URL** — throws `API_OPENAPI.INVALID_SPEC_URL` for empty/whitespace spec URL.
- **Public route** — `meta.auth: null` so auth middleware skips it.
- **Depends only on core** — `@suaveplan/api-openapi-core` and `@suaveplan/api-types`; never on the meta-package.
- **Dependencies**: `@suaveplan/api-openapi-core`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/redoc/redoc.ts` before modifying the HTML template
- [ ] Preserve CDN version pinning — update `REDOC_VERSION` when bumping
- [ ] Ensure spec URL and title HTML-escaping remains safe

View File

@@ -0,0 +1,84 @@
---
name: sp-api-openapi-ui-scalar
description: Self-contained Scalar API Reference doc-UI shell for the Fetch-native Suaveplan API platform. Exposes scalarUiHtml / scalarUiRoute / SCALAR_VERSION — a complete HTML document using the <script id="api-reference" data-url> pattern plus the pinned @scalar/api-reference standalone bundle from a CDN, wrapped in a public GET text/html RouteDescriptor. Spec-URL validation, HTML escaping, and route wrapper come from @suaveplan/api-openapi-core; this package owns only the Scalar-specific HTML template.
---
# @suaveplan/api-openapi-ui-scalar
## Purpose
Self-contained Scalar API Reference doc-UI shell for the Fetch-native Suaveplan API platform. Exposes `scalarUiHtml` / `scalarUiRoute` / `SCALAR_VERSION`: a complete `<!doctype html>` document using the `<script id="api-reference" data-url="...">` pattern plus the pinned `@scalar/api-reference@1.25.28` standalone bundle from a CDN, wrapped in a public `GET` `text/html` `RouteDescriptor`.
Spec-URL validation (`resolveSpecUrl`), HTML escaping (`escapeHtml`), and the route wrapper (`htmlRoute`) come from `@suaveplan/api-openapi-core`; this package owns only the Scalar-specific HTML template and pinned asset version.
It is re-exported from `@suaveplan/api-openapi` at the `./ui/scalar` subpath, so existing `@suaveplan/api-openapi/ui/scalar` imports keep working unchanged.
## Architecture
| Component | Source | Role |
|---|---|---|
| `scalarUiHtml` | `src/scalar/scalar.ts` | Builds the Scalar HTML shell |
| `scalarUiRoute` | `src/scalar/scalar.ts` | Returns a public GET `text/html` RouteDescriptor |
| `SCALAR_VERSION` | `src/scalar/scalar.ts` | Pinned CDN version constant (`"1.25.28"`) |
## File Structure
```
src/
index.ts # Exports SCALAR_VERSION, scalarUiHtml, scalarUiRoute
scalar/
scalar.ts # Scalar HTML shell + route factory
scalar.md
scalar.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-openapi-ui-scalar` | `scalarUiHtml`, `scalarUiRoute`, `SCALAR_VERSION` |
## Patterns
### Serve Scalar at /docs
```ts
import { scalarUiRoute } from "@suaveplan/api-openapi-ui-scalar";
const route = scalarUiRoute({ specUrl: "/openapi.json", path: "/docs" });
// route.method === "GET", serves Scalar against /openapi.json
```
### Combined with OpenAPI document route
```ts
import { buildOpenApiDocument } from "@suaveplan/api-openapi/document";
import { openApiRoute } from "@suaveplan/api-openapi/openapi-route";
import { scalarUiRoute } from "@suaveplan/api-openapi-ui-scalar";
const doc = buildOpenApiDocument(routes, { title: "API", version: "1.0.0" });
const docsPlugin = {
manifest: { name: "api-docs", version: "1.0.0", apiHost: "^0.1.0", isolation: "trusted", trustLevel: "first-party", requiredCapabilities: [] },
register(reg) {
reg.addRoute(openApiRoute(doc)); // GET /openapi.json
reg.addRoute(scalarUiRoute({ specUrl: "/openapi.json" })); // GET /docs
},
};
```
## Constraints
- **CDN-loaded** — Scalar bundle loaded from jsdelivr CDN; no client assets bundled.
- **Pinned version** — `SCALAR_VERSION` is pinned; update when bumping.
- **HTML-escaped** — both spec URL and title are escaped before interpolation.
- **INVALID_SPEC_URL** — throws `API_OPENAPI.INVALID_SPEC_URL` for empty/whitespace spec URL.
- **Public route** — `meta.auth: null` so auth middleware skips it.
- **Depends only on core** — `@suaveplan/api-openapi-core` and `@suaveplan/api-types`; never on the meta-package.
- **Dependencies**: `@suaveplan/api-openapi-core`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/scalar/scalar.ts` before modifying the HTML template
- [ ] Preserve CDN version pinning — update `SCALAR_VERSION` when bumping
- [ ] Ensure spec URL and title HTML-escaping remains safe

View File

@@ -0,0 +1,84 @@
---
name: sp-api-openapi-ui-swagger
description: Self-contained Swagger UI doc-UI shell for the Fetch-native Suaveplan API platform. Exposes swaggerUiHtml / swaggerUiRoute / SWAGGER_UI_VERSION — a complete HTML document that loads the pinned swagger-ui-dist bundle + stylesheet from a CDN and calls SwaggerUIBundle({ url: specUrl }), wrapped in a public GET text/html RouteDescriptor. Spec-URL validation, HTML escaping, and route wrapper come from @suaveplan/api-openapi-core; this package owns only the Swagger-UI-specific HTML template.
---
# @suaveplan/api-openapi-ui-swagger
## Purpose
Self-contained Swagger UI doc-UI shell for the Fetch-native Suaveplan API platform. Exposes `swaggerUiHtml` / `swaggerUiRoute` / `SWAGGER_UI_VERSION`: a complete `<!doctype html>` document that loads the pinned `swagger-ui-dist@5.17.14` bundle + stylesheet from a CDN and calls `SwaggerUIBundle({ url: specUrl })`, wrapped in a public `GET` `text/html` `RouteDescriptor`.
Spec-URL validation (`resolveSpecUrl`), HTML escaping (`escapeHtml`), and the route wrapper (`htmlRoute`) come from `@suaveplan/api-openapi-core`; this package owns only the Swagger-UI-specific HTML template and pinned asset version.
It is re-exported from `@suaveplan/api-openapi` at the `./ui/swagger` subpath, so existing `@suaveplan/api-openapi/ui/swagger` imports keep working unchanged.
## Architecture
| Component | Source | Role |
|---|---|---|
| `swaggerUiHtml` | `src/swagger/swagger.ts` | Builds the Swagger UI HTML shell |
| `swaggerUiRoute` | `src/swagger/swagger.ts` | Returns a public GET `text/html` RouteDescriptor |
| `SWAGGER_UI_VERSION` | `src/swagger/swagger.ts` | Pinned CDN version constant (`"5.17.14"`) |
## File Structure
```
src/
index.ts # Exports SWAGGER_UI_VERSION, swaggerUiHtml, swaggerUiRoute
swagger/
swagger.ts # Swagger UI HTML shell + route factory
swagger.md
swagger.test.ts
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-openapi-ui-swagger` | `swaggerUiHtml`, `swaggerUiRoute`, `SWAGGER_UI_VERSION` |
## Patterns
### Serve Swagger UI at /docs
```ts
import { swaggerUiRoute } from "@suaveplan/api-openapi-ui-swagger";
const route = swaggerUiRoute({ specUrl: "/openapi.json", path: "/docs" });
// route.method === "GET", serves Swagger UI against /openapi.json
```
### Combined with OpenAPI document route
```ts
import { buildOpenApiDocument } from "@suaveplan/api-openapi/document";
import { openApiRoute } from "@suaveplan/api-openapi/openapi-route";
import { swaggerUiRoute } from "@suaveplan/api-openapi-ui-swagger";
const doc = buildOpenApiDocument(routes, { title: "API", version: "1.0.0" });
const docsPlugin = {
manifest: { name: "api-docs", version: "1.0.0", apiHost: "^0.1.0", isolation: "trusted", trustLevel: "first-party", requiredCapabilities: [] },
register(reg) {
reg.addRoute(openApiRoute(doc)); // GET /openapi.json
reg.addRoute(swaggerUiRoute({ specUrl: "/openapi.json" })); // GET /docs
},
};
```
## Constraints
- **CDN-loaded** — Swagger UI bundle + CSS loaded from jsdelivr CDN; no client assets bundled.
- **Pinned version** — `SWAGGER_UI_VERSION` is pinned; update when bumping.
- **HTML-escaped** — both spec URL and title are escaped before interpolation.
- **INVALID_SPEC_URL** — throws `API_OPENAPI.INVALID_SPEC_URL` for empty/whitespace spec URL.
- **Public route** — `meta.auth: null` so auth middleware skips it.
- **Depends only on core** — `@suaveplan/api-openapi-core` and `@suaveplan/api-types`; never on the meta-package.
- **Dependencies**: `@suaveplan/api-openapi-core`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/swagger/swagger.ts` before modifying the HTML template
- [ ] Preserve CDN version pinning — update `SWAGGER_UI_VERSION` when bumping
- [ ] Ensure spec URL and title HTML-escaping remains safe

View File

@@ -0,0 +1,104 @@
---
name: sp-api-openapi
description: Fetch-native OpenAPI 3.1 document generation and self-contained doc UIs (Swagger UI, Redoc, Scalar) for the Suaveplan API platform. Maps framework-neutral RouteDescriptor[] into a deterministic OpenAPI 3.1.0 document, serves it at /openapi.json, and exposes three pinned doc-UI shells as ./ui/* subpaths. Meta-package re-exporting doc-UI renderers from sibling packages; owns document builder and openapi-route directly.
---
# @suaveplan/api-openapi
## Purpose
Fetch-native OpenAPI 3.1 document generation and self-contained doc UIs for the Suaveplan API platform. Turns a plugin's framework-neutral `RouteDescriptor[]` into a deterministic OpenAPI 3.1.0 document, serves it as a `RouteDescriptor` at `/openapi.json`, and exposes three pinned doc-UI shells (Swagger UI, Redoc, Scalar) as `./ui/*` subpaths.
Depends on no schema runtime (`zod`/`valibot`) and no host framework — the same runtime that serves the API serves its documentation.
This is a **meta-package**: the doc-UI shared contract + helpers and the `API_OPENAPI.*` error codes live in `@suaveplan/api-openapi-core`, and the three doc-UI renderers live in `@suaveplan/api-openapi-ui-{redoc,scalar,swagger}`; this package re-exports their public surface unchanged so existing imports keep working, and continues to own the `document` builder and the `openapi-route` descriptor factory directly.
## Architecture
| Component | Source | Role |
|---|---|---|
| `buildOpenApiDocument` | `src/document/document.ts` | Routes → OpenAPI 3.1.0 document |
| `openApiRoute` | `src/openapi-route/openapi-route.ts` | GET RouteDescriptor serving document as JSON |
| `swaggerUiRoute` | re-exported from `@suaveplan/api-openapi-ui-swagger` | Swagger UI shell |
| `redocUiRoute` | re-exported from `@suaveplan/api-openapi-ui-redoc` | Redoc shell |
| `scalarUiRoute` | re-exported from `@suaveplan/api-openapi-ui-scalar` | Scalar API Reference shell |
| `API_OPENAPI_CODES` | re-exported from `@suaveplan/api-openapi-core` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: re-exports from core + UI packages, owns document + openapi-route
document/
document.ts # buildOpenApiDocument implementation
document.types.ts # OpenApiDocument, Operation, PathItem, etc.
document.md
openapi-route/
openapi-route.ts # openApiRoute factory
openapi-route.md
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-openapi` | `buildOpenApiDocument`, `openApiRoute`, `swaggerUiRoute`, `redocUiRoute`, `scalarUiRoute`, `API_OPENAPI_CODES`, `ApiOpenApiError`, doc UI types |
| `@suaveplan/api-openapi/document` | `buildOpenApiDocument` + OpenAPI types |
| `@suaveplan/api-openapi/openapi-route` | `openApiRoute`, `OpenApiRouteOptions` |
| `@suaveplan/api-openapi/error-codes` | `API_OPENAPI_CODES`, `ApiOpenApiError` |
| `@suaveplan/api-openapi/ui/swagger` | `swaggerUiRoute`, `swaggerUiHtml`, `SWAGGER_UI_VERSION` |
| `@suaveplan/api-openapi/ui/redoc` | `redocUiRoute`, `redocUiHtml`, `REDOC_VERSION` |
| `@suaveplan/api-openapi/ui/scalar` | `scalarUiRoute`, `scalarUiHtml`, `SCALAR_VERSION` |
| `@suaveplan/api-openapi/ui/shared` | Shared spec-URL validation, escaping, route wrapper |
## Patterns
### Serve OpenAPI document + Swagger UI
```ts
import { createApi, type Plugin, type RouteRegistrar } from "@suaveplan/api";
import { buildOpenApiDocument } from "@suaveplan/api-openapi/document";
import { openApiRoute } from "@suaveplan/api-openapi/openapi-route";
import { swaggerUiRoute } from "@suaveplan/api-openapi/ui/swagger";
const doc = buildOpenApiDocument(myRoutes, { title: "API", version: "1.0.0" });
const docsPlugin: Plugin = {
manifest: { name: "api-docs", version: "1.0.0", apiHost: "^0.1.0", isolation: "trusted", trustLevel: "first-party", requiredCapabilities: [] },
register(reg: RouteRegistrar) {
reg.addRoute(openApiRoute(doc)); // GET /openapi.json
reg.addRoute(swaggerUiRoute({ specUrl: "/openapi.json" })); // GET /docs
},
};
const runtime = createApi({ plugins: [docsPlugin], capabilities: [] });
await runtime.start("api-docs");
```
### Document builder only
```ts
import { buildOpenApiDocument } from "@suaveplan/api-openapi/document";
const doc = buildOpenApiDocument(routes, {
title: "My API",
version: "1.0.0",
});
```
## Constraints
- **Meta-package** — doc-UI renderers and error codes live in sibling packages; this package re-exports them unchanged.
- **No schema runtime dependency** — works with structural JSON-Schema embedding; generic fallback when none exposed.
- **Deterministic output** — same routes always produce the same document.
- **`:param` → `{param}`** — Express-style path params converted to OpenAPI templating.
- **Auth → bearer security** — platform auth schemes mapped to OpenAPI security definitions.
- **Public route** — `openApiRoute` marks `meta.auth: null` so auth middleware skips it.
- **Dependencies**: `@suaveplan/api-core`, `@suaveplan/api-openapi-core`, `@suaveplan/api-openapi-ui-redoc`, `@suaveplan/api-openapi-ui-scalar`, `@suaveplan/api-openapi-ui-swagger`, `@suaveplan/api-types`, `tslib`.
## Checklist
- [ ] Read `src/document/document.ts` before modifying OpenAPI generation
- [ ] Read `src/openapi-route/openapi-route.ts` before changing route semantics
- [ ] Preserve meta-package boundary — do not inline sibling package code
- [ ] Ensure deterministic output — same input must produce identical documents

View File

@@ -0,0 +1,225 @@
---
name: api-stack-api-plugins-navigator
description: Navigator for api-stack/api-plugins. Pick a library to get detailed API guidance.
---
# Api plugins (api-stack)
Packages in `api-stack/api-plugins`.
## Libraries
### @suaveplan/api-plugin-admin
Admin / moderation plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /admin/users, /admin/posts, /adm...
→ Skill: `sp-api-plugin-admin/SKILL.md`
### @suaveplan/api-plugin-analytics
Product-analytics plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /analytics (track, track/batch, q...
→ Skill: `sp-api-plugin-analytics/SKILL.md`
### @suaveplan/api-plugin-asset-register
Fixed-asset lifecycle plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /assets, persisting to its ow...
→ Skill: `sp-api-plugin-asset-register/SKILL.md`
### @suaveplan/api-plugin-audit
Audit-trail plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: bridges @suaveplan/audit-logging (chain-integrity, GDPR export, retention) onto t...
→ Skill: `sp-api-plugin-audit/SKILL.md`
### @suaveplan/api-plugin-auth
Better-Auth-first authentication plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts 9 typed auth routes plus a Better-Auth wildcard catch...
→ Skill: `sp-api-plugin-auth/SKILL.md`
### @suaveplan/api-plugin-banking
Bank + bank-account domain plugin for the Fetch-native @suaveplan/api platform, with default-account auto-provisioning on signup. Authored with @suaveplan/api-plugin-kit definePlugin: contributes type...
→ Skill: `sp-api-plugin-banking/SKILL.md`
### @suaveplan/api-plugin-campaigns
Campaigns + suppression + delivery-webhooks plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes 18 typed RouteDescriptors across /cam...
→ Skill: `sp-api-plugin-campaigns/SKILL.md`
### @suaveplan/api-plugin-cms
Tenant-scoped headless CMS plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: 11 typed routes for content models, entries, status lifecycle, and...
→ Skill: `sp-api-plugin-cms/SKILL.md`
### @suaveplan/api-plugin-customs
Cross-border customs / compliance plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /customs/declarat...
→ Skill: `sp-api-plugin-customs/SKILL.md`
### @suaveplan/api-plugin-database
SurrealDB-backed database plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: pools a real @suaveplan/api-db-surreal SurrealDatabase (AdminDataba...
→ Skill: `sp-api-plugin-database/SKILL.md`
### @suaveplan/api-plugin-event-bus
Event-bus plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin (requires events): wraps @suaveplan/events (pub/sub, async dispatch, interceptors, b...
→ Skill: `sp-api-plugin-event-bus/SKILL.md`
### @suaveplan/api-plugin-events
Event-management plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts 10 typed RouteDescriptor routes (list / get / create / update / delet...
→ Skill: `sp-api-plugin-events/SKILL.md`
### @suaveplan/api-plugin-export
Data-export plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors at /exports (list, create, get, delete, stream...
→ Skill: `sp-api-plugin-export/SKILL.md`
### @suaveplan/api-plugin-file-upload
Multipart + chunked file-upload plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: six auth-gated typed routes (requireAuth from @suaveplan/api-...
→ Skill: `sp-api-plugin-file-upload/SKILL.md`
### @suaveplan/api-plugin-gtap
Global Trade Automation Platform (GTAP) plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /gtap/licen...
→ Skill: `sp-api-plugin-gtap/SKILL.md`
### @suaveplan/api-plugin-import
Tabular-data ingest plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: CSV / JSON / TSV-XLSX parsers, declarative transform pipeline (coerce + v...
→ Skill: `sp-api-plugin-import/SKILL.md`
### @suaveplan/api-plugin-inventory
Inventory-management plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /inventory/{items,warehouses,s...
→ Skill: `sp-api-plugin-inventory/SKILL.md`
### @suaveplan/api-plugin-kit
Plugin-authoring toolkit for the Suaveplan API platform. Provides definePlugin (a typed authoring factory that normalises a friendly spec into a @suaveplan/api Plugin and narrows ctx.capabilities to t...
→ Skill: `sp-api-plugin-kit/SKILL.md`
### @suaveplan/api-plugin-logistics
Shipment / carrier / transport-leg logistics plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /logis...
→ Skill: `sp-api-plugin-logistics/SKILL.md`
### @suaveplan/api-plugin-messaging
In-app messaging plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /messaging/threads, /messaging/thr...
→ Skill: `sp-api-plugin-messaging/SKILL.md`
### @suaveplan/api-plugin-notifications
Notifications + push plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts 15 typed RouteDescriptor routes (10 /notifications CRUD + 5 /push...
→ Skill: `sp-api-plugin-notifications/SKILL.md`
### @suaveplan/api-plugin-payment
Payment-lifecycle plugin for the Fetch-native @suaveplan/api platform. Wraps @suaveplan/payment + a caller-injected PaymentProvider (Stripe/Adyen/PayPal/Square/Mollie/custom) as an @suaveplan/api-plug...
→ Skill: `sp-api-plugin-payment/SKILL.md`
### @suaveplan/api-plugin-port-tracking
Maritime port-tracking plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /port-tracking/vessels, /por...
→ Skill: `sp-api-plugin-port-tracking/SKILL.md`
### @suaveplan/api-plugin-procurement
Procurement plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: suppliers, requisitions, purchase orders + line items, goods-received notes (GRN)...
→ Skill: `sp-api-plugin-procurement/SKILL.md`
### @suaveplan/api-plugin-search
Full-text search plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts 9 typed RouteDescriptor routes (query / suggest / list-indexes / crea...
→ Skill: `sp-api-plugin-search/SKILL.md`
### @suaveplan/api-plugin-site
White-label multi-site / multi-domain management plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes public + private typed RouteDesc...
→ Skill: `sp-api-plugin-site/SKILL.md`
### @suaveplan/api-plugin-social
Social graph + activity-feed plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /social (follow, feed,...
→ Skill: `sp-api-plugin-social/SKILL.md`
### @suaveplan/api-plugin-store
E-commerce storefront plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /store/products, /store/categ...
→ Skill: `sp-api-plugin-store/SKILL.md`
### @suaveplan/api-plugin-subscription
Subscription-billing plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, it owns its persistence (plan, subscription, invoice, invoice_line, mete...
→ Skill: `sp-api-plugin-subscription/SKILL.md`
### @suaveplan/api-plugin-tenant
Multi-tenancy plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: pairs a TenantResolverChain (the global ctx.tenant/ctx.tenantId seam, built fro...
→ Skill: `sp-api-plugin-tenant/SKILL.md`
### @suaveplan/api-plugin-user
User-profile CRUD plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts /users/me GET/PATCH/DELETE under a configurable prefix (default /use...
→ Skill: `sp-api-plugin-user/SKILL.md`
### @suaveplan/api-plugin-vatdash-bridge
VAT-dashboard bridge plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /vatdash/transactions, /vatdas...
→ Skill: `sp-api-plugin-vatdash-bridge/SKILL.md`
### @suaveplan/api-plugin-vault
Per-user vault HTTP plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, it contributes 5 typed RouteDescriptor CRUD routes over /vault/items, wra...
→ Skill: `sp-api-plugin-vault/SKILL.md`
### @suaveplan/api-plugin-warehouse-ops
Warehouse-operations plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: contributes typed RouteDescriptors across /warehouse/{zones,aisles,bins,...
→ Skill: `sp-api-plugin-warehouse-ops/SKILL.md`
### @suaveplan/api-plugin-webhook
Signed outbound webhook delivery plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin: mounts 6 typed RouteDescriptor CRUD + test routes under a co...
→ Skill: `sp-api-plugin-webhook/SKILL.md`
## Common composition patterns
- Check sibling packages in this subcategory for adapters and plugins.
- Many packages expose subpath exports; check `package.json` `exports` before importing deep paths.

View File

@@ -0,0 +1,159 @@
---
name: sp-api-plugin-admin
description: Admin / moderation plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, it contributes 15 typed RouteDescriptors across /admin/users, /admin/posts, /admin/sites, /admin/stats, and /admin/audit, persists moderation state plus an append-only audit log to its own SurrealDB tables via an injected SurrealAdapter, reads ctx.principal + ctx.tenantId, and applies its schema in start(ctx). Every route gated by requireAuth + requireRole from @suaveplan/api-plugin-auth.
---
# @suaveplan/api-plugin-admin
## Purpose
Admin / moderation plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes 15 typed `RouteDescriptor`s across `/admin/users`, `/admin/posts`, `/admin/sites`, `/admin/stats`, and `/admin/audit`, persists moderation state plus an append-only audit log to its OWN SurrealDB tables (via an injected `SurrealAdapter`), reads `ctx.principal` + `ctx.tenantId`, and applies its schema in `start(ctx)`. Every route is gated by `requireAuth()` + `requireRole(adminRole)` from `@suaveplan/api-plugin-auth`.
The plugin owns its persistence — three SurrealDB repositories built from one explicit `SurrealAdapter`. No `ServiceRegistry`, no `db` capability. It replaces the old Hono-coupled admin service with a platform-native plugin.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAdminPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createAdmin` | `src/plugin/plugin.ts` | Returns `{ plugin, moderation, site, audit }` for live repository access |
| `buildAllAdminRoutes` | `src/routes/all-routes/all-routes.ts` | Concatenates all 15 route descriptors |
| `SurrealModerationRepository` | `src/repositories/moderation/moderation.ts` | User/post moderation state persistence |
| `SurrealSiteMetadataRepository` | `src/repositories/site/site.ts` | Site metadata persistence |
| `SurrealAuditRepository` | `src/repositories/audit/audit.ts` | Append-only audit log |
| `applyAdminSchema` | `src/schema/schema.ts` | SurrealQL DDL applied in `start(ctx)` |
| `ADMIN_API_CODES` | `src/error-codes/error-codes.ts` | 12-code `ADMIN_API.*` registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, repositories, routes, schemas, codes, types
plugin/
plugin.ts # createAdminPlugin, createAdmin, AdminWiring
routes/
all-routes/ # buildAllAdminRoutes — concatenates 5 route groups
users/ # ban, suspend, unsuspend, unban, role, read, list
posts/ # approve, remove, list
sites/ # list, read, upsert
stats/ # dashboard aggregate
audit/ # query audit log
repositories/
moderation/ # SurrealModerationRepository + InMemoryModerationStore
site/ # SurrealSiteMetadataRepository + InMemorySiteMetadataStore
audit/ # SurrealAuditRepository + InMemoryAuditStore
shared/ # Row mappers, query helpers, shared types
schema/
schema.ts # SurrealQL DDL + applyAdminSchema
error-codes/
error-codes.ts # ADMIN_API_CODES + AdminApiError
otel/
instruments.ts # admin_api.* OTel bundle
middleware/
require-tenant.ts # Tenant guard + requireAdminActor
schemas/
options.ts # AdminPluginOptionsSchema
types/
types.ts # Public compile-time types
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-admin` | `createAdminPlugin`, `createAdmin`, repositories, routes, schemas, codes, types |
| `@suaveplan/api-plugin-admin/plugin` | `createAdminPlugin`, `createAdmin`, `AdminWiring` |
| `@suaveplan/api-plugin-admin/routes` | All route builders + schemas |
| `@suaveplan/api-plugin-admin/repositories` | All repository classes + types |
| `@suaveplan/api-plugin-admin/middleware` | `requireAuth`, `requireRole`, `requireAdminActor` |
| `@suaveplan/api-plugin-admin/error-codes` | `ADMIN_API_CODES`, `AdminApiError` |
| `@suaveplan/api-plugin-admin/otel` | OTel instruments |
| `@suaveplan/api-plugin-admin/schemas` | `AdminPluginOptionsSchema` |
| `@suaveplan/api-plugin-admin/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { SurrealAdapter } from "@suaveplan/surrealdb";
import { createAdminPlugin } from "@suaveplan/api-plugin-admin";
const db = new SurrealAdapter({ url, namespace, database, username, password });
await db.connect();
const plugin = createAdminPlugin({ db });
const runtime = createApi({
plugins: [authPlugin, plugin],
sessionResolver,
tenantResolver,
});
await runtime.start("auth");
await runtime.start("admin"); // applies schema, then serves routes
```
### Access live repositories
```ts
import { createAdmin } from "@suaveplan/api-plugin-admin";
const { plugin, moderation, site, audit } = createAdmin({ db });
// moderation, site, audit are the live Surreal repository instances
```
## Routes
All routes are tenant-scoped (`ctx.tenantId`) and gated by `requireAuth()` + `requireRole(adminRole)`.
| Method | Path | Operation |
|---|---|---|
| GET | `/admin/users` | list moderation records |
| GET | `/admin/users/:id` | read one user's moderation state |
| POST | `/admin/users/:id/suspend` | suspend user |
| POST | `/admin/users/:id/unsuspend` | lift suspension |
| POST | `/admin/users/:id/ban` | ban user |
| POST | `/admin/users/:id/unban` | lift ban |
| POST | `/admin/users/:id/role` | promote/demote role |
| GET | `/admin/posts` | moderation queue |
| POST | `/admin/posts/:id/approve` | approve flagged post |
| POST | `/admin/posts/:id/remove` | remove flagged post |
| GET | `/admin/sites` | list site metadata |
| GET | `/admin/sites/:id` | read one site's metadata |
| PUT | `/admin/sites/:id` | create-or-replace site metadata |
| GET | `/admin/stats` | dashboard aggregate counts |
| GET | `/admin/audit` | query the audit log |
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`; no browser runtime.
- **Owns its persistence** — three SurrealDB tables: `admin_user_action`, `admin_post_action`, `admin_site_metadata`, `admin_audit_log`.
- **Schema-on-install** — `applyAdminSchema(db)` run in `start(ctx)`.
- **In-memory variants** — each repository has an `InMemory*Store` for testing.
- **12 error codes** — `ADMIN_API.*` namespace with `status`-bearing `AdminApiError`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Error Codes
| Code | Meaning |
|---|---|
| `ADMIN_API.INVALID_OPTIONS` | Plugin options invalid |
| `ADMIN_API.INVALID_INPUT` | Request input invalid |
| `ADMIN_API.TENANT_REQUIRED` | Tenant not resolved |
| `ADMIN_API.USER_NOT_FOUND` | Target user does not exist |
| `ADMIN_API.POST_NOT_FOUND` | Target post does not exist |
| `ADMIN_API.SITE_NOT_FOUND` | Target site does not exist |
| `ADMIN_API.INVALID_ROLE` | Role outside configured vocabulary |
| `ADMIN_API.INVALID_STATE_TRANSITION` | Illegal moderation state change |
| `ADMIN_API.ACTION_FORBIDDEN` | Caller lacks required role |
| `ADMIN_API.DUPLICATE_SITE_METADATA` | Site metadata already exists |
| `ADMIN_API.DB_UNAVAILABLE` | Database connection failed |
| `ADMIN_API.INTERNAL_ERROR` | Unexpected internal failure |
## Checklist
- [ ] Read `src/plugin/plugin.ts` before modifying plugin wiring
- [ ] Read `src/routes/all-routes/all-routes.ts` before adding/removing routes
- [ ] Preserve tenant-scoping on every route
- [ ] Ensure requireAuth + requireRole guards remain on all mutations
- [ ] Keep schema-on-install idempotent

View File

@@ -0,0 +1,133 @@
---
name: sp-api-plugin-analytics
description: Product-analytics plugin for the Fetch-native @suaveplan/api platform. Ingests analytics events and serves aggregate / funnel / cohort reporting, persisting to its own SurrealDB tables via an injected SurrealAdapter (plugin OWNS its persistence) with an in-memory ring-buffer fallback for dev/test. Authored with @suaveplan/api-plugin-kit definePlugin. PII masking on ingest via @suaveplan/data-masking. Server-tier.
---
# @suaveplan/api-plugin-analytics
## Purpose
Product-analytics plugin for the Fetch-native `@suaveplan/api` platform. Ingests analytics events and serves an aggregate / funnel / cohort reporting surface, persisting to its own SurrealDB tables via an injected `SurrealAdapter` (the plugin OWNS its persistence) — with an in-memory ring-buffer fallback for dev and test.
Authored with `@suaveplan/api-plugin-kit` `definePlugin`. Server-tier, version `0.1.0`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAnalyticsPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createAnalytics` | `src/plugin/plugin.ts` | Returns `{ plugin, service, storage }` for live access |
| `AnalyticsService` | `src/analytics/analytics.ts` | Ingestion and aggregation core |
| `FunnelAnalyzer` | `src/funnel/funnel.ts` | Ordered conversion funnel computation |
| `CohortAnalyzer` | `src/cohort/cohort.ts` | Retention cohort analysis |
| `SurrealStorageAdapter` | `src/storage/surreal-storage.ts` | Production storage backend |
| `MemoryStorageAdapter` | `src/storage/memory-storage.ts` | Dev/test bounded ring buffer |
| `createPropertyMasker` | `src/masking/masking.ts` | PII masking on ingestion |
| `ANALYTICS_API_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, service, analysers, storage, codes, types
plugin/
plugin.ts # createAnalyticsPlugin, createAnalytics
analytics/
analytics.ts # AnalyticsService — ingestion + aggregation core
funnel/
funnel.ts # FunnelAnalyzer
cohort/
cohort.ts # CohortAnalyzer
storage/
surreal-storage.ts # SurrealStorageAdapter
memory-storage.ts # MemoryStorageAdapter
masking/
masking.ts # PII property masker
routes/
all-routes/ # buildAllAnalyticsRoutes
track/ # POST /analytics/track, /analytics/track/batch
query/ # POST /analytics/query/aggregate, /query/funnel, /query/cohort
admin/ # DELETE /analytics/purge, GET /analytics/stats
schema/
schema.ts # SurrealQL DDL + applyAnalyticsSchema
error-codes/
error-codes.ts # ANALYTICS_API_CODES + AnalyticsApiError
otel/
instruments.ts # analytics_api.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-analytics` | `createAnalyticsPlugin`, `createAnalytics`, `AnalyticsService`, `FunnelAnalyzer`, `CohortAnalyzer`, storage adapters, codes, types |
| `@suaveplan/api-plugin-analytics/plugin` | `createAnalyticsPlugin`, `createAnalytics`, `AnalyticsWiring` |
| `@suaveplan/api-plugin-analytics/analytics` | `AnalyticsService`, `dateToPeriodKey` |
| `@suaveplan/api-plugin-analytics/routes` | `buildAllAnalyticsRoutes`, `RouteDeps` |
| `@suaveplan/api-plugin-analytics/storage` | `SurrealStorageAdapter`, `MemoryStorageAdapter` |
| `@suaveplan/api-plugin-analytics/error-codes` | `ANALYTICS_API_CODES`, `AnalyticsApiError` |
| `@suaveplan/api-plugin-analytics/otel` | OTel instruments |
| `@suaveplan/api-plugin-analytics/schemas` | `AnalyticsPluginOptionsSchema` |
| `@suaveplan/api-plugin-analytics/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { SurrealAdapter } from "@suaveplan/surrealdb";
import { createAnalytics } from "@suaveplan/api-plugin-analytics";
const db = new SurrealAdapter({ url: "ws://localhost:8000" });
await db.connect();
const { plugin } = createAnalytics({
db,
adminRole: "analytics-admin",
maskRules: [{ fields: ["email"], strategy: "email" }],
});
const runtime = createApi({
plugins: [authPlugin, plugin],
sessionResolver,
tenantResolver,
});
await runtime.start("analytics");
```
### Dev mode (no DB)
```ts
const { plugin } = createAnalytics({ maskRules: [] });
// Uses MemoryStorageAdapter automatically
```
## Routes
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/analytics/track` | authenticated | Track one event |
| POST | `/analytics/track/batch` | authenticated | Track batch (idempotent, 1–1000) |
| POST | `/analytics/query/aggregate` | authenticated | Time-series rollup |
| POST | `/analytics/query/funnel` | authenticated | Conversion funnel |
| POST | `/analytics/query/cohort` | authenticated | Retention cohort |
| DELETE | `/analytics/purge` | admin role | Delete tenant events |
| GET | `/analytics/stats` | admin role | Per-tenant storage stats |
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`.
- **Owns its persistence** — `analytics_event` SurrealDB table with indexes.
- **Pluggable storage** — `StorageAdapter` port with SurrealDB and in-memory implementations.
- **PII masking** — event `properties` masked on ingest via `@suaveplan/data-masking`.
- **Schema-on-install** — `applyAnalyticsSchema(db)` in `start(ctx)`.
- **100% coverage** — Istanbul-gated on every metric.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/data-masking`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/analytics/analytics.ts` before modifying ingestion/aggregation
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve PII masking — never persist raw personal data
- [ ] Ensure MemoryStorageAdapter remains bounded (ring buffer)

View File

@@ -0,0 +1,128 @@
---
name: sp-api-plugin-asset-register
description: Fixed-asset lifecycle plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, contributes 15 typed RouteDescriptors under /assets, persisting to its own SurrealDB tables via four SurrealDB-backed repositories from one injected SurrealAdapter (plugin OWNS its persistence). Computes depreciation (straight-line, reducing-balance, units-of-activity), renders asset-tag QR labels with @suaveplan/qrcode, manages maintenance records and cost-centre assignments. Server-tier.
---
# @suaveplan/api-plugin-asset-register
## Purpose
Fixed-asset lifecycle plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes 15 typed `RouteDescriptor`s under `/assets`, persisting to its own SurrealDB tables (`asset`, `depreciation_schedule`, `maintenance_record`, `asset_assignment`) via four SurrealDB-backed repositories constructed from one injected `SurrealAdapter` — the plugin OWNS its persistence.
It computes depreciation (straight-line, reducing-balance, units-of-activity) with `@suaveplan/datetime` date math, renders asset-tag QR labels with `@suaveplan/qrcode`, and manages maintenance records and cost-centre assignments.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAssetRegisterPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createAssetRegister` | `src/plugin/plugin.ts` | Returns `{ plugin, repositories }` for live access |
| `calculateDepreciation` | `src/depreciation/depreciation.ts` | Pure depreciation engine |
| `SurrealAssetRepository` | `src/repositories/asset/asset.ts` | Asset CRUD persistence |
| `SurrealDepreciationRepository` | `src/repositories/depreciation/depreciation.ts` | Depreciation schedule persistence |
| `SurrealMaintenanceRepository` | `src/repositories/maintenance/maintenance.ts` | Maintenance record persistence |
| `SurrealAssignmentRepository` | `src/repositories/assignment/assignment.ts` | Cost-centre assignment persistence |
| `ASSET_REGISTER_API_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, repositories, routes, depreciation, codes, types
plugin/
plugin.ts # createAssetRegisterPlugin, createAssetRegister
depreciation/
depreciation.ts # calculateDepreciation (pure)
repositories/
asset/ # SurrealAssetRepository + InMemoryAssetStore
depreciation/ # SurrealDepreciationRepository + InMemoryDepreciationStore
maintenance/ # SurrealMaintenanceRepository + InMemoryMaintenanceStore
assignment/ # SurrealAssignmentRepository + InMemoryAssignmentStore
shared/ # Row mappers, query helpers
routes/
all-routes/ # buildAllAssetRegisterRoutes
assets/ # CRUD, depreciation, maintenance, assignment, label routes
schema/
schema.ts # SurrealQL DDL + applyAssetRegisterSchema
error-codes/
error-codes.ts # ASSET_REGISTER_API_CODES + AssetRegisterApiError
otel/
instruments.ts # asset_register_api.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-asset-register` | `createAssetRegisterPlugin`, `createAssetRegister`, repositories, routes, depreciation, codes, types |
| `@suaveplan/api-plugin-asset-register/plugin` | `createAssetRegisterPlugin`, `createAssetRegister`, `AssetRegisterWiring` |
| `@suaveplan/api-plugin-asset-register/depreciation` | `calculateDepreciation`, `DepreciationContext`, `DepreciationResult` |
| `@suaveplan/api-plugin-asset-register/routes` | `buildAllAssetRegisterRoutes`, `buildAssetRoutes`, `RouteDeps` |
| `@suaveplan/api-plugin-asset-register/repositories/asset` | `SurrealAssetRepository`, `InMemoryAssetStore`, types |
| `@suaveplan/api-plugin-asset-register/error-codes` | `ASSET_REGISTER_API_CODES`, `AssetRegisterApiError` |
| `@suaveplan/api-plugin-asset-register/otel` | OTel instruments |
| `@suaveplan/api-plugin-asset-register/schemas` | `AssetRegisterPluginOptionsSchema` |
| `@suaveplan/api-plugin-asset-register/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createAuth } from "@suaveplan/api-plugin-auth";
import { createAssetRegister } from "@suaveplan/api-plugin-asset-register";
import { SurrealAdapter } from "@suaveplan/surrealdb";
const db = new SurrealAdapter({ url: "ws://localhost:8000" });
await db.connect();
const { plugin: authPlugin, sessionResolver } = createAuth({ betterAuth });
const { plugin } = createAssetRegister({ db });
const runtime = createApi({
plugins: [authPlugin, plugin],
sessionResolver,
tenantResolver,
});
await runtime.start("auth");
await runtime.start("asset-register");
```
## Routes
All routes are auth-gated and tenant-scoped. Default mount prefix `/`.
| Method | Path | Operation |
|---|---|---|
| GET | `/assets` | list assets |
| POST | `/assets` | create asset |
| GET | `/assets/:id` | read one asset |
| PATCH | `/assets/:id` | update asset |
| POST | `/assets/:id/dispose` | dispose asset |
| GET | `/assets/:id/label` | QR SVG label |
| GET | `/assets/:id/depreciation` | compute depreciation snapshot |
| GET | `/assets/:id/depreciation/history` | depreciation audit trail |
| GET | `/assets/:id/maintenance` | list maintenance records |
| POST | `/assets/:id/maintenance` | schedule maintenance |
| PATCH | `/assets/:id/maintenance/:maintenanceId` | complete maintenance |
| DELETE | `/assets/:id/maintenance/:maintenanceId` | cancel maintenance |
| GET | `/assets/:id/assignments` | list assignments |
| POST | `/assets/:id/assignments` | assign to cost centre |
| POST | `/assets/:id/assignments/:assignmentId/return` | return assignment |
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`.
- **Owns its persistence** — four SurrealDB tables: `asset`, `depreciation_schedule`, `maintenance_record`, `asset_assignment`.
- **Depreciation methods** — straight-line, reducing-balance, units-of-activity.
- **Lifecycle enforcement** — disposed assets reject further updates (409); invalid maintenance transitions raise `MAINTENANCE_CONFLICT` (409); at most one active assignment per asset.
- **In-memory variants** — each repository has an `InMemory*Store` for testing.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/datetime`, `@suaveplan/error`, `@suaveplan/qrcode`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/depreciation/depreciation.ts` before modifying depreciation logic
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve lifecycle state-machine constraints
- [ ] Ensure disposed assets remain immutable

View File

@@ -0,0 +1,131 @@
---
name: sp-api-plugin-audit
description: Audit-trail plugin for the Fetch-native @suaveplan/api platform. Bridges @suaveplan/audit-logging (chain-integrity, GDPR export, retention) onto the platform's @suaveplan/api-middleware auditMiddleware as a write-through AuditSink, and mounts typed, auth-gated audit-trail routes (query, GDPR export, retention, chain verification, semantic log) under /audit gated via @suaveplan/api-plugin-auth requireRole. Sink is caller-supplied; default is in-memory for dev. Server-tier.
---
# @suaveplan/api-plugin-audit
## Purpose
Audit-trail plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it bridges `@suaveplan/audit-logging` (chain integrity, GDPR export, retention) onto the platform's `@suaveplan/api-middleware` `auditMiddleware` as a write-through `AuditSink` — the per-request capture seam — and mounts five typed, auth-gated audit-trail routes under a configurable prefix (default `/audit`).
The sink is caller-supplied (any `AuditStorageAdapter`); the default is an in-memory adapter for development. Server-tier — runs in Bun and Node.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAuditPlugin` | `src/plugin/plugin.ts` | Plugin factory — returns `{ plugin, middleware, service }` |
| `createAuditService` | `src/service/audit-service.ts` | Stable `AuditService` over `AuditLogger` |
| `createAuditLoggerSink` | `src/sink/audit-sink.ts` | Platform `auditMiddleware` → `AuditLogger` bridge |
| `createAuditMetadataFn` | `src/sink/metadata.ts` | Redacted per-request metadata extractor |
| `buildAuditRoutes` | `src/routes/build-routes.ts` | Five typed audit-trail RouteDescriptors |
| `redactPayload` | `src/redact/redact.ts` | Payload redactor |
| `AUDIT_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: createAuditPlugin, service, sink, routes, redact, codes, types
plugin/
plugin.ts # createAuditPlugin factory
service/
audit-service.ts # createAuditService
sink/
audit-sink.ts # createAuditLoggerSink bridge
metadata.ts # createAuditMetadataFn
routes/
build-routes.ts # buildAuditRoutes — 5 typed routes
routes.schemas.ts # Zod schemas for route boundaries
redact/
redact.ts # redactPayload, DEFAULT_REDACTION_RULES
options/
options.ts # Zod options boundary
error-codes/
error-codes.ts # AUDIT_PLUGIN_CODES + AuditPluginError
otel/
instruments.ts # audit.* OTel bundle
types/
types.ts # Public type surface
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-audit` | `createAuditPlugin`, `createAuditService`, `createAuditLoggerSink`, `createAuditMetadataFn`, `redactPayload`, route schemas, codes, types |
| `@suaveplan/api-plugin-audit/plugin` | `createAuditPlugin`, `AuditWiring` |
| `@suaveplan/api-plugin-audit/service` | `createAuditService` |
| `@suaveplan/api-plugin-audit/sink` | `createAuditLoggerSink`, `createAuditMetadataFn` |
| `@suaveplan/api-plugin-audit/routes` | `buildAuditRoutes`, `AuditRouteDeps`, route schemas |
| `@suaveplan/api-plugin-audit/redact` | `redactPayload`, `DEFAULT_REDACTION_RULES`, `REDACTED` |
| `@suaveplan/api-plugin-audit/options` | `AuditPluginOptionsSchema`, defaults |
| `@suaveplan/api-plugin-audit/error-codes` | `AUDIT_PLUGIN_CODES`, `AuditPluginError` |
| `@suaveplan/api-plugin-audit/otel` | OTel instruments |
| `@suaveplan/api-plugin-audit/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createAuth } from "@suaveplan/api-plugin-auth";
import { createAuditPlugin } from "@suaveplan/api-plugin-audit";
const { plugin: authPlugin, sessionResolver } = createAuth({ auth });
const { plugin: auditPlugin, middleware } = createAuditPlugin({
secretKey: process.env.AUDIT_SECRET_KEY,
samplingRate: 1,
excludePaths: ["/health", "/openapi.json"],
});
const runtime = createApi({
plugins: [authPlugin, auditPlugin],
middleware: [middleware],
sessionResolver,
});
```
### Record a semantic event
```ts
await runtime.fetch(new Request("http://host/audit/events", {
method: "POST",
headers: { cookie: "<session>", "content-type": "application/json" },
body: JSON.stringify({
type: "user.deleted",
severity: "warning",
actor: { userId: "u1" },
target: { type: "user", id: "u2" },
}),
}));
```
## Routes
| Method + path | Role | Operation |
|---|---|---|
| `POST /audit/events` | `audit:write` | Record semantic audit event |
| `GET /audit/events` | `audit:read` | Query the trail |
| `POST /audit/verify` | `audit:read` | Verify cryptographic chain |
| `GET /audit/export/:userId` | `audit:read` | GDPR data export |
| `POST /audit/retention` | `audit:write` | Apply retention policy |
## Constraints
- **Server-tier** — runs in Bun and Node.
- **Write-through sink** — bridges platform `auditMiddleware` to `@suaveplan/audit-logging`.
- **Never throws into request** — sink failures logged at warn and counted; never break a request.
- **Configurable redaction** — case-insensitive headers, exact query/body names.
- **Head-end sampling** — caller-supplied RNG; excluded paths skip entirely.
- **4 OTel instruments** — `audit.requests_recorded_total`, `requests_excluded_total`, `sink_failures_total`, `middleware_duration_ms`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-middleware-audit`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/audit-logging`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/sink/audit-sink.ts` before modifying bridge behaviour
- [ ] Read `src/plugin/plugin.ts` before changing plugin wiring
- [ ] Preserve never-throw-into-request semantics
- [ ] Ensure redaction runs before persistence

View File

@@ -0,0 +1,149 @@
---
name: sp-api-plugin-auth
description: Better-Auth-first authentication plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, mounts 9 typed auth routes plus a Better-Auth wildcard catch-all under /auth, pairs with a matching SessionResolver built from the same Better-Auth instance (the global ctx.principal seam), applies the canonical 17-table SurrealDB schema in start(ctx), and exports requireAuth / requireRole / requireEmailVerified Fetch-native middleware for sibling plugins. Server-tier.
---
# @suaveplan/api-plugin-auth
## Purpose
Better-Auth-first authentication plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it mounts 9 typed auth routes plus a Better-Auth wildcard catch-all under a configurable prefix (default `/auth`), pairs with a matching `SessionResolver` built from the **same** Better-Auth instance (the global `ctx.principal` seam), applies the canonical 17-table SurrealDB schema in `start(ctx)` via `@suaveplan/better-auth-surrealdb`, and exports `requireAuth` / `requireRole` / `requireEmailVerified` Fetch-native middleware for sibling plugins.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createAuth` | `src/plugin/plugin.ts` | Returns `{ plugin, sessionResolver }` |
| `createAuthPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createAuthSessionResolver` | `src/plugin/plugin.ts` | Builds SessionResolver from Better-Auth instance |
| `buildAuthRoutes` | `src/routes/build-routes.ts` | 9 typed auth RouteDescriptors + wildcard catch-all |
| `requireAuth` | `src/middleware/require-auth.ts` | Rejects anonymous requests with 401 |
| `requireRole` | `src/middleware/require-role.ts` | Authorises by role scope |
| `requireEmailVerified` | `src/middleware/require-email-verified.ts` | Rejects unverified emails |
| `mapBetterAuthSessionToPrincipal` | `src/principal/principal.ts` | Maps Better-Auth session → Principal |
| `AUTH_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | 8-code `AUTH_PLUGIN.*` registry |
| `AUTH_EVENTS` | `src/events/events.ts` | App-level `auth.user.created` event contract |
## File Structure
```
src/
index.ts # Barrel: plugin factories, middleware, principal, events, codes, schemas, otel
plugin/
plugin.ts # createAuth, createAuthPlugin, createAuthSessionResolver
routes/
build-routes.ts # buildAuthRoutes, buildTypedAuthRoutes
middleware/
require-auth.ts # requireAuth
require-role.ts # requireRole
require-email-verified.ts # requireEmailVerified
principal/
principal.ts # mapBetterAuthSessionToPrincipal, hasScope, isEmailVerified
principal.types.ts # Principal, BetterAuthSessionView, etc.
events/
events.ts # AUTH_EVENTS, AuthUserCreatedSchema
emit.ts # createAuthUserCreatedPublisher, composeDatabaseHooks
error-codes/
error-codes.ts # AUTH_PLUGIN_CODES + AuthPluginError
otel/
instruments.ts # auth.* OTel bundle
schemas/
principal.schemas.ts # AccountSchema, PrincipalSchema, SessionSchema
routes/ # Per-route Zod schemas
options/
options.ts # AuthPluginOptionsSchema
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-auth` | `createAuth`, `createAuthPlugin`, `createAuthSessionResolver`, `requireAuth`, `requireRole`, `requireEmailVerified`, `mapBetterAuthSessionToPrincipal`, `AUTH_EVENTS`, codes, schemas, types |
| `@suaveplan/api-plugin-auth/plugin` | `createAuth`, `createAuthPlugin`, `createAuthSessionResolver`, `AuthWiring` |
| `@suaveplan/api-plugin-auth/middleware` | `requireAuth`, `requireRole`, `requireEmailVerified` |
| `@suaveplan/api-plugin-auth/principal` | `mapBetterAuthSessionToPrincipal`, `hasScope`, `isEmailVerified`, types |
| `@suaveplan/api-plugin-auth/events` | `AUTH_EVENTS`, `AuthUserCreatedSchema`, `createAuthUserCreatedPublisher`, `composeDatabaseHooks` |
| `@suaveplan/api-plugin-auth/error-codes` | `AUTH_PLUGIN_CODES`, `AuthPluginError` |
| `@suaveplan/api-plugin-auth/otel` | OTel instruments |
| `@suaveplan/api-plugin-auth/schemas` | Principal + route Zod schemas |
| `@suaveplan/api-plugin-auth/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createAuth } from "@suaveplan/api-plugin-auth";
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
const { plugin, sessionResolver } = createAuth({
betterAuth: {
database: betterAuthSurrealAdapter({ client: surreal }),
secret: process.env.AUTH_SECRET,
baseURL: "http://localhost:3000",
basePath: "/auth",
emailAndPassword: { enabled: true },
},
surrealClient: surreal,
});
const runtime = createApi({ plugins: [plugin], sessionResolver });
await runtime.start("auth");
```
### Enable user-created provisioning events
```ts
import { MemoryEvents } from "@suaveplan/api-capability-events-memory";
const events = new MemoryEvents();
const { plugin, sessionResolver } = createAuth({
betterAuth: { /* … */ },
surrealClient: surreal,
events, // ← opt in to publishing auth.user.created
});
```
### Guard a route in a sibling plugin
```ts
import { requireAuth, requireRole } from "@suaveplan/api-plugin-auth";
router.get("/admin", adminHandler, {
middleware: [requireAuth(), requireRole("admin")],
});
```
## Routes
9 typed routes under `/auth` plus a GET/POST wildcard catch-all:
| Method | Path | Operation |
|---|---|---|
| POST | `/auth/sign-up` | Create account |
| POST | `/auth/sign-in` | Authenticate |
| POST | `/auth/sign-out` | End session |
| GET | `/auth/session` | Get current session |
| POST | `/auth/forgot-password` | Request reset |
| POST | `/auth/reset-password` | Reset password |
| POST | `/auth/verify-email` | Verify email |
| GET | `/auth/oauth/:provider/start` | OAuth start |
| GET | `/auth/oauth/:provider/callback` | OAuth callback |
## Constraints
- **Server-tier** — runs in Bun and Node.
- **Better-Auth-first** — built on `better-auth` with SurrealDB adapter.
- **17-table schema** — applied in `start(ctx)` via `@suaveplan/better-auth-surrealdb`.
- **SessionResolver** — built from the same Better-Auth instance to ensure `ctx.principal` consistency.
- **Provisioning events** — best-effort `auth.user.created` event; events capability is optional.
- **5 OTel instruments** — `auth.sessions_resolved_total`, `session_resolve_duration_ms`, `role_check_total`, `signups_total`, `signins_total`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-auth-better-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/better-auth-surrealdb`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `better-auth`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/plugin/plugin.ts` before modifying auth wiring
- [ ] Read `src/routes/build-routes.ts` before adding/removing routes
- [ ] Preserve SessionResolver ↔ Better-Auth instance consistency
- [ ] Ensure requireAuth/requireRole guards work for sibling plugins

View File

@@ -0,0 +1,112 @@
---
name: sp-api-plugin-banking
description: Bank + bank-account domain plugin for the Fetch-native @suaveplan/api platform, with default-account auto-provisioning on signup. Authored with @suaveplan/api-plugin-kit definePlugin, contributes typed RouteDescriptors across /banks and /accounts, persisting to its own SurrealDB tables via an injected SurrealAdapter. Balances are integer minor units in ISO-4217 currency, arithmetic via @suaveplan/money. Subscribes best-effort to auth.user.created and runs a reconciliation sweep on start. Server-tier.
---
# @suaveplan/api-plugin-banking
## Purpose
Bank + bank-account domain plugin for the Fetch-native `@suaveplan/api` platform, with default-account auto-provisioning on signup. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes typed `RouteDescriptor`s across `/banks` and `/accounts`, persisting to its own SurrealDB tables (`bank`, `bank_account`, `user_account` relation, `bank_transaction`, `bank_consent`, `bank_payment`) via an injected `SurrealAdapter`.
Balances are integer minor units in an ISO-4217 currency, arithmetic via `@suaveplan/money`. Subscribes best-effort to `auth.user.created` (idempotent, credential-provider-scoped) and runs a reconciliation sweep on `start`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createBankingPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createBanking` | `src/plugin/plugin.ts` | Returns `{ plugin, service, repositories }` |
| `BankingService` | `src/service/service.ts` | Domain service layer |
| `SurrealBankRepository` | `src/repositories/bank/bank.ts` | Bank persistence |
| `SurrealAccountRepository` | `src/repositories/account/account.ts` | Account persistence |
| `reconcileDefaultAccounts` | `src/reconciliation/reconciliation.ts` | Reconciliation sweep |
| `createProvisioningHandler` | `src/subscriber/subscriber.ts` | Auth event subscriber |
| `BANKING_API_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, service, repositories, reconciliation, subscriber, codes, types
plugin/
plugin.ts # createBankingPlugin, createBanking
service/
service.ts # BankingService + createBankingService
repositories/
bank/ # SurrealBankRepository
account/ # SurrealAccountRepository
routes/
all-routes/ # buildAllBankingRoutes
banks/ # Bank CRUD routes
accounts/ # Account CRUD + transaction routes
money/
money.ts # Minor-unit arithmetic helpers
reconciliation/
reconciliation.ts # reconcileDefaultAccounts
subscriber/
subscriber.ts # createProvisioningHandler, subscribeProvisioning
schema/
schema.ts # SurrealQL DDL + applyBankingSchema
error-codes/
error-codes.ts # BANKING_API_CODES + BankingApiError
otel/
instruments.ts # banking_api.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-banking` | `createBankingPlugin`, `createBanking`, `BankingService`, repositories, reconciliation, subscriber, codes, types |
| `@suaveplan/api-plugin-banking/plugin` | `createBankingPlugin`, `createBanking`, `BankingWiring` |
| `@suaveplan/api-plugin-banking/service` | `BankingService`, `createBankingService` |
| `@suaveplan/api-plugin-banking/routes` | `buildAllBankingRoutes`, `RouteDeps` |
| `@suaveplan/api-plugin-banking/repositories/bank` | `SurrealBankRepository`, types |
| `@suaveplan/api-plugin-banking/repositories/account` | `SurrealAccountRepository`, `AccountFunds`, types |
| `@suaveplan/api-plugin-banking/error-codes` | `BANKING_API_CODES`, `BankingApiError` |
| `@suaveplan/api-plugin-banking/otel` | OTel instruments |
| `@suaveplan/api-plugin-banking/schemas` | `BankingPluginOptionsSchema` |
| `@suaveplan/api-plugin-banking/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createAuth } from "@suaveplan/api-plugin-auth";
import { createBanking } from "@suaveplan/api-plugin-banking";
import { SurrealAdapter } from "@suaveplan/surrealdb";
const db = new SurrealAdapter({ url: "ws://localhost:8000" });
await db.connect();
const { plugin: authPlugin, sessionResolver } = createAuth({ betterAuth });
const { plugin: bankingPlugin } = createBanking({ db });
const runtime = createApi({
plugins: [authPlugin, bankingPlugin],
sessionResolver,
tenantResolver,
});
await runtime.start("auth");
await runtime.start("banking");
```
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`.
- **Owns its persistence** — six SurrealDB tables: `bank`, `bank_account`, `user_account`, `bank_transaction`, `bank_consent`, `bank_payment`.
- **Integer minor units** — balances in ISO-4217 minor units; arithmetic via `@suaveplan/money`.
- **Auto-provisioning** — subscribes to `auth.user.created` for default account creation.
- **Idempotent provisioning** — credential-provider-scoped; safe to replay.
- **Reconciliation sweep** — `reconcileDefaultAccounts` runs on `start(ctx)`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/idempotency`, `@suaveplan/money`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/plugin/plugin.ts` before modifying plugin wiring
- [ ] Read `src/service/service.ts` before changing domain logic
- [ ] Preserve integer minor-unit arithmetic — never use float for money
- [ ] Ensure auto-provisioning remains idempotent

View File

@@ -0,0 +1,98 @@
---
name: sp-api-plugin-campaigns
description: Campaigns + suppression + delivery-webhooks plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, contributes 18 typed RouteDescriptors across /campaigns, /suppression, /webhooks, persisting to its own SurrealDB tables via CampaignStore, CampaignSuppressionStore, and SurrealDeliveryEventStore. Enforces the @suaveplan/campaigns state machine, suppresses on negative delivery events, applies schema in start(ctx). Server-tier.
---
# @suaveplan/api-plugin-campaigns
## Purpose
Campaigns + suppression + delivery-webhooks plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes 18 typed `RouteDescriptor`s across `/campaigns`, `/suppression`, and `/webhooks`, persisting to its own SurrealDB tables via the `@suaveplan/campaigns` `CampaignStore`, a local `CampaignSuppressionStore`, and the `@suaveplan/delivery-tracking` `SurrealDeliveryEventStore`.
Handlers read `ctx.principal` and `ctx.tenantId`; the plugin applies its schema in `start(ctx)`. It enforces the `@suaveplan/campaigns` state machine and suppresses on negative delivery events.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createCampaigns` | `src/plugin/plugin.ts` | Returns `{ plugin, stores }` |
| `createCampaignsPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `SurrealCampaignRepository` | `src/repositories/campaign/campaign.ts` | Campaign persistence |
| `SurrealCampaignSuppressionRepository` | `src/repositories/suppression/suppression.ts` | Suppression persistence |
| `CampaignStateMachine` | from `@suaveplan/campaigns` | Transition guard (409 on forbidden) |
| `CAMPAIGNS_API_CODES` | `src/error-codes/error-codes.ts` | 11-code `CAMPAIGNS_API.*` registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, repositories, routes, codes, types
plugin/
plugin.ts # createCampaigns, createCampaignsPlugin
repositories/
campaign/ # SurrealCampaignRepository
suppression/ # SurrealCampaignSuppressionRepository + InMemoryCampaignSuppressionStore
routes/
all-routes/ # buildAllCampaignRoutes
campaigns/ # CRUD + action routes (send/pause/cancel/duplicate/test/stats/events)
suppression/ # list/check/create/delete
webhooks/ # delivery ingest, one-click unsubscribe
schema/
schema.ts # SurrealQL DDL + applyCampaignSchema
error-codes/
error-codes.ts # CAMPAIGNS_API_CODES + CampaignsApiError
otel/
instruments.ts # campaigns_api.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-campaigns` | `createCampaigns`, `createCampaignsPlugin`, repositories, routes, codes, types |
| `@suaveplan/api-plugin-campaigns/plugin` | `createCampaigns`, `createCampaignsPlugin` |
| `@suaveplan/api-plugin-campaigns/routes` | `buildAllCampaignRoutes` |
| `@suaveplan/api-plugin-campaigns/repositories/campaign` | `SurrealCampaignRepository` |
| `@suaveplan/api-plugin-campaigns/repositories/suppression` | `SurrealCampaignSuppressionRepository` |
| `@suaveplan/api-plugin-campaigns/error-codes` | `CAMPAIGNS_API_CODES`, `CampaignsApiError` |
| `@suaveplan/api-plugin-campaigns/otel` | OTel instruments |
| `@suaveplan/api-plugin-campaigns/schemas` | Options schema |
| `@suaveplan/api-plugin-campaigns/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createCampaigns } from "@suaveplan/api-plugin-campaigns";
import { SurrealAdapter } from "@suaveplan/surrealdb";
const db = new SurrealAdapter({ url: "ws://localhost:8000" });
await db.connect();
const { plugin } = createCampaigns({ db });
const runtime = createApi({
plugins: [authPlugin, plugin],
sessionResolver,
tenantResolver,
});
await runtime.start("auth");
await runtime.start("campaigns");
```
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`.
- **Owns its persistence** — SurrealDB tables for campaigns, suppression, delivery events.
- **State machine** — `@suaveplan/campaigns` transition guard; 409 on forbidden transitions.
- **Suppression on negative events** — webhook delivery ingest triggers suppression.
- **Unsubscribe tokens** — base64-JSON decode for one-click unsubscribe.
- **2 OTel instruments** — `campaigns_api.requests_total`, `request_duration_ms`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/campaigns`, `@suaveplan/delivery-tracking`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/plugin/plugin.ts` before modifying plugin wiring
- [ ] Preserve state-machine enforcement — never skip transition validation
- [ ] Ensure suppression webhook logic stays intact

View File

@@ -0,0 +1,123 @@
---
name: sp-api-plugin-cms
description: Tenant-scoped headless CMS plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, contributes 11 typed routes for content models, entries, draft→review→published→archived status lifecycle, and version history under /cms. Persisted through a CmsStore port (SurrealDB-backed or in-memory). Six mutating routes auth-gated via @suaveplan/api-plugin-auth. Server-tier.
---
# @suaveplan/api-plugin-cms
## Purpose
Tenant-scoped headless CMS plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes 11 typed HTTP routes for content models, entries, a `draft → review → published → archived` status lifecycle, and a per-entry version trail — baked under a configurable prefix (default `/cms`).
Content is persisted through a `CmsStore` port backed either by SurrealDB (`SurrealCmsStore`) or an in-memory reference (`InMemoryCmsStore`); the six mutating (authoring) routes are auth-gated via `@suaveplan/api-plugin-auth` `requireAuth` / `requireRole` reading `ctx.principal`, and every row is scoped to `ctx.tenantId`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createCms` | `src/plugin/plugin.ts` | Returns `{ plugin, engine }` |
| `createCmsPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `CmsEngine` | `src/engine/cms-engine.ts` | Content-management core over store port |
| `SurrealCmsStore` | `src/store/surreal-store.ts` | Durable SurrealDB-backed store |
| `InMemoryCmsStore` | `src/store/in-memory-store.ts` | In-process reference store |
| `buildCmsRoutes` | `src/routes/build-routes.ts` | 11 typed RouteDescriptors |
| `CMS_PLUGIN_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, engine, stores, routes, codes, types
plugin/
plugin.ts # createCms, createCmsPlugin
engine/
cms-engine.ts # CmsEngine core
transitions.ts # Per-transition policy + entry shaping
store/
surreal-store.ts # SurrealCmsStore
in-memory-store.ts # InMemoryCmsStore
schema.ts # SurrealQL DDL + applyCmsSchema
content/
content-model.ts # Slug + validation + status-transition table
routes/
build-routes.ts # 11 typed RouteDescriptors
route-helpers.ts # Tenant + body + query helpers
problem-json.ts # RFC 7807 renderer
error-codes/
error-codes.ts # CMS_PLUGIN_CODES + CmsPluginError
otel/
instruments.ts # 7 CMS OTel counters
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-cms` | `createCms`, `createCmsPlugin`, `CmsEngine`, stores, routes, codes, types |
| `@suaveplan/api-plugin-cms/plugin` | `createCms`, `createCmsPlugin` |
| `@suaveplan/api-plugin-cms/engine` | `CmsEngine`, transition helpers |
| `@suaveplan/api-plugin-cms/store` | `SurrealCmsStore`, `InMemoryCmsStore`, `applyCmsSchema` |
| `@suaveplan/api-plugin-cms/content` | Content model utilities |
| `@suaveplan/api-plugin-cms/routes` | `buildCmsRoutes`, route helpers |
| `@suaveplan/api-plugin-cms/error-codes` | `CMS_PLUGIN_CODES`, `CmsPluginError` |
| `@suaveplan/api-plugin-cms/otel` | OTel instruments |
| `@suaveplan/api-plugin-cms/options` | Options types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createAuth } from "@suaveplan/api-plugin-auth";
import { createCms } from "@suaveplan/api-plugin-cms";
const { plugin: authPlugin, sessionResolver } = createAuth({ auth, surrealClient });
const { plugin: cmsPlugin, engine } = createCms({
surrealClient: { query: (sql, params) => surreal.query(sql, params) },
requireRoles: ["editor"],
});
const runtime = createApi({
plugins: [authPlugin, cmsPlugin],
sessionResolver,
tenantResolver,
});
await runtime.start("auth");
await runtime.start("cms");
engine.events.on("content.published", (entry) => invalidateCdn(entry.slug));
```
## Routes
All paths under `routePrefix` (default `/cms`). Reads are public; mutating routes require auth.
| Method | Path | Auth | Operation |
|---|---|---|---|
| GET | `/cms/models` | public | list models |
| POST | `/cms/models` | required | create model |
| GET | `/cms/models/:id` | public | get model |
| DELETE | `/cms/models/:id` | required | delete model |
| GET | `/cms/content` | public | query entries |
| POST | `/cms/content` | required | create entry |
| GET | `/cms/content/:id` | public | get entry |
| PATCH | `/cms/content/:id` | required | update entry |
| DELETE | `/cms/content/:id` | required | delete entry |
| POST | `/cms/content/:id/transition` | required | transition status |
| GET | `/cms/content/:id/versions` | public | list version trail |
## Constraints
- **Server-tier** — Bun + Node.
- **CmsStore port** — swappable SurrealDB or in-memory backend.
- **Status lifecycle** — `draft → review → published → archived`; invalid transitions rejected.
- **Tenant-scoped** — every store + engine method scoped to `tenantId`.
- **7 OTel counters** — on every content mutation.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/engine/cms-engine.ts` before modifying content logic
- [ ] Preserve status FSM — never allow invalid transitions
- [ ] Ensure tenant scoping on every store operation

View File

@@ -0,0 +1,120 @@
---
name: sp-api-plugin-customs
description: Cross-border customs / compliance plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, contributes 19 typed RouteDescriptors across /declarations, /border-holds, /hs-codes, /duty-payments, and /intrastat, persisting to its own SurrealDB tables via five repositories. Declarations link to logistics shipments, duty amounts use @suaveplan/money exact arithmetic, border holds gate clearance, HS-code fuzzy lookup, Intrastat period aggregation. Server-tier.
---
# @suaveplan/api-plugin-customs
## Purpose
Cross-border customs / compliance plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it contributes 19 typed `RouteDescriptor`s across `/declarations`, `/border-holds`, `/hs-codes`, `/duty-payments`, and `/intrastat`, persisting to its own SurrealDB tables via five SurrealDB-backed repositories.
Declarations link to logistics shipments through the exported `@suaveplan/api-plugin-logistics` `ShipmentService`; duty amounts use `@suaveplan/money` for exact integer-minor-unit arithmetic. Handlers read `ctx.principal` and `ctx.tenantId`; the plugin applies its schema in `start(ctx)`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createCustomsPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createCustoms` | `src/plugin/plugin.ts` | Returns plugin + five live services |
| `DeclarationService` | `src/services/declaration/declaration-service.ts` | Declaration lifecycle |
| `BorderHoldService` | `src/services/border-hold/border-hold-service.ts` | Border hold placement/release |
| `DutyService` | `src/services/duty/duty-service.ts` | Duty + tax payments |
| `HsCodeService` | `src/services/hs-code/hs-code-service.ts` | HS-code classification |
| `IntrastatService` | `src/services/intrastat/intrastat-service.ts` | EU trade-statistics reporting |
| `CUSTOMS_API_CODES` | `src/error-codes/error-codes.ts` | 16-code `CUSTOMS_API.*` registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, services, repositories, routes, codes, types
plugin/
plugin.ts # createCustomsPlugin, createCustoms
services/
declaration/ # DeclarationService + state machine
border-hold/ # BorderHoldService
duty/ # DutyService
hs-code/ # HsCodeService + fuzzy scorer
intrastat/ # IntrastatService + aggregate helpers
repositories/
declaration/ # Surreal + in-memory DeclarationStore
border-hold/ # Surreal + in-memory BorderHoldStore
duty-payment/ # Surreal + in-memory DutyPaymentStore
hs-code/ # Surreal + in-memory HsCodeStore
intrastat/ # Surreal + in-memory IntrastatReportStore
shared/ # Helpers shared by all repositories
routes/
all-routes/ # buildAllCustomsRoutes
declarations/ # submit/get/list/review/clear/reject
border-holds/ # place/get/list/release
duty-payments/ # record/list
hs-codes/ # register/lookup
intrastat/ # generate/get/list/submit
schema/
schema.ts # SurrealQL DDL + applyCustomsSchema
error-codes/
error-codes.ts # CUSTOMS_API_CODES + CustomsApiError
otel/
instruments.ts # customs_api.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-customs` | `createCustomsPlugin`, `createCustoms`, all services, repositories, routes, codes, types |
| `@suaveplan/api-plugin-customs/plugin` | `createCustomsPlugin`, `createCustoms` |
| `@suaveplan/api-plugin-customs/routes` | `buildAllCustomsRoutes` |
| `@suaveplan/api-plugin-customs/services` | All five services |
| `@suaveplan/api-plugin-customs/repositories` | All five repository classes |
| `@suaveplan/api-plugin-customs/error-codes` | `CUSTOMS_API_CODES`, `CustomsApiError` |
| `@suaveplan/api-plugin-customs/otel` | OTel instruments |
| `@suaveplan/api-plugin-customs/schema` | `applyCustomsSchema` |
| `@suaveplan/api-plugin-customs/types` | Public types |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createCustomsPlugin } from "@suaveplan/api-plugin-customs";
import { createLogistics } from "@suaveplan/api-plugin-logistics";
import { SurrealAdapter } from "@suaveplan/surrealdb";
const db = new SurrealAdapter({ url: "ws://localhost:8000" });
await db.connect();
const logistics = createLogistics({ db, routePrefix: "/logistics" });
const customs = createCustomsPlugin({
db,
shipmentService: logistics.shipmentService,
routePrefix: "/customs",
});
const runtime = createApi({
plugins: [authPlugin, logistics.plugin, customs],
sessionResolver,
tenantResolver,
});
await runtime.start("customs");
```
## Constraints
- **Server-tier** — consumes `@suaveplan/surrealdb`.
- **Five entities** — declarations, border holds, HS codes, duty payments, Intrastat reports.
- **Declaration state machine** — `submitted → under_review → cleared | rejected`; illegal transitions → 409.
- **Border hold gate** — clearance blocked by unreleased hold → 409.
- **Exact money arithmetic** — `@suaveplan/money` for duty totals; currency mismatch → 409.
- **Intrastat period closure** — submitted period closed → 409.
- **In-memory variants** — each repository has a `Map`-backed test implementation.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-plugin-logistics`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/money`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/services/declaration/state-machine.ts` before modifying transitions
- [ ] Preserve border-hold gate on clearance
- [ ] Ensure exact money arithmetic — never use float for duty
- [ ] Maintain Intrastat period closure semantics

View File

@@ -0,0 +1,133 @@
---
name: sp-api-plugin-database
description: SurrealDB-backed database plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin, pools a real SurrealDatabase (AdminDatabaseLike), registers it as the platform db capability so sibling plugins consume it via requires:["db"], runs .surql migrations under an advisory lock in start(ctx), provides tenant-provisioning callables, exponential-backoff retry, RFC 7807 problem-json, typed admin routes, and full OTel surface. Server-tier.
---
# @suaveplan/api-plugin-database
## Purpose
SurrealDB-backed database plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin`, it pools a real `@suaveplan/api-db-surreal` `SurrealDatabase` (an `AdminDatabaseLike` over `@suaveplan/surrealdb`), registers it as the platform `db` capability so sibling plugins consume it via `requires: ["db"]`, runs `.surql` migrations under an advisory lock in `start(ctx)`, ships tenant-provisioning callables for three canonical multi-tenancy strategies, exponential-backoff retry, RFC 7807 problem-json rendering, typed admin routes (`GET /db/health`, `GET /db/pool`), and a complete OpenTelemetry surface.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createDatabasePlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createDatabase` | `src/plugin/plugin.ts` | Returns `{ plugin, dbCapability }` |
| `ConnectionPool` | `src/pool/pool.ts` | Bounded pool of SurrealAdapter instances |
| `PooledSurrealAdapter` | `src/pool/extended-adapter.ts` | SurrealAdapter subclass surfacing raw client |
| `LazyDatabase` | `src/database/lazy-database.ts` | Deferred AdminDatabaseLike |
| `createDbCapabilityRegistration` | `src/capability/capability.ts` | Turns AdminDatabaseLike into platform `db` capability |
| `runMigrations` | `src/migrations/runner.ts` | Advisory-locked .surql migration runner |
| `provisionTenant` / `decommissionTenant` | `src/tenancy/tenancy.ts` | Tenant lifecycle callables |
| `withRetry` | `src/retry/retry.ts` | Exponential-backoff helper |
| `DATABASE_CODES` | `src/error-codes/error-codes.ts` | 8-code `DATABASE.*` registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, pool, database, migrations, tenancy, retry, codes
plugin/
plugin.ts # createDatabasePlugin, createDatabase
pool/
pool.ts # ConnectionPool
extended-adapter.ts # PooledSurrealAdapter
database/
database.ts # connectPooledDatabase
lazy-database.ts # LazyDatabase
capability/
capability.ts # createDbCapabilityRegistration
migrations/
runner.ts # runMigrations
lock.ts # Advisory lock
tenancy/
tenancy.ts # provisionTenant, decommissionTenant
retry/
retry.ts # withRetry
routes/
admin-routes.ts # GET /db/health, GET /db/pool
problem-json.ts # RFC 7807 renderer
config/
config-schema.md # Zod schemas for options
error-codes/
error-codes.ts # DATABASE_CODES + DatabaseError
otel/
instruments.ts # db.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-database` | `createDatabasePlugin`, `createDatabase`, `dbCapability`, pool, tenancy, retry, codes |
| `@suaveplan/api-plugin-database/plugin` | `createDatabasePlugin`, `createDatabase` |
| `@suaveplan/api-plugin-database/pool` | `ConnectionPool`, `PooledSurrealAdapter` |
| `@suaveplan/api-plugin-database/database` | `LazyDatabase`, `connectPooledDatabase` |
| `@suaveplan/api-plugin-database/capability` | `createDbCapabilityRegistration` |
| `@suaveplan/api-plugin-database/migrations` | `runMigrations`, lock helpers |
| `@suaveplan/api-plugin-database/tenancy` | `provisionTenant`, `decommissionTenant` |
| `@suaveplan/api-plugin-database/retry` | `withRetry` |
| `@suaveplan/api-plugin-database/routes` | Admin RouteDescriptors |
| `@suaveplan/api-plugin-database/config` | Zod schemas |
| `@suaveplan/api-plugin-database/error-codes` | `DATABASE_CODES`, `DatabaseError` |
| `@suaveplan/api-plugin-database/otel` | OTel instruments |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { createDatabase } from "@suaveplan/api-plugin-database";
const { plugin, dbCapability } = createDatabase({
url: "ws://localhost:8000/rpc",
namespace: "app",
database: "main",
username: process.env.SURREAL_USER ?? "root",
password: process.env.SURREAL_PASS ?? "root",
pool: { min: 2, max: 20, idleMs: 30_000, acquireTimeoutMs: 5_000 },
migrations: { autoRun: true, directory: "./surql" },
});
const runtime = createApi({ plugins: [plugin], capabilities: [dbCapability] });
await runtime.start("database");
```
### Consume db capability in a sibling plugin
```ts
import { definePlugin } from "@suaveplan/api-plugin-kit/define-plugin";
const reports = definePlugin({
id: "reports",
version: "0.1.0",
requires: ["db"] as const,
routes(router) {
router.get("/reports/count", async (ctx) => {
const rows = await ctx.capabilities.db.query<unknown[]>("SELECT * FROM report");
return Response.json({ count: rows.length });
});
},
});
```
## Constraints
- **Server-tier** — Bun + Node.
- **Bounded pool** — eager `min`, lazy growth to `max`, queue + `acquireTimeoutMs`.
- **Advisory-locked migrations** — soft-skip vs strict `LOCK_CONTENDED`.
- **Three tenancy strategies** — `shared`, `namespace-per-tenant`, `database-per-tenant`.
- **Exponential backoff** — jitter, `isRetryable`, abort signal support.
- **4 OTel instruments** — `db.pool.checkouts_total`, `checkins_total`, `saturation`, `query.duration_ms`.
- **8 error codes** — `CONNECTION_FAILED`, `POOL_EXHAUSTED`, `POOL_CLOSED`, `QUERY_FAILED`, `MIGRATION_FAILED`, `LOCK_CONTENDED`, `TENANT_INVALID`, `INVALID_CONFIG`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-db-surreal`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/surrealdb`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/pool/pool.ts` before modifying pool behaviour
- [ ] Read `src/migrations/runner.ts` before changing migration logic
- [ ] Preserve advisory-lock semantics — never run migrations without it
- [ ] Ensure pool exhaustion returns 503, not 500

View File

@@ -0,0 +1,121 @@
---
name: sp-api-plugin-event-bus
description: Event-bus plugin for the Fetch-native @suaveplan/api platform. Authored with @suaveplan/api-plugin-kit definePlugin (requires events), wraps @suaveplan/events in a tenant-scoped EventBusService, bridges ctx.capabilities.events both ways, exposes per-request tenant-scoped publish/subscribe handle, and optionally mounts 3 auth-gated typed routes. Per-tenant topic scoping isolates publishes by prefixing topics with tenant:<id>:. Five OTel instruments. Server-tier.
---
# @suaveplan/api-plugin-event-bus
## Purpose
Event-bus plugin for the Fetch-native `@suaveplan/api` platform. Authored with `@suaveplan/api-plugin-kit` `definePlugin` (`requires: ["events"]`), it wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listeners) in a tenant-scoped `EventBusService`, bridges the platform `events` capability onto that engine, exposes a per-request tenant-scoped publish/subscribe handle, and optionally mounts three auth-gated typed `RouteDescriptor` routes (publish / list / delete).
Per-tenant topic scoping isolates publishes by prefixing topics with `tenant:<id>:`.
## Architecture
| Component | Source | Role |
|---|---|---|
| `createEventBusPlugin` | `src/plugin/plugin.ts` | Plugin factory |
| `createEventBus` | `src/plugin/plugin.ts` | Returns `{ plugin, service }` |
| `EventBusService` | `src/event-bus-service/event-bus-service.ts` | Rich engine wrapping `@suaveplan/events` |
| `scopedHandleFor` | `src/scoped-handle/scoped-handle.ts` | Per-request tenant-scoped publish/subscribe handle |
| `capabilityBridge` | `src/capability-bridge/capability-bridge.ts` | Bridges platform `events` capability ↔ EventBusService |
| `buildEventBusRoutes` | `src/routes/build-routes.ts` | Three typed RouteDescriptors |
| `EVENT_BUS_CODES` | `src/error-codes/error-codes.ts` | Error-code registry |
## File Structure
```
src/
index.ts # Barrel: plugin factories, service, routes, scoped handle, codes, otel
plugin/
plugin.ts # createEventBusPlugin, createEventBus
event-bus-service/
event-bus-service.ts # EventBusService core
helpers.ts # Pure helpers
scoped-handle/
scoped-handle.ts # Tenant-scoped handle builder
capability-bridge/
capability-bridge.ts # Platform capability ↔ service bridge
routes/
build-routes.ts # POST /publish, GET /subscriptions, DELETE /subscriptions/:id
response.ts # Shared Response builder
backpressure/
backpressure.ts # BoundedQueue adapter
idempotency-cache/
idempotency-cache.ts # Default IdempotencyCache
topic/
topic.ts # Topic validation + tenant scoping
options/
options.ts # Zod options boundary
error-codes/
error-codes.ts # EVENT_BUS_CODES + EventBusError
otel/
instruments.ts # event_bus.* OTel bundle
```
## Entry Points
| Import | Exports |
|---|---|
| `@suaveplan/api-plugin-event-bus` | `createEventBusPlugin`, `createEventBus`, `EventBusService`, `scopedHandleFor`, route builders, codes, otel |
| `@suaveplan/api-plugin-event-bus/plugin` | `createEventBusPlugin`, `createEventBus` |
| `@suaveplan/api-plugin-event-bus/event-bus-service` | `EventBusService`, helpers |
| `@suaveplan/api-plugin-event-bus/scoped-handle` | `scopedHandleFor` |
| `@suaveplan/api-plugin-event-bus/capability-bridge` | Capability bridge |
| `@suaveplan/api-plugin-event-bus/routes` | `buildEventBusRoutes` |
| `@suaveplan/api-plugin-event-bus/topic` | Topic utilities |
| `@suaveplan/api-plugin-event-bus/error-codes` | `EVENT_BUS_CODES`, `EventBusError` |
| `@suaveplan/api-plugin-event-bus/otel` | OTel instruments |
## Patterns
### Basic usage
```ts
import { createApi } from "@suaveplan/api";
import { MemoryEvents } from "@suaveplan/api-capability-events-memory";
import { createEventBusPlugin } from "@suaveplan/api-plugin-event-bus";
const runtime = createApi({
plugins: [authPlugin, createEventBusPlugin()],
capabilities: [{ id: "events", factory: () => new MemoryEvents() }],
sessionResolver,
tenantResolver,
});
await runtime.start("event-bus");
```
### In-handler publish (sibling plugins)
```ts
import { scopedHandleFor } from "@suaveplan/api-plugin-event-bus/scoped-handle";
const bus = scopedHandleFor(service, ctx, true);
const result = await bus.publish("order.created", { id: "o1" });
```
## Routes
When `mountRoutes` is on (default), three typed routes under `routePrefix` (default `/event-bus`):
| Method | Path | Operation |
|---|---|---|
| POST | `/event-bus/publish` | Publish event |
| GET | `/event-bus/subscriptions` | List active subscriptions |
| DELETE | `/event-bus/subscriptions/:id` | Cancel subscription (404 if foreign) |
## Constraints
- **Requires `events` capability** — validated at construction; `HOST.CAPABILITY_MISSING` if absent.
- **Per-tenant isolation** — topics prefixed with `tenant:<id>:` when `tenantIsolation: true` (default).
- **Backpressure** — `BoundedQueue` with `drop-oldest`, `drop-newest`, `block`, `throw` policies.
- **Idempotent listeners** — duplicate publish suppression via `IdempotencyCache`.
- **5 OTel instruments** — `event_bus.published_total`, `dropped_total`, `duplicates_total`, `subscriptions_active`, `publish_duration_ms`.
- **Dependencies**: `@suaveplan/api`, `@suaveplan/api-capability-events-memory`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-kit`, `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`.
## Checklist
- [ ] Read `src/event-bus-service/event-bus-service.ts` before modifying dispatch logic
- [ ] Preserve tenant topic prefixing — never leak cross-tenant events
- [ ] Ensure backpressure policies map correctly to queue behaviour

View File

@@ -0,0 +1,213 @@
# sp-api-plugin-events
## One-liner
Event-management plugin for the Fetch-native `@suaveplan/api` platform — 11 typed routes (list/get/create/update/delete/rsvp/cancel-rsvp/my-rsvp/attendees/invite/ics-export) gated by `requireAuth()`, backed by SurrealDB `EventRepository` + `RsvpRepository`, with a hand-rolled RFC 5545 ICS export, 11 `EVENTS_PLUGIN.*` error codes, and 7 OTel instruments.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createEventsPlugin` | `@suaveplan/api-plugin-kit` `definePlugin` factory; `dependsOn: ["auth"]` |
| **Routes** | `buildEventRoutes`, `buildRsvpRoutes`, `buildIcsRoute` | 11 `RouteDescriptor[]` (5 CRUD + 5 RSVP/invite + 1 ICS), each gated via `requireAuth()` |
| **Repositories** | `EventRepository`, `RsvpRepository` | Raw SurrealQL over owned `SurrealAdapter` — plugin owns persistence (recipe §4) |
| **ICS builder** | `buildVCalendar`, `escapeText`, `foldLine` | Hand-rolled RFC 5545 `VCALENDAR` — no third-party `ical` dep |
| **Schema** | `EVENT_SCHEMA`, `applyEventSchema` | Bundled SurrealQL DDL applied in `start(ctx)` |
| **Error codes** | `EVENTS_PLUGIN_CODES`, `EventsPluginError` | 11-code namespace; `status` field for RFC 9457 mapping |
| **OTel** | `getEventsInstruments()` | 7 counters bound to global OTel meter |
---
## Public API surface
### Entry points
```ts
// Main factory — returns a Plugin
createEventsPlugin(options: EventsPluginOptions): Plugin
// Options interface
interface EventsPluginOptions {
surrealAdapter?: SurrealAdapter; // required — owns persistence
routePrefix?: string; // default "/events"
maxInvitesPerRequest?: number; // default 50
applySchemaOnInit?: boolean; // default true
enableNotifications?: boolean; // reserved, not yet bound
emitEvent?: EmitEvent; // forward domain events (default no-op)
}
```
### Repositories
```ts
// EventRepository — SurrealDB-backed event CRUD
new EventRepository({ db: surrealAdapter })
.findAll(query?: EventQuery) -> Promise<Event[]>
.findById(id: string) -> Promise<Event | null>
.create(userId: string, data: CreateEvent) -> Promise<Event>
.update(id: string, data: UpdateEvent) -> Promise<Event>
.delete(id: string) -> Promise<void>
.isCreator(eventId, userId) -> Promise<boolean>
.updateAttendeeCount(eventId, status, increment) -> Promise<void>
// RsvpRepository — RSVP / invitation persistence
new RsvpRepository({ db: surrealAdapter })
.upsert(eventId, userId, data) -> Promise<Rsvp>
.findByEventAndUser(eventId, userId) -> Promise<Rsvp | null>
.findByEvent(eventId) -> Promise<Rsvp[]>
.delete(eventId, userId) -> Promise<void>
.findAttendees(eventId) -> Promise<Attendee[]>
.invite(eventId, userIds, inviterId) -> Promise<InviteResponse>
```
### ICS export
```ts
import { buildVCalendar } from "@suaveplan/api-plugin-events";
const ics = buildVCalendar({
event: { title, start_time, end_time, timezone, location, description },
attendees: [{ email, status: "ACCEPTED" }],
});
// Returns a complete RFC 5545 VCALENDAR string
```
### Error codes
```ts
import { EVENTS_PLUGIN_CODES, EventsPluginError } from "@suaveplan/api-plugin-events";
// 11 codes: INVALID_CONFIG, EVENT_CREATE_FAILED, EVENT_UPDATE_FAILED,
// RSVP_CREATE_FAILED, RSVP_UPDATE_FAILED, RSVP_NOT_FOUND, EVENT_NOT_FOUND,
// UNAUTHENTICATED, FORBIDDEN, INVITE_LIMIT_EXCEEDED, ICS_INVALID_QUERY
throw new EventsPluginError(
EVENTS_PLUGIN_CODES.EVENT_NOT_FOUND,
"Event not found",
{ context: { statusCode: 404 } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createEventsPlugin`, repositories, routes, schemas, ICS builder |
| `./plugin` | `createEventsPlugin`, `EventsPluginOptions` |
| `./routes` | `buildEventRoutes`, `buildRsvpRoutes`, `buildIcsRoute`, helpers |
| `./schemas` | Event + RSVP Zod schemas |
| `./repositories` | `EventRepository`, `RsvpRepository`, contracts |
| `./ics` | `buildVCalendar`, `escapeText`, `foldLine` |
| `./schema` | `EVENT_SCHEMA`, `applyEventSchema` |
| `./error-codes` | `EVENTS_PLUGIN_CODES`, `EventsPluginError` |
| `./otel` | `getEventsInstruments`, `resetEventsInstruments` |
| `./options` | `EventsPluginOptionsSchema`, config types |
| `./types` | `EmitEvent`, `EventsPluginEvents`, `EventRoutesConfig` |
---
## Integration patterns
### With api-plugin-auth
The plugin declares `dependsOn: ["auth"]` so the runtime topo-sorts it after `@suaveplan/api-plugin-auth`. Each route sets `config.middleware: [requireAuth()]`; handlers read `ctx.principal` for the caller identity. The auth plugin's `SessionResolver` populates `ctx.principal`; the events plugin reads it.
### With SurrealDB
The plugin **owns** its SurrealDB adapter — it takes `surrealAdapter` as an explicit constructor argument and uses raw SurrealQL (positional `$p0,$p1,…` placeholders). This is recipe §4: the engine-neutral `db` capability cannot express raw SurrealQL, so the plugin owns its persistence.
### Domain events
The optional `emitEvent` callback forwards typed domain events:
```ts
createEventsPlugin({
surrealAdapter,
emitEvent: (event) => bus.emit(event.type, event.payload),
});
// Events emitted: "event:created", "event:updated", "event:cancelled"
```
---
## Code patterns
### Route descriptor with auth gating
From `src/plugin/plugin.ts`:
```ts
return definePlugin({
id: "events",
version: "0.1.0",
dependsOn: ["auth"],
routes(router) {
const guard = requireAuth();
for (const route of buildEventRoutes(routeConfig)) {
const config = { ...route.meta, middleware: [guard] };
if (route.method === "GET") {
router.get(route.path, route.handler, config);
} else if (route.method === "POST") {
router.post(route.path, route.handler, config);
} else if (route.method === "PUT") {
router.put(route.path, route.handler, config);
} else {
router.delete(route.path, route.handler, config);
}
}
},
async start() {
if (resolved.config.applySchemaOnInit) {
await applyEventSchema({ adapter: resolved.surrealAdapter });
}
},
});
```
### Repository with raw SurrealQL
From `src/repositories/event-repository/event-repository.ts`:
```ts
async findAll(query?: EventQuery): Promise<Event[]> {
const conditions: string[] = [];
const params: unknown[] = [];
if (query?.status !== undefined) {
params.push(query.status);
conditions.push(`status = $p${params.length - 1}`);
}
if (query?.upcoming) conditions.push("end_time > time::now()");
let sql = "SELECT * FROM event";
if (conditions.length > 0) sql += ` WHERE ${conditions.join(" AND ")}`;
params.push(limit);
sql += ` LIMIT $p${params.length - 1}`;
const rows = await this.db.query<Event>(sql, params);
return rows;
}
```
---
## Error-code registry
`EVENTS_PLUGIN_CODES` via `defineCodes("EVENTS_PLUGIN", [...])` — 11 codes. Every code is thrown from at least one production path. The namespace is `"EVENTS_PLUGIN"` (not `"EVENTS"`) to avoid collision with `@suaveplan/events`.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- 100% line/branch/function/statement coverage
- OTel instruments tested with `@opentelemetry/sdk-metrics` `MeterProvider`
- `resetEventsInstruments()` available for test isolation
---
## Related packages
- `@suaveplan/api-plugin-auth` — auth gating + `ctx.principal`
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/surrealdb` — `SurrealAdapter`
- `@suaveplan/events` — the typed event-bus library (NOT this plugin)

View File

@@ -0,0 +1,258 @@
# sp-api-plugin-export
## One-liner
Data-export plugin for the Fetch-native `@suaveplan/api` platform — CSV (RFC 4180), JSON, and XLSX (SpreadsheetML XML) exporters, a `DataProvider` registry, an `ExportJobStore` (in-memory + SurrealDB), and five typed routes (list, create, get, streaming download, delete) with optional auth gating, RFC 9457 problem-json, and a six-instrument OTel surface.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createExport`, `createExportPlugin` | `definePlugin` factory; returns `{ plugin, providers, jobs }` wiring |
| **Routes** | `buildExportRoutes` | 5 `RouteDescriptor[]` (list/create/get/download/delete) |
| **Service** | `runExport`, `getExporter`, `inferColumns` | Orchestrates provider fetch → exporter → result |
| **Exporters** | `CsvExporter`, `JsonExporter`, `XlsxExporter` | Pure, dependency-free format writers |
| **Store** | `ExportJobStore` contract | `MemoryExportJobStore` (default) + `SurrealExportStore` (durable) |
| **Error codes** | `EXPORT_CODES`, `ExportError` | 9-code namespace; `status` for RFC 9457 |
| **OTel** | `getExportInstruments()` | 6 instruments (5 counters + 1 histogram) |
---
## Public API surface
### Entry points
```ts
// Wiring factory — returns plugin + live handles
const { plugin, providers, jobs } = createExport({
basePath: "/v1/exports",
jobStore: new SurrealExportStore({ db: adapter }),
requireRoles: ["export:read"], // opt-in auth gating
});
// Plugin-only factory
const plugin = createExportPlugin({ basePath: "/exports" });
```
### DataProvider
```ts
import { type DataProvider, registerDataProvider } from "@suaveplan/api-plugin-export";
const userProvider: DataProvider = {
resource: "users",
fetch: async () => db.select("user").get(),
};
registerDataProvider(providers, userProvider);
// Or directly: providers.set(userProvider.resource, userProvider);
```
### Exporters
```ts
import { CsvExporter, JsonExporter, XlsxExporter } from "@suaveplan/api-plugin-export";
const csv = new CsvExporter().export(data, columns, { delimiter: ";" });
// Returns: { content, mimeType: "text/csv", filename, rowCount, byteSize }
```
### Job stores
```ts
import { MemoryExportJobStore } from "@suaveplan/api-plugin-export/store";
import { SurrealExportStore } from "@suaveplan/api-plugin-export/store";
// In-memory (default)
const store = new MemoryExportJobStore();
// SurrealDB-backed
const store = new SurrealExportStore({ db: adapter });
```
### Error codes
```ts
import { EXPORT_CODES, ExportError } from "@suaveplan/api-plugin-export";
// 9 codes: INVALID_CONFIG, PROVIDER_NOT_FOUND, ROW_LIMIT_EXCEEDED,
// FILE_SIZE_EXCEEDED, UNSUPPORTED_FORMAT, PROVIDER_FETCH_FAILED,
// JOB_NOT_FOUND, JOB_NOT_READY, JOB_PERSISTENCE_FAILED
throw new ExportError(
EXPORT_CODES.PROVIDER_NOT_FOUND,
"No provider registered for resource 'orders'",
{ context: { statusCode: 404 } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createExport`, `createExportPlugin`, exporters, types, stores |
| `./plugin` | `createExport`, `createExportPlugin`, `registerDataProvider` |
| `./routes` | `buildExportRoutes`, `buildTypedExportRoutes`, download handlers |
| `./services` | `runExport`, `getExporter`, `inferColumns`, `DEFAULT_EXPORT_CONFIG` |
| `./exporters` | `CsvExporter`, `JsonExporter`, `XlsxExporter` + helpers |
| `./store` | `MemoryExportJobStore`, `SurrealExportStore`, `ExportJobStore` contract |
| `./error-codes` | `EXPORT_CODES`, `ExportError` |
| `./otel` | `getExportInstruments`, `resetExportInstruments` |
| `./types` | `ExportFormat`, `ExportStatus`, `DataProvider`, `ExportJob`, schemas |
---
## Integration patterns
### Auth gating
Opt-in via `requireAuth` or `requireRoles`:
```ts
const { plugin } = createExport({
requireAuth: true, // gates with requireAuth()
requireRoles: ["admin"], // gates with requireRole("admin")
});
```
The plugin resolves the middleware chain: `requireRoles` takes precedence, then `requireAuth`, then public routes.
### Streaming download
The download route sets `meta.streaming: true` so the host bypasses response buffering:
```ts
const download: RouteDescriptor = {
method: "GET",
path: `${base}/:id/download`,
handler: async (ctx) => {
const outcome = await runDownloadExport(deps, id);
return new Response(outcome.body, {
status: 200,
headers: {
"content-type": outcome.contentType,
"content-disposition": buildContentDisposition(outcome.filename),
},
});
},
meta: { streaming: true, ... },
};
```
### Problem-json
Errors serialize to RFC 9457 / RFC 7807 `application/problem+json`:
```ts
import { respondProblemJson } from "@suaveplan/api-plugin-export";
return respondProblemJson({
type: "about:blank",
title: "Export job not ready",
status: 409,
detail: "Job is still processing",
});
```
---
## Code patterns
### Route builder with auth middleware
From `src/routes/build-routes/build-routes.ts`:
```ts
export function buildExportRoutes(
deps: ExportRoutesDeps,
routePrefix: string,
options: BuildRoutesOptions = {}
): readonly RouteDescriptor[] {
const mw = options.middleware ?? [];
const withMw = (descriptor: RouteDescriptor): RouteDescriptor =>
mw.length === 0 ? descriptor : { ...descriptor, middleware: mw };
const list: RouteDescriptor = {
method: "GET",
path: collectionPath,
handler: async (ctx) => { /* ... */ },
meta: {
operationId: "export.list",
summary: "List export jobs",
request: { query: ExportJobListQuerySchema },
responses: { "200": { body: ExportJobListResponseSchema } },
},
};
return [list, create, get, download, remove].map(withMw);
}
```
### CSV exporter (RFC 4180)
From `src/exporters/csv/csv.ts`:
```ts
export function escapeCsvField(value: string, delimiter: string): string {
const needsQuoting =
value.includes(delimiter) ||
value.includes('"') ||
value.includes("\n") ||
value.includes("\r");
if (needsQuoting) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
export function convertToCsv(
data: Record<string, unknown>[],
columns: ColumnDefinition[],
options?: ExportOptions
): string {
const delimiter = options?.delimiter ?? ",";
const lines: string[] = [];
// header row
const headerRow = columns
.map((col) => escapeCsvField(col.header, delimiter))
.join(delimiter);
lines.push(headerRow);
// data rows
for (const row of data) {
const values = columns.map((col) => {
const rawValue = row[col.key];
const transform = col.transform as ((v: unknown) => unknown) | undefined;
const value = transform ? transform(rawValue) : rawValue;
return escapeCsvField(formatCellValue(value), delimiter);
});
lines.push(values.join(delimiter));
}
return lines.join("\n");
}
```
---
## Error-code registry
`EXPORT_CODES` via `defineCodes("EXPORT", [...])` — 9 codes. Every code is thrown from at least one production path under `src/`.
---
## Testing notes
- Real SurrealDB testcontainer via `@suaveplan/testing-integration-surrealdb`
- 100% coverage target
- `resetExportInstruments()` for test isolation
- `buildTypedExportRoutes` for OpenAPI document generation tests
---
## Related packages
- `@suaveplan/api-plugin-auth` — opt-in `requireAuth` / `requireRole` gating
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/api-core` — RFC 9457 problem serialiser
- `@suaveplan/surrealdb` — `SurrealAdapter` for `SurrealExportStore`

View File

@@ -0,0 +1,262 @@
# sp-api-plugin-file-upload
## One-liner
Multipart + chunked file-upload plugin for the Fetch-native `@suaveplan/api` platform — six auth-gated typed routes, size + MIME validation with magic-byte sniffing, SHA-256 hash verification, resumable chunked sessions, per-principal anti-enumeration, pluggable `UploadStorage` (Memory + FileSystem) and `UploadSessionStore` (Memory + SurrealDB), ten `UPLOAD.*` error codes, and five OTel instruments.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createFileUpload`, `createFileUploadPlugin` | `definePlugin` factory; returns `{ plugin, service, config }` wiring |
| **Routes** | `buildUploadRoutes` | 6 `RouteDescriptor[]` (single-shot, initiate, append-chunk, complete, abort, status) |
| **Service** | `FileUploadService` | Canonical back-end: `initiate`, `appendChunk`, `complete`, `abort`, `get`, `singleShot`, `cleanupExpired` |
| **Storage** | `UploadStorage` contract | `MemoryUploadStorage` (test) + `FileSystemUploadStorage` (production) |
| **Session store** | `UploadSessionStore` contract | `MemoryUploadSessionStore` (default) + `SurrealUploadSessionStore` (durable) |
| **Validators** | `validateMime`, `enforceSizeLimit`, `computeTotalChunks` | MIME allow/deny + magic-byte sniff, size limits, chunk geometry |
| **Assembler** | `assembleChunks`, `sha256Hex`, `verifyHash` | Chunk concat + SHA-256 verification |
| **Error codes** | `UPLOAD_CODES`, `FileUploadError` | 10-code namespace; `status` for RFC 9457 |
| **OTel** | `getUploadInstruments()` | 4 counters + 1 histogram |
---
## Public API surface
### Entry points
```ts
// Wiring factory
const { plugin, service, config } = createFileUpload({
basePath: "/uploads",
singleShotLimitBytes: 32 * 1024 * 1024, // 32 MB
maxFileSizeBytes: 4 * 1024 * 1024 * 1024, // 4 GB
storage: new FileSystemUploadStorage({ rootDir: "/var/lib/uploads" }),
sessionStore: new SurrealUploadSessionStore({ db: adapter }),
});
// Plugin-only factory
const plugin = createFileUploadPlugin({ storage, sessionStore });
```
### Storage adapters
```ts
import { MemoryUploadStorage, FileSystemUploadStorage } from "@suaveplan/api-plugin-file-upload";
// In-memory (tests)
const mem = new MemoryUploadStorage();
// Filesystem (production)
const fs = new FileSystemUploadStorage({ rootDir: "/var/lib/uploads" });
// Stores bytes as flat files; guards against path traversal
```
### Session stores
```ts
import { MemoryUploadSessionStore, SurrealUploadSessionStore } from "@suaveplan/api-plugin-file-upload";
// In-memory (default)
const mem = new MemoryUploadSessionStore();
// SurrealDB-backed
const surreal = new SurrealUploadSessionStore({ db: adapter });
// Requires surql/tables/upload_session.surql + upload_record.surql
```
### Service interface
```ts
interface FileUploadService {
initiate(input: InitiateInput): Promise<UploadSession>;
appendChunk(uploadId, chunkIndex, bytes, scope): Promise<UploadSession>;
complete(uploadId, scope, overrideSha256?): Promise<UploadRecord>;
abort(uploadId, scope): Promise<void>;
get(uploadId, scope): Promise<UploadSession | UploadRecord>;
singleShot(input: SingleShotInput): Promise<UploadRecord>;
cleanupExpired(now: number): Promise<number>;
}
```
### Error codes
```ts
import { UPLOAD_CODES, FileUploadError } from "@suaveplan/api-plugin-file-upload";
// 10 codes: INVALID_INPUT, NOT_FOUND, FORBIDDEN, MIME_REJECTED, TOO_LARGE,
// CHUNK_OUT_OF_ORDER, HASH_MISMATCH, SESSION_EXPIRED, STORAGE_FAILURE, INCOMPLETE
throw new FileUploadError(
UPLOAD_CODES.NOT_FOUND,
`Upload ${uploadId} not found`,
{ context: { statusCode: 404, uploadId } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createFileUpload`, `createFileUploadPlugin`, service, stores, validators |
| `./plugin/plugin` | `createFileUpload`, `createFileUploadPlugin`, `makeCleanupTick` |
| `./routes/routes` | `buildUploadRoutes`, `buildTypedUploadRoutes`, `buildHandlers` |
| `./services/file-upload-service` | `createFileUploadService`, `FileUploadService`, input types |
| `./storage/memory` | `MemoryUploadStorage` |
| `./storage/file-system` | `FileSystemUploadStorage`, `FileSystemUploadStorageOptions` |
| `./session-store/memory` | `MemoryUploadSessionStore` |
| `./session-store/surreal` | `SurrealUploadSessionStore`, config schema |
| `./assembler/assembler` | `assembleChunks`, `sha256Hex`, `verifyHash` |
| `./validators/mime` | `validateMime` |
| `./validators/size` | `enforceSizeLimit`, `computeTotalChunks` |
| `./error-codes/error-codes` | `UPLOAD_CODES`, `FileUploadError` |
| `./otel/instruments` | `getUploadInstruments`, `resetUploadInstruments` |
| `./types/types` | `UploadSession`, `UploadRecord`, `UploadStorage`, `UploadSessionStore` |
---
## Integration patterns
### Auth gating
Every route is `requireAuth()`-gated via route-local middleware. The owner is read from `ctx.principal.id`; cross-user enumeration is impossible — foreign upload IDs return 404.
### Chunked upload flow
```
POST /uploads/initiate → { uploadId, totalChunks }
PUT /uploads/:id/chunks/:n → { receivedChunks: [...] }
... (repeat for each chunk)
POST /uploads/:id/complete → { uploadId, fileName, storageKey, sha256 }
```
### Single-shot upload
```
POST /uploads → multipart/form-data → { uploadId, ... }
```
### Scheduled cleanup
The plugin's `start` hook schedules `cleanupExpired` on a `sessionTtlMs/4`-aligned interval. The timer is `unref()`'d so it does not pin the event loop after `stop`.
---
## Code patterns
### Service with ownership assertions
From `src/services/file-upload-service.ts`:
```ts
export function createFileUploadService(
options: CreateFileUploadServiceOptions
): FileUploadService {
const { config, storage, sessionStore } = options;
const newId = options.idGenerator ?? randomUUID;
const now = options.clock ?? Date.now;
async function ensureSession(
uploadId: string,
scope: PrincipalScope
): Promise<UploadSession> {
const session = await sessionStore.read(uploadId);
if (session === undefined) notFound(uploadId);
assertOwnership(scope, session, uploadId);
if (session.createdAt + config.sessionTtlMs <= now()) {
throw new FileUploadError(
UPLOAD_CODES.SESSION_EXPIRED,
`Upload session ${uploadId} expired`,
{ context: { statusCode: 410, uploadId } }
);
}
return session;
}
return {
async initiate(input) {
assertPrincipalPresent(input);
enforceSizeLimit(input.totalSize, config.maxFileSizeBytes, "initiate");
const totalChunks = computeTotalChunks(input.totalSize, chunkSize);
const mimeType = validateMime({
declaredMimeType: input.mimeType,
allowedMimeTypes: config.allowedMimeTypes,
deniedMimeTypes: config.deniedMimeTypes,
});
const session: UploadSession = {
uploadId: newId(),
principalId: input.principalId,
fileName: input.fileName,
totalSize: input.totalSize,
totalSha256: input.totalSha256.toLowerCase(),
mimeType,
chunkSize,
totalChunks,
receivedChunks: new Set(),
createdAt: now(),
};
await sessionStore.create(session);
return session;
},
// ... appendChunk, complete, abort, get, singleShot, cleanupExpired
};
}
```
### Plugin lifecycle with cleanup timer
From `src/plugin/plugin.ts`:
```ts
const plugin = definePlugin({
id: "file-upload",
version: "0.1.0",
dependsOn: ["auth"],
routes(router) {
for (const route of buildUploadRoutes(service, config.basePath)) {
const cfg = { ...route.meta, middleware: route.middleware };
if (route.method === "POST") router.post(route.path, route.handler, cfg);
else if (route.method === "PUT") router.put(route.path, route.handler, cfg);
else if (route.method === "DELETE") router.delete(route.path, route.handler, cfg);
else router.get(route.path, route.handler, cfg);
}
},
start() {
const intervalMs = Math.max(1000, Math.floor(config.sessionTtlMs / 4));
const timer = setInterval(makeCleanupTick(service), intervalMs);
timer.unref();
state.cleanupTimer = timer;
},
stop() {
if (state.cleanupTimer !== undefined) {
clearInterval(state.cleanupTimer);
state.cleanupTimer = undefined;
}
},
});
```
---
## Error-code registry
`UPLOAD_CODES` via `defineCodes("UPLOAD", [...])` — 10 codes. Every code is thrown from at least one production path. Anti-enumeration: `NOT_FOUND` is used for both missing and foreign-owner uploads.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- `fast-check` for property-based testing
- `resetUploadInstruments()` for test isolation
- `makeCleanupTick` is a top-level exported function for unit testing without the interval scheduler
---
## Related packages
- `@suaveplan/api-plugin-auth` — `requireAuth()` gating + `ctx.principal`
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/mime` — MIME type utilities
- `@suaveplan/surrealdb` — `SurrealAdapter` for `SurrealUploadSessionStore`

View File

@@ -0,0 +1,263 @@
# sp-api-plugin-gtap
## One-liner
Global Trade Automation Platform (GTAP) plugin for the Fetch-native `@suaveplan/api` platform — 16 typed routes across trade licenses, sanctioned-party screening, certificates of origin, and AEO status, persisting to SurrealDB tables via an owned `SurrealAdapter`, with license lifecycle state machine, pluggable sanction-screening adapter, and optional `@suaveplan/api-plugin-customs` HS-code validation.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createGtap`, `createGtapPlugin` | `definePlugin` factory; returns `{ plugin, licenseStore, screeningStore, certificateStore, aeoStore, screenAdapter }` |
| **Routes** | `buildAllGtapRoutes` | 16 `RouteDescriptor[]` (5 licenses + 3 screening + 3 certificates + 5 AEO) |
| **Repositories** | `SurrealTradeLicenseRepository`, `SurrealScreeningLogRepository`, `SurrealCertificateRepository`, `SurrealAeoStatusRepository` | SurrealDB-backed stores over one injected `SurrealAdapter` |
| **In-memory stores** | `InMemoryTradeLicenseStore`, `InMemoryScreeningLogStore`, `InMemoryCertificateStore`, `InMemoryAeoStatusStore` | Reference implementations for tests + OpenAPI doc gen |
| **Lifecycle** | `nextLicenseStatus`, `canApply` | License state machine: `draft → active → {suspended \| expired \| revoked}` |
| **Screening** | `SanctionScreenAdapter` (port) + `InMemorySanctionScreenAdapter` (default impl) | Pluggable watch-list screening; logs every hit |
| **Middleware** | `requireTenant` | Extracts `ctx.tenantId` for per-tenant scoping |
| **Error codes** | `GTAP_API_CODES`, `GtapApiError` | 13-code namespace; `status` for RFC 9457 |
| **OTel** | `getGtapApiInstruments()` | Counters + histograms on production hot paths |
---
## Public API surface
### Entry points
```ts
// Wiring factory — returns plugin + live stores
const { plugin, licenseStore, screeningStore, certificateStore, aeoStore, screenAdapter } =
createGtap({ db: surrealAdapter, routePrefix: "/gtap" });
// Plugin-only factory
const plugin = createGtapPlugin({ db: surrealAdapter });
// With customs HS-code service
const { plugin } = createGtap({
db: surrealAdapter,
hsCodeService: customs.hsCodeService, // optional
});
```
### Repositories
```ts
import {
SurrealTradeLicenseRepository,
SurrealScreeningLogRepository,
SurrealCertificateRepository,
SurrealAeoStatusRepository,
} from "@suaveplan/api-plugin-gtap";
const licenses = new SurrealTradeLicenseRepository({ db: adapter, clock });
const screenings = new SurrealScreeningLogRepository({ db: adapter });
```
### In-memory stores (tests)
```ts
import {
InMemoryTradeLicenseStore,
InMemoryScreeningLogStore,
InMemoryCertificateStore,
InMemoryAeoStatusStore,
} from "@suaveplan/api-plugin-gtap";
```
### Screening adapter
```ts
import { InMemorySanctionScreenAdapter } from "@suaveplan/api-plugin-gtap";
const adapter = new InMemorySanctionScreenAdapter({ threshold: 0.85 });
// threshold: similarity threshold for block-on-hit mode
```
### Middleware
```ts
import { requireTenant } from "@suaveplan/api-plugin-gtap";
const { tenantId } = requireTenant(ctx);
// Throws GTAP_API.TENANT_REQUIRED (401) if ctx.tenantId is missing
```
### Error codes
```ts
import { GTAP_API_CODES, GtapApiError } from "@suaveplan/api-plugin-gtap";
// 13 codes: INVALID_OPTIONS, INTERNAL_ERROR, INVALID_INPUT, DB_UNAVAILABLE,
// TENANT_REQUIRED, LICENSE_NOT_FOUND, LICENSE_EXPIRED, INVALID_LICENSE_TRANSITION,
// SANCTION_HIT, SCREENING_NOT_FOUND, CERTIFICATE_NOT_FOUND, INVALID_HS_CODE, AEO_NOT_FOUND
throw new GtapApiError(
GTAP_API_CODES.INVALID_LICENSE_TRANSITION,
"Cannot suspend a revoked license",
{ context: { statusCode: 409, from: "revoked", to: "suspended" } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createGtap`, `createGtapPlugin`, stores, routes, types |
| `./plugin` | `createGtap`, `createGtapPlugin`, `GtapWiring` |
| `./middleware` | `requireTenant`, `TenantPrincipal` |
| `./repositories/license` | `SurrealTradeLicenseRepository`, `InMemoryTradeLicenseStore`, lifecycle |
| `./repositories/screening` | `SurrealScreeningLogRepository`, `InMemoryScreeningLogStore` |
| `./repositories/certificate` | `SurrealCertificateRepository`, `InMemoryCertificateStore` |
| `./repositories/aeo` | `SurrealAeoStatusRepository`, `InMemoryAeoStatusStore` |
| `./routes` | `buildAllGtapRoutes`, per-group builders, envelope helpers |
| `./adapters` | `InMemorySanctionScreenAdapter`, `WatchListEntry` |
| `./error-codes` | `GTAP_API_CODES`, `GtapApiError` |
| `./otel` | `getGtapApiInstruments`, `resetGtapApiInstruments` |
| `./schemas` | `GtapPluginOptionsSchema` |
| `./types` | Domain types: `TradeLicense`, `AeoStatus`, `CertificateOfOrigin`, etc. |
| `./schema` | `applyGtapSchema`, `buildGtapSchemaStatements` |
---
## Routes (16)
Baked under configurable `routePrefix` (default `/gtap`):
| Group | Routes |
|-------|--------|
| **Trade licenses** | `GET /licenses`, `POST /licenses`, `GET /licenses/:id`, `POST /licenses/:id/transition`, `DELETE /licenses/:id` |
| **Sanction screening** | `POST /screen`, `GET /screen`, `GET /screen/:id` |
| **Certificates of origin** | `POST /certificates`, `GET /certificates`, `GET /certificates/:id` |
| **AEO status** | `POST /aeo-status`, `GET /aeo-status`, `GET /aeo-status/by-operator/:operatorId`, `GET /aeo-status/:id`, `POST /aeo-status/:id/transition` |
---
## Integration patterns
### With api-plugin-auth
Declares `dependsOn: ["auth"]`. Routes read `ctx.principal` for caller identity. Write routes additionally gate via `requireAuth()` / `requireRole()`.
### With api-plugin-customs
Optional HS-code service injection for certificate validation:
```ts
const { hsCodeService } = createCustoms({ db, shipmentService });
const { plugin } = createGtap({ db, hsCodeService });
```
When injected, certificate issuance validates the referenced HS code against the customs reference table (`INVALID_HS_CODE` on mismatch).
### Tenant scoping
Every repository query is scoped to `tenantId`, resolved by `requireTenant(ctx)`. Tables have per-tenant indexes and per-tenant uniqueness on natural keys.
### Schema-on-install
Bundled SurrealQL DDL is applied in `start(ctx)` (gated by `applySchemaOnInit`):
```ts
await applyGtapSchema(db, {
license: "trade_license",
screening: "sanction_screening_log",
certificate: "certificate_of_origin",
aeo: "aeo_status",
});
```
---
## Code patterns
### Route builder with tenant guard
From `src/routes/licenses/licenses.ts`:
```ts
export function buildListLicensesRoute(deps: RouteDeps, prefix: string): RouteDescriptor {
return {
method: "GET",
path: `${basePath(prefix)}/licenses`,
handler: async (ctx) => {
const { tenantId } = requireTenant(ctx);
const params = new URL(ctx.request.url).searchParams;
const query = LicenseQuerySchema.parse({
status: params.get("status") ?? undefined,
direction: params.get("direction") ?? undefined,
limit: params.get("limit") ?? undefined,
offset: params.get("offset") ?? undefined,
});
const licenses = await deps.licenseStore.list({
tenantId,
limit: query.limit ?? 50,
offset: query.offset ?? 0,
});
getGtapApiInstruments().requestsTotal.add(1, {
route: "licenses.list",
outcome: "success",
});
return jsonOk({ licenses: licenses.map(licenseToResponse), total: licenses.length }, 200);
},
meta: {
operationId: "gtap.licenses.list",
summary: "List trade licenses",
request: { query: LicenseQuerySchema },
responses: responseSlot("200", successEnvelope(LicenseListResponseSchema)),
},
};
}
```
### License lifecycle state machine
From `src/repositories/license/lifecycle.ts`:
```ts
export function nextLicenseStatus(
current: TradeLicenseStatus,
event: LicenseEvent
): TradeLicenseStatus {
const transitions: Record<TradeLicenseStatus, Partial<Record<LicenseEvent, TradeLicenseStatus>>> = {
draft: { activate: "active" },
active: { suspend: "suspended", expire: "expired", revoke: "revoked" },
suspended: { reinstate: "active", revoke: "revoked" },
expired: {},
revoked: {},
};
const next = transitions[current]?.[event];
if (next === undefined) {
throw new GtapApiError(
GTAP_API_CODES.INVALID_LICENSE_TRANSITION,
`Cannot ${event} a ${current} license`,
{ context: { statusCode: 409, current, event } }
);
}
return next;
}
```
---
## Error-code registry
`GTAP_API_CODES` via `defineCodes("GTAP_API", [...])` — 13 codes. Every code is thrown from at least one production path. The `status` field maps to the correct HTTP code for the host's RFC 9457 serialiser.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- In-memory stores back unit tests and OpenAPI document generation
- `resetGtapApiInstruments()` for test isolation
---
## Related packages
- `@suaveplan/api-plugin-auth` — auth gating + `ctx.principal`
- `@suaveplan/api-plugin-customs` — optional `HsCodeService` for certificate HS-code validation
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/surrealdb` — `SurrealAdapter`

View File

@@ -0,0 +1,275 @@
# sp-api-plugin-import
## One-liner
Tabular-data ingest plugin for the Fetch-native `@suaveplan/api` platform — CSV / JSON / TSV-XLSX parsers, declarative transform pipeline (coerce + validate + computed), real-time progress tracker, async `ImportStoreAdapter` (in-memory + SurrealDB), canonical `EventBus`-based `ImportService`, six auth-gated typed routes, a long-lived SSE progress stream, RFC 9457 problem+json, nine `IMPORT.*` error codes, and nine OTel instruments.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createImport`, `createImportPlugin` | `definePlugin` factory; returns `{ plugin, service, config }` wiring |
| **Routes** | `buildImportRoutes` | 6 `RouteDescriptor[]` (preview, create, list, get, progress-SSE, delete) |
| **Service** | `ImportService` | Canonical back-end: `preview`, `startImport`, `getJob`, `getResults`, `listJobs`, `deleteJob` |
| **Parsers** | `parseCsv`, `parseJson`, `parseXlsx` | CSV (with headers), JSON array, TSV-style XLSX |
| **Transform** | `transformRow`, `coerceType`, `validateField` | Three-stage pipeline: coerce → validate → computed |
| **Progress** | `ImportProgressTracker` | EventBus-emitting progress tracking |
| **Store** | `ImportStoreAdapter` contract | `MemoryImportStore` (default) + `SurrealImportStore` (durable) |
| **Error codes** | `IMPORT_CODES`, `ImportError` | 9-code namespace; `status` for RFC 9457 |
| **OTel** | `getImportInstruments()` | 9 instruments (4 service + 2 store + 3 SSE) |
---
## Public API surface
### Entry points
```ts
// Wiring factory
const { plugin, service, config } = createImport({
routePrefix: "/imports",
store: new SurrealImportStore({ db: adapter }),
});
// Plugin-only factory
const plugin = createImportPlugin({ routePrefix: "/imports" });
// Subscribe to service events
service.bus.on("import.completed", (snap) => audit.record(snap));
service.bus.on("import.failed", (snap) => alerts.send(snap));
```
### Direct service use (no HTTP)
```ts
import { ImportService } from "@suaveplan/api-plugin-import";
const service = new ImportService();
const progress = await service.startImport(
{
format: "csv",
data: "name\nAda\nBob",
mappings: [{ sourceColumn: "name", targetField: "displayName" }],
},
{ ownerId: "alice" }
);
console.log(progress.successCount); // 2
```
### Store adapters
```ts
import { MemoryImportStore, SurrealImportStore } from "@suaveplan/api-plugin-import";
// In-memory (default)
const mem = new MemoryImportStore();
// SurrealDB-backed
const surreal = new SurrealImportStore({ db: adapter });
// Requires surql/tables/import_job.surql
```
### Transform pipeline
```ts
import { transformRow, coerceType, validateField } from "@suaveplan/api-plugin-import";
const result = transformRow(
{ name: "Ada", age: "42" },
[
{ sourceColumn: "name", targetField: "displayName" },
{ sourceColumn: "age", targetField: "age", transform: (v) => coerceType(v, "number") },
]
);
```
### Error codes
```ts
import { IMPORT_CODES, ImportError } from "@suaveplan/api-plugin-import";
// 9 codes: NOT_FOUND, FORBIDDEN, VALIDATION, FILE_TOO_LARGE, UNSUPPORTED_FORMAT,
// PROCESSING_FAILED, STORAGE_ERROR, TIMEOUT, INVALID_OPTIONS
throw new ImportError(
IMPORT_CODES.NOT_FOUND,
`Import job "${jobId}" not found`,
{ context: { jobId, statusCode: 404 } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createImport`, `createImportPlugin`, `ImportService`, parsers, transform |
| `./plugin` | `createImport`, `createImportPlugin`, `ImportWiring` |
| `./service` | `ImportService`, `ImportServiceOptions`, `ImportServiceListFilter` |
| `./parsers` | `parseCsv`, `parseJson`, `parseXlsx`, `streamParseCsv`, `detectColumns` |
| `./transform` | `transformRow`, `coerceType`, `validateField`, `toRowError` |
| `./progress` | `ImportProgressTracker`, `ProgressCallback`, `ProgressSnapshot` |
| `./store` | `MemoryImportStore`, `ImportStoreAdapter` contract |
| `./store/surreal` | `SurrealImportStore`, config schema |
| `./routes` | `buildImportRoutes`, `buildTypedImportRoutes`, SSE handler |
| `./error-codes` | `IMPORT_CODES`, `ImportError` |
| `./otel` | `getImportInstruments`, `resetImportInstruments` |
| `./schemas` | Plugin options + request/response Zod schemas |
| `./types` | `ImportFormat`, `ImportStatus`, `ImportJobRecord`, `ColumnMapping` |
---
## Integration patterns
### Auth gating
Declares `dependsOn: ["auth"]`. Every route sets route-local `requireAuth()` middleware. Owner is `ctx.principal.id`. Anti-enumeration: foreign-owner lookups return `NOT_FOUND` (404), never `FORBIDDEN`.
### SSE progress stream
```ts
GET /imports/:id/progress → text/event-stream
```
The SSE route is `streaming: true` and excluded from the OpenAPI document. It emits progress events via the service's `EventBus`.
### EventBus subscriptions
```ts
service.bus.on("import.started", (snap) => { /* ... */ });
service.bus.on("import.completed", (snap) => { /* ... */ });
service.bus.on("import.failed", (snap) => { /* ... */ });
service.bus.on("import.progress", (snap) => { /* ... */ });
service.bus.on("import.row_error", (err) => { /* ... */ });
```
### Problem-json
Inline body-validation failures render as RFC 7807/9457 `application/problem+json`:
```ts
import { problemJsonResponse } from "@suaveplan/api-plugin-import";
return problemJsonResponse({
type: "about:blank",
title: "Validation failed",
status: 400,
detail: "request body failed validation",
});
```
---
## Code patterns
### ImportService with EventBus
From `src/service/service.ts`:
```ts
export class ImportService {
public readonly bus: EventBus<ImportEventMap>;
private readonly progress = new Map<string, ImportProgress>();
public constructor(options: ImportServiceOptions = {}) {
this.bus = new EventBus<ImportEventMap>();
this.store = options.store;
this.clock = options.clock ?? defaultClock;
this.idFactory = options.idFactory ?? this.defaultId.bind(this);
this.instruments = options.instruments ?? getImportInstruments();
}
public async startImport(config: ImportJobConfig, owner: ImportOwnerContext): Promise<ImportProgress> {
this.validateFormat(config.format);
if (!config.mappings.length) {
throw new ImportError(IMPORT_CODES.VALIDATION, "at least one mapping is required", {
context: { statusCode: 400 },
});
}
const rows = this.parseData(config.format, config.data);
const jobId = this.idFactory();
const initial: ImportProgress = {
jobId, ownerId: owner.ownerId, status: "importing",
totalRows: rows.length, processedRows: 0, successCount: 0, errorCount: 0,
errors: [], startedAt: new Date(this.clock()).toISOString(),
};
this.progress.set(jobId, initial);
this.bus.emit("import.started", initial);
const imported = this.processRows(jobId, owner.ownerId, rows, config.mappings, ...);
const final = this.getJob(jobId, owner.ownerId);
this.instruments.jobsTotal.add(1, { outcome: final.status === "completed" ? "success" : "error" });
this.instruments.rowsTotal.add(final.successCount, { outcome: "success" });
this.instruments.durationMs.record(this.clock() - startedNs);
return final;
}
/** Owner-scoped read — throws NOT_FOUND on missing/foreign. */
public getJob(jobId: string, ownerId: string): ImportProgress {
const job = this.progress.get(jobId);
if (!job || job.ownerId !== ownerId) {
throw new ImportError(IMPORT_CODES.NOT_FOUND, `Import job "${jobId}" not found`, {
context: { jobId, statusCode: 404 },
});
}
return job;
}
}
```
### Route builder with auth + SSE
From `src/routes/build-routes.ts`:
```ts
export function buildImportRoutes(service: ImportService, routePrefix: string): readonly ImportRouteDescriptor[] {
const base = basePath(routePrefix);
const guard: readonly Middleware[] = [requireAuth()];
const preview: ImportRouteDescriptor = {
method: "POST", path: `${base}/preview`, middleware: guard,
handler: async (ctx) => {
const body = await readBody<{ format: "csv" | "json" | "xlsx"; data: string }>(ctx, PreviewRequestSchema);
const result = service.preview(body.format, body.data);
return jsonResponse({ totalRows: result.totalRows, columns: [...result.columns], ... }, 200);
},
meta: { operationId: "import.preview", summary: "Preview an import payload",
request: { body: PreviewRequestSchema }, responses: ok(PreviewResponseSchema) },
};
const progressSse: ImportRouteDescriptor = {
method: "GET", path: `${base}/:id/progress`, middleware: guard,
handler: createProgressSseHandler({ service }),
meta: { operationId: "import.progress", summary: "Stream import job progress as SSE",
request: { params: IdParamSchema }, streaming: true },
};
return [preview, create, list, get, progressSse, remove];
}
```
---
## Error-code registry
`IMPORT_CODES` via `defineCodes("IMPORT", [...])` — 9 codes. Every code is thrown from at least one production path. Anti-enumeration: foreign-owner reads map to `NOT_FOUND`, never `FORBIDDEN`.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- `resetImportInstruments()` for test isolation
- SSE progress stream tested via the service's `EventBus`
- `buildTypedImportRoutes` excludes the SSE route from OpenAPI document generation
---
## Related packages
- `@suaveplan/api-plugin-auth` — `requireAuth()` gating + `ctx.principal`
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/events` — `EventBus` for progress + completion events
- `@suaveplan/surrealdb` — `SurrealAdapter` for `SurrealImportStore`

View File

@@ -0,0 +1,247 @@
# sp-api-plugin-inventory
## One-liner
Inventory-management plugin for the Fetch-native `@suaveplan/api` platform — 19 typed routes across items, warehouses, stock, movements, adjustments, and reports, persisting to 6 SurrealDB tables via an owned `SurrealAdapter`. Features integer minor-unit costing (FIFO/LIFO/average), a `StockOps` mutation engine, and a best-effort `procurement:grn:completed` subscriber for idempotent stock receipt.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createInventory`, `createInventoryPlugin` | `definePlugin` factory; returns `{ plugin, itemStore, warehouseStore, stockLevelStore, movementStore, lotStore, adjustmentStore, stockOps, grnSubscriber }` |
| **Routes** | `buildAllInventoryRoutes` | 19 `RouteDescriptor[]` (4 items + 4 warehouses + 4 movements + 2 stock + 2 adjustments + 3 reports) |
| **Repositories** | `SurrealItemRepository`, `SurrealWarehouseRepository`, `SurrealStockLevelRepository`, `SurrealMovementRepository`, `SurrealLotRepository`, `SurrealAdjustmentRepository` | SurrealDB-backed stores over one injected `SurrealAdapter` |
| **StockOps** | `StockOps` (`createStockOps`) | Authoritative receive/issue/adjust engine shared by routes and GRN subscriber |
| **Costing** | `computeIssue`, `valuation` | Pure FIFO/LIFO/average cost-of-goods engine |
| **GRN subscriber** | `createGrnSubscriber` | Best-effort `procurement:grn:completed` event handler; idempotent via `@suaveplan/idempotency` |
| **Error codes** | `INVENTORY_API_CODES`, `InventoryApiError` | 11-code namespace; `status` for RFC 9457 |
| **OTel** | `getInventoryApiInstruments()` | Counters + histograms on production hot paths |
---
## Public API surface
### Entry points
```ts
// Wiring factory — returns plugin + live stores + stock engine + GRN subscriber
const { plugin, itemStore, warehouseStore, stockLevelStore, movementStore,
lotStore, adjustmentStore, stockOps, grnSubscriber } =
createInventory({ db: surrealAdapter, routePrefix: "/inventory" });
// Plugin-only factory
const plugin = createInventoryPlugin({ db: surrealAdapter });
```
### StockOps
```ts
import { createStockOps } from "@suaveplan/api-plugin-inventory";
const stockOps = createStockOps({ itemStore, warehouseStore, stockLevelStore,
movementStore, lotStore, adjustmentStore });
// Receive stock — mints a cost-layer lot, appends movement, updates level
const result = await stockOps.receive(tenantId, {
itemId: "item:1", warehouseId: "wh:1",
quantity: 100, unitCost: 500, // minor units (e.g. 500 = $5.00)
lotNumber: "LOT-001",
});
// Issue stock — draws lots down per costing method
const issue = await stockOps.issue(tenantId, {
itemId: "item:1", warehouseId: "wh:1", quantity: 10,
});
// Physical count adjustment
const adjust = await stockOps.adjust(tenantId, {
itemId: "item:1", warehouseId: "wh:1",
countedQuantity: 95, reason: "annual_stock_take",
});
```
### Costing
```ts
import { computeIssue, valuation } from "@suaveplan/api-plugin-inventory";
// Given available lots, compute which lots to consume for an issue
const { consumedLots, totalCost } = computeIssue(lots, quantity, "fifo");
// Methods: "fifo" | "lifo" | "average"
// Valuation of remaining stock
const value = valuation(remainingLots, "average");
```
### Repositories
```ts
import { SurrealItemRepository, SurrealWarehouseRepository,
SurrealStockLevelRepository, SurrealMovementRepository,
SurrealLotRepository, SurrealAdjustmentRepository } from "@suaveplan/api-plugin-inventory";
```
### Error codes
```ts
import { INVENTORY_API_CODES, InventoryApiError } from "@suaveplan/api-plugin-inventory";
// 11 codes: INVALID_OPTIONS, INVALID_INPUT, DB_UNAVAILABLE, TENANT_REQUIRED,
// ITEM_NOT_FOUND, WAREHOUSE_NOT_FOUND, INSUFFICIENT_STOCK, LOT_EXPIRED,
// COSTING_ERROR, ADJUSTMENT_CONFLICT, DUPLICATE_SKU
throw new InventoryApiError(
INVENTORY_API_CODES.INSUFFICIENT_STOCK,
"Not enough stock available",
{ context: { statusCode: 409, itemId, warehouseId, requested: 10, available: 3 } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createInventory`, `createInventoryPlugin`, stores, `StockOps`, costing, GRN subscriber |
| `./plugin` | `createInventory`, `createInventoryPlugin`, `InventoryWiring` |
| `./repositories` | All 6 SurrealDB repositories + store contracts |
| `./costing` | `computeIssue`, `valuation` |
| `./stock-ops` | `createStockOps`, `StockOps`, input/output types |
| `./routes` | `buildAllInventoryRoutes`, envelope helpers |
| `./error-codes` | `INVENTORY_API_CODES`, `InventoryApiError` |
| `./otel` | `getInventoryApiInstruments`, `resetInventoryApiInstruments` |
| `./middleware` | `requireTenant`, `TenantPrincipal` |
| `./schemas` | `InventoryPluginOptionsSchema` |
| `./types` | Domain types: `InventoryItem`, `Warehouse`, `StockLevel`, `Lot`, etc. |
| `./schema` | `applyInventorySchema`, `buildInventorySchemaStatements` |
---
## Integration patterns
### With api-plugin-procurement (GRN → stock)
The plugin best-effort subscribes to `procurement:grn:completed` in `start(ctx)`:
```ts
const { plugin: procurement } = createProcurement({ db, events });
const { plugin: inventory } = createInventory({ db, events });
// Completing a GRN through procurement auto-receives stock in inventory
```
Ingestion is idempotent (keyed by `grnId` via `@suaveplan/idempotency`). When no events capability is wired, ingestion is a logged no-op and `start` still succeeds.
### With api-plugin-auth
Declares `dependsOn: ["auth"]`. Routes read `ctx.principal` and `ctx.tenantId` via `requireTenant(ctx)`. All monetary values are integer minor currency units — no `decimal.js` dependency.
### Tenant scoping
Every repository query is scoped to `tenantId`. Tables have per-tenant indexes and per-tenant uniqueness on natural keys (e.g. `(tenantId, sku)` for items).
---
## Code patterns
### StockOps receive operation
From `src/stock-ops/stock-ops.ts`:
```ts
export class StockOps {
public async receive(tenantId: string, input: ReceiveInput): Promise<ReceiveResult> {
const item = await this.requireItem(tenantId, input.itemId);
const warehouse = await this.requireWarehouse(tenantId, input.warehouseId);
if (input.unitCost < 0) {
throw new InventoryApiError(
INVENTORY_API_CODES.INVALID_INPUT,
"receive unitCost must be >= 0",
{ context: { statusCode: 400 } }
);
}
if (input.expiresAt !== undefined && input.expiresAt.getTime() <= 0) {
throw new InventoryApiError(
INVENTORY_API_CODES.LOT_EXPIRED,
"received lot has already expired",
{ context: { statusCode: 400 } }
);
}
const totalCost = input.quantity * input.unitCost;
const lot = await this.stores.lotStore.create(tenantId, {
itemId: item.id, warehouseId: warehouse.id,
lotNumber: input.lotNumber ?? `LOT-${input.itemId}`,
quantityReceived: input.quantity, unitCost: input.unitCost,
});
const movement = await this.stores.movementStore.append(tenantId, {
itemId: item.id, warehouseId: warehouse.id,
type: "receive", quantity: input.quantity, unitCost: input.unitCost, totalCost,
});
// ... update stock level
}
}
```
### Plugin with GRN subscriber lifecycle
From `src/plugin/plugin.ts`:
```ts
export function createInventoryPlugin(options: InventoryPluginOptions): Plugin {
return buildPlugin(resolve(options), options);
}
function buildPlugin(resolved: Resolved, options: InventoryPluginOptions): Plugin {
const { deps, grnSubscriber, config, events, logger } = resolved;
return definePlugin({
id: "inventory",
version: "0.1.0",
dependsOn: ["auth"],
routes(router) {
for (const route of buildAllInventoryRoutes(deps, config.routePrefix)) {
dispatch(router, route);
}
},
async start(ctx) {
if (config.applySchemaOnInit) {
await applyInventorySchema(options.db, config.tables);
}
if (!config.subscribeToProcurement) return;
const bus = resolveEventsBus(ctx, events);
if (bus === undefined) {
logger?.info("inventory GRN ingestion is disabled — no events capability");
return;
}
grnSubscriber.bind((topic, listener) => bus.on(topic, listener));
},
async stop() {
grnSubscriber.unbind();
},
});
}
```
---
## Error-code registry
`INVENTORY_API_CODES` via `defineCodes("INVENTORY_API", [...])` — 11 codes. Every code is thrown from at least one production path.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- In-memory stores for unit tests
- `resetInventoryApiInstruments()` for test isolation
- Integration tests run the full item → warehouse → receive → issue → adjust lifecycle through the real `createApi` runtime
---
## Related packages
- `@suaveplan/api-plugin-auth` — auth gating + `ctx.principal`
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/api-plugin-procurement` — emits `procurement:grn:completed` events
- `@suaveplan/idempotency` — GRN subscriber deduplication
- `@suaveplan/surrealdb` — `SurrealAdapter`

View File

@@ -0,0 +1,286 @@
# sp-api-plugin-kit
## One-liner
Plugin-authoring toolkit for the Suaveplan API platform — provides `definePlugin` (a typed authoring factory that normalises a friendly spec into a `@suaveplan/api` `Plugin` and narrows `ctx.capabilities` to the declared `requires` set), an in-memory test harness (`mountPlugin`/`createTestApp`) that serves plugins through the real `createApi` runtime with start/stop/reload helpers and no mocks, and a canonical reference example plugin proving the full author-to-runtime-to-serve-to-reload contributor loop.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **definePlugin** | `definePlugin(spec)` | Authoring factory — turns a `DefinePluginSpec` into a host `Plugin` |
| **KitRouter** | `RecordingRouter` | In-memory router that records `RouteDescriptor[]` per verb; composes plugin-scoped + route-local middleware |
| **Harness** | `mountPlugin`, `createTestApp` | Stand up an in-memory app over the real `createApi` runtime; no mocks |
| **Example** | `examplePlugin`, `examplePluginV2` | Canonical reference plugins proving the full surface end-to-end |
---
## Public API surface
### definePlugin
```ts
import { definePlugin } from "@suaveplan/api-plugin-kit";
const greeter = definePlugin({
id: "greeter",
version: "1.0.0",
requires: ["logger"] as const,
routes(router) {
router.get("/hello/:name", (ctx) => {
ctx.capabilities.logger.info("greet", { name: ctx.params.name });
return json({ message: `hello, ${ctx.params.name}` });
});
},
});
```
### DefinePluginSpec
```ts
interface DefinePluginSpec<Caps extends KitCapabilities = KitCapabilities> {
id: string; // manifest.name
version: string; // manifest.version
displayName?: string;
description?: string;
dependsOn?: readonly string[]; // topo-sort ordering
requires?: Caps; // capability ids; narrows handler context
permissions?: PermissionsManifest;
isolation?: "trusted" | "isolated" | "sandboxed";
trustLevel?: "first-party" | "third-party" | "untrusted";
apiHost?: string; // semver range (default "^0.1.0")
globalMiddleware?: readonly Middleware<Caps>[]; // pipeline-wide
routes(router: KitRouter<Caps>): void;
start?(ctx: PluginContext<Caps>): Promise<void> | void;
stop?(ctx: PluginContext<Caps>): Promise<void> | void;
}
```
### KitRouter
```ts
interface KitRouter<Caps extends KitCapabilities> {
get(path, handler, config?): void;
post(path, handler, config?): void;
put(path, handler, config?): void;
patch(path, handler, config?): void;
delete(path, handler, config?): void;
options(path, handler, config?): void;
head(path, handler, config?): void;
use(middleware: Middleware<Caps>): void; // plugin-scoped
descriptors(): readonly RouteDescriptor<Caps>[];
}
```
Per-route config (`KitRouteConfig`) carries `RouteMeta` + optional `middleware` (route-local):
```ts
router.get("/items/:id", handler, {
operationId: "items.get",
summary: "Get an item",
request: { params: IdParamSchema },
responses: { "200": { body: ItemSchema } },
middleware: [requireAuth()],
});
```
### Test harness
```ts
import { mountPlugin, createTestApp } from "@suaveplan/api-plugin-kit";
// Single plugin
const app = await mountPlugin(greeter);
const res = await app.fetch("/hello/ada");
console.log(await res.json()); // { message: "hello, ada" }
await app.dispose();
// Multiple plugins with custom capabilities
const app = await createTestApp([pluginA, pluginB], {
capabilities: [{ id: "logger", factory: () => logger }],
autoStart: true,
});
// Reload a plugin (blue-green via InMemoryModuleResolver)
await app.reload("greeter", nextVersionPlugin);
// Lifecycle helpers
await app.start("plugin-id");
await app.stop("plugin-id");
await app.dispose();
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `definePlugin`, `mountPlugin`, `createTestApp`, `examplePlugin`, types |
| `./define-plugin` | `definePlugin`, `KitRouter`, `KitRouteConfig`, `DefinePluginSpec`, `KitCapabilities` |
| `./harness` | `mountPlugin`, `createTestApp`, `TestAppHandle`, `RecordingLogger` |
| `./example` | `examplePlugin`, `examplePluginV2` |
---
## Integration patterns
### Capability narrowing
`requires: ["logger"] as const` narrows `ctx.capabilities` so handlers see only the declared capabilities:
```ts
definePlugin({
id: "audit",
requires: ["logger", "db"] as const,
routes(router) {
// ctx.capabilities is typed as { logger: Logger; db: Database }
router.post("/audit", (ctx) => {
ctx.capabilities.logger.info("audit", { event: "login" });
ctx.capabilities.db.query("INSERT audit ...");
});
},
});
```
### Plugin-scoped vs route-local vs global middleware
Three middleware scopes:
1. **Plugin-scoped** (`router.use(mw)`) — runs for every route THIS plugin registers
2. **Route-local** (`config.middleware`) — runs only for that specific route
3. **Global** (`globalMiddleware` in spec) — runs for EVERY route in the ENTIRE app (cross-cutting concerns like rate-limit, CORS, audit)
### RecordingRouter middleware composition
From `src/define-plugin/define-plugin.ts`:
```ts
class RecordingRouter<Caps> implements KitRouter<Caps> {
private readonly recorded: RouteDescriptor<Caps>[] = [];
private readonly scoped: Middleware<Caps>[] = [];
public use(middleware: Middleware<Caps>): void {
this.scoped.push(middleware);
}
public descriptors(): readonly RouteDescriptor<Caps>[] {
return this.recorded.map((descriptor) => {
const local = descriptor.middleware ?? [];
const middleware = [...this.scoped, ...local];
return middleware.length === 0
? descriptor
: { ...descriptor, middleware };
});
}
}
```
---
## Code patterns
### definePlugin implementation
From `src/define-plugin/define-plugin.ts`:
```ts
export function definePlugin<Caps extends KitCapabilities = KitCapabilities>(
spec: DefinePluginSpec<Caps>
): Plugin {
const manifest = buildManifest(spec);
const base: Plugin = {
manifest,
register(reg: RouteRegistrar): void {
const router = new RecordingRouter<Caps>();
spec.routes(router);
for (const descriptor of router.descriptors()) {
reg.addRoute(descriptor as RouteDescriptor);
}
},
};
const plugin: Plugin =
spec.globalMiddleware === undefined
? base
: { ...base, globalMiddleware: spec.globalMiddleware as readonly Middleware[] };
if (spec.start !== undefined) {
const start = spec.start;
return Object.assign(plugin, {
start: (ctx: PluginContext) => start(ctx as PluginContext<Caps>),
...(spec.stop === undefined ? {} : { stop: assignStop(spec.stop) }),
});
}
if (spec.stop !== undefined) {
return Object.assign(plugin, { stop: assignStop(spec.stop) });
}
return plugin;
}
```
### Test harness — createTestApp
From `src/harness/harness.ts`:
```ts
export async function createTestApp(
plugins: readonly Plugin[],
options: TestAppOptions = {}
): Promise<TestAppHandle> {
const records: LogRecord[] = [];
const logger = createRecordingLogger(records);
const capabilities = options.capabilities ?? [{ id: "logger", factory: () => logger }];
const resolver = new InMemoryModuleResolver();
const runtime = createApi({ plugins, capabilities, moduleResolver: resolver });
if (options.autoStart !== false) {
for (const plugin of plugins) {
await runtime.start(plugin.manifest.name);
}
}
return {
runtime,
logger,
async fetch(input, init): Promise<Response> {
return runtime.fetch(toRequest(input, init));
},
async reload(pluginName, nextPlugin): Promise<void> {
const specifier = `kit:reload/${pluginName}`;
resolver.set(specifier, nextPlugin);
await runtime.reload(pluginName, specifier);
},
async dispose(): Promise<void> {
// stop all started plugins
},
};
}
```
---
## Design philosophy
`api-plugin-kit` implements the **Authoring** row of the API platform redesign. It supersedes:
- The former `create-suaveplan-plugin` scaffolder
- The `api-plugin` authoring surface
- The old `api-plugin-example`
Every other `api-plugin-*` package in the monorepo is authored with `definePlugin` from this kit. The kit is **not** a plugin itself — it is the toolchain used to build plugins.
---
## Testing notes
- The harness runs plugins through the **real** `createApi` runtime — no mocks
- `RecordingLogger` captures all log output for assertions
- `InMemoryModuleResolver` enables blue-green reload testing
- `examplePlugin` and `examplePluginV2` serve as living documentation
---
## Related packages
- `@suaveplan/api` — the host runtime (`createApi`, `Plugin`, `RouteRegistrar`)
- `@suaveplan/api-types` — `RouteDescriptor`, `RouteMeta`, `Middleware`, `PluginContext`
- `@suaveplan/api-core` — `json()` response helper
- Every `api-plugin-*` package — all authored with this kit

View File

@@ -0,0 +1,215 @@
# sp-api-plugin-logistics
## One-liner
Shipment / carrier / transport-leg logistics plugin for the Fetch-native `@suaveplan/api` platform — 30 typed routes across carriers, shipments, pallets, containers, transport-legs, and loss-reports, persisting to 6 SurrealDB tables via an owned `SurrealAdapter`. Features a shipment state machine (`created → dispatched → in_transit → delivered`), Code128 barcode labels, pallet weight-capacity and container volume-capacity enforcement, and six load-bearing services for downstream plugin consumption.
---
## Architecture
| Layer | Key components | Role |
|-------|----------------|------|
| **Plugin** | `createLogistics`, `createLogisticsPlugin` | `definePlugin` factory; returns `{ plugin, shipmentService, carrierService, palletService, containerService, transportLegService, lossInvestigationService }` |
| **Routes** | `buildAllLogisticsRoutes` | 30 `RouteDescriptor[]` (5 carriers + 7 shipments + 5 pallets + 5 containers + 3 transport-legs + 5 loss-reports) |
| **Repositories** | `SurrealCarrierRepository`, `SurrealShipmentRepository`, `SurrealPalletRepository`, `SurrealContainerRepository`, `SurrealTransportLegRepository`, `SurrealLossReportRepository` | SurrealDB-backed stores |
| **In-memory stores** | `InMemoryCarrierStore`, `InMemoryShipmentStore`, etc. | Reference implementations for tests + OpenAPI doc gen |
| **Services** | `ShipmentService`, `CarrierService`, `PalletService`, `ContainerService`, `TransportLegService`, `LossInvestigationService` | Orchestration layer with lifecycle rules; consumed by downstream plugins |
| **State machine** | `canTransition`, `assertTransition` | Shipment lifecycle: `created → dispatched → in_transit → delivered`; any non-delivered → `lost` |
| **Error codes** | `LOGISTICS_API_CODES`, `LogisticsApiError` | 13-code namespace; `status` for RFC 9457 |
| **OTel** | `getLogisticsApiInstruments()` | Counters + histograms |
---
## Public API surface
### Entry points
```ts
// Wiring factory — returns plugin + 6 live services
const { plugin, shipmentService, carrierService, palletService,
containerService, transportLegService, lossInvestigationService } =
createLogistics({ db: surrealAdapter, routePrefix: "/logistics" });
// Plugin-only factory
const plugin = createLogisticsPlugin({ db: surrealAdapter });
```
### Services (downstream consumption)
```ts
// Customs and port-tracking plugins consume ShipmentService directly
const { shipmentService } = createLogistics({ db });
const label = await shipmentService.getLabelBarcode(shipmentId);
// Returns: { shipmentId, reference, payload, svg }
```
### Shipment state machine
```ts
import { canTransition, assertTransition } from "@suaveplan/api-plugin-logistics";
// Predicate check
if (canTransition("created", "dispatched")) { /* ... */ }
// Assert + return (throws LOGISTICS_API.ILLEGAL_STATUS_TRANSITION on illegal)
const next = assertTransition(currentStatus, requestedStatus);
```
### Capacity enforcement
```ts
// Pallet: rejects add that would exceed maxWeightKg → PALLET_OVERLOAD (409)
// Container: rejects add that would exceed maxVolumeM3 → CONTAINER_FULL (409)
```
### Barcode labels
```ts
GET /logistics/shipments/:id/label → Code128 SVG label
// Powered by @suaveplan/barcode
```
### Error codes
```ts
import { LOGISTICS_API_CODES, LogisticsApiError } from "@suaveplan/api-plugin-logistics";
// 13 codes: INVALID_OPTIONS, INVALID_INPUT, DB_UNAVAILABLE, TENANT_REQUIRED,
// SHIPMENT_NOT_FOUND, CARRIER_NOT_FOUND, PALLET_NOT_FOUND, CONTAINER_NOT_FOUND,
// LOSS_REPORT_NOT_FOUND, ILLEGAL_STATUS_TRANSITION, PALLET_OVERLOAD,
// CONTAINER_FULL, LOSS_REPORT_ALREADY_RESOLVED
throw new LogisticsApiError(
LOGISTICS_API_CODES.ILLEGAL_STATUS_TRANSITION,
"cannot move shipment from 'delivered' to 'in_transit'",
{ context: { statusCode: 409, from: "delivered", to: "in_transit" } }
);
```
### Subpath exports
| Export path | Exports |
|-------------|---------|
| `.` | `createLogistics`, `createLogisticsPlugin`, services, stores, routes |
| `./plugin` | `createLogistics`, `createLogisticsPlugin`, `LogisticsWiring` |
| `./services` | All 6 services + options types |
| `./repositories` | All 6 Surreal + in-memory stores + contracts |
| `./routes` | `buildAllLogisticsRoutes`, per-group builders, schemas |
| `./error-codes` | `LOGISTICS_API_CODES`, `LogisticsApiError` |
| `./otel` | `getLogisticsApiInstruments`, `resetLogisticsApiInstruments` |
| `./middleware` | `requireTenant`, `TenantPrincipal` |
| `./schemas` | `LogisticsPluginOptionsSchema` |
| `./types` | Domain types: `Carrier`, `Shipment`, `Pallet`, `Container`, etc. |
| `./schema` | `applyLogisticsSchema`, `buildLogisticsSchemaStatements` |
---
## Routes (30)
| Group | Routes |
|-------|--------|
| **Carriers** | `GET /carriers`, `POST /carriers`, `GET /carriers/:id`, `PATCH /carriers/:id`, `DELETE /carriers/:id` |
| **Shipments** | `GET /shipments`, `POST /shipments`, `GET /shipments/:id`, `PATCH /shipments/:id`, `POST /shipments/:id/transition`, `DELETE /shipments/:id`, `GET /shipments/:id/label` |
| **Pallets** | `GET /pallets`, `POST /pallets`, `GET /pallets/:id`, `POST /pallets/:id/shipments`, `DELETE /pallets/:id/shipments/:shipmentId` |
| **Containers** | `GET /containers`, `POST /containers`, `GET /containers/:id`, `POST /containers/:id/pallets`, `DELETE /containers/:id/pallets/:palletId` |
| **Transport legs** | `GET /shipments/:id/legs`, `POST /shipments/:id/legs`, `DELETE /legs/:legId` |
| **Loss reports** | `GET /loss-reports`, `POST /loss-reports`, `GET /loss-reports/:id`, `PATCH /loss-reports/:id`, `POST /loss-reports/:id/resolve` |
---
## Integration patterns
### With api-plugin-auth
Declares `dependsOn: ["auth"]`. Write routes gate via `requireAuth()`; all routes read `ctx.principal` + `ctx.tenantId` via `requireTenant(ctx)`.
### Downstream plugin consumption
Customs and port-tracking plugins consume `ShipmentService` directly (not the repository) so lifecycle rules stay in one place:
```ts
const { shipmentService } = createLogistics({ db });
// Use in customs / port-tracking plugin
```
### Capacity enforcement
- **Pallet overload**: Adding a shipment that would exceed `maxWeightKg` → `PALLET_OVERLOAD` (409)
- **Container full**: Adding a pallet that would exceed `maxVolumeM3` → `CONTAINER_FULL` (409); each pallet occupies a fixed `palletVolumeM3` footprint
---
## Code patterns
### Shipment state machine
From `src/services/shipment/state-machine.ts`:
```ts
const TRANSITIONS: Readonly<Record<ShipmentStatus, readonly ShipmentStatus[]>> = {
created: ["dispatched", "lost"],
dispatched: ["in_transit", "lost"],
in_transit: ["delivered", "lost"],
delivered: [],
lost: [],
};
export function canTransition(from: ShipmentStatus, to: ShipmentStatus): boolean {
return TRANSITIONS[from].includes(to);
}
export function assertTransition(from: ShipmentStatus, to: ShipmentStatus): ShipmentStatus {
if (!canTransition(from, to)) {
throw new LogisticsApiError(
LOGISTICS_API_CODES.ILLEGAL_STATUS_TRANSITION,
`cannot move shipment from '${from}' to '${to}'`,
{ context: { statusCode: 409, from, to } }
);
}
return to;
}
```
### Service-layer orchestration
From `src/plugin/plugin.ts`:
```ts
const shipmentService = new ShipmentService({ store: shipmentStore, clock, idGenerator });
const palletService = new PalletService({
palletStore, shipmentStore,
defaultMaxWeightKg: parsed.data.palletDefaultMaxKg,
clock, idGenerator,
});
const containerService = new ContainerService({
containerStore, palletStore,
defaultMaxVolumeM3: parsed.data.containerDefaultMaxM3,
clock, idGenerator,
});
```
---
## Error-code registry
`LOGISTICS_API_CODES` via `defineCodes("LOGISTICS_API", [...])` — 13 codes. Every code is thrown from at least one production path.
---
## Testing notes
- Uses `@suaveplan/testing-integration-surrealdb` for real SurrealDB testcontainer tests
- In-memory stores back unit tests and OpenAPI document generation
- Integration tests run the full carrier → shipment → leg → loss-report lifecycle through the real `createApi` runtime
- `tests/integration/openapi-doc.test.ts` asserts all 30 operations appear in the emitted OpenAPI document
- `resetLogisticsApiInstruments()` for test isolation
---
## Related packages
- `@suaveplan/api-plugin-auth` — auth gating + `ctx.principal`
- `@suaveplan/api-plugin-kit` — `definePlugin` scaffold
- `@suaveplan/barcode` — Code128 SVG label generation
- `@suaveplan/surrealdb` — `SurrealAdapter`

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