updated skills
This commit is contained in:
@@ -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`
|
||||
@@ -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
|
||||
171
plugins/library-navigator/skills/SKILL.md
Normal file
171
plugins/library-navigator/skills/SKILL.md
Normal 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.
|
||||
187
plugins/library-navigator/skills/api-stack/SKILL.md
Normal file
187
plugins/library-navigator/skills/api-stack/SKILL.md
Normal 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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
57
plugins/library-navigator/skills/api-stack/api-auth/SKILL.md
Normal file
57
plugins/library-navigator/skills/api-stack/api-auth/SKILL.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
39
plugins/library-navigator/skills/api-stack/api-core/SKILL.md
Normal file
39
plugins/library-navigator/skills/api-stack/api-core/SKILL.md
Normal 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.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
39
plugins/library-navigator/skills/api-stack/api-db/SKILL.md
Normal file
39
plugins/library-navigator/skills/api-stack/api-db/SKILL.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
33
plugins/library-navigator/skills/api-stack/api-jobs/SKILL.md
Normal file
33
plugins/library-navigator/skills/api-stack/api-jobs/SKILL.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
225
plugins/library-navigator/skills/api-stack/api-plugins/SKILL.md
Normal file
225
plugins/library-navigator/skills/api-stack/api-plugins/SKILL.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user