Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# `@suaveplan/api-plugin-example` — reference plugin exercising every capability under every isolation tier
**Package**: `@suaveplan/api-plugin-example`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 8 — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Per design doc §13.5, the reference plugin is a load-bearing integration test, not a tutorial. It exercises every contract surface in the foundation — every capability, every lifecycle hook, every route variant, every distributed-transaction surface, every error code mapping, every migration class. The host's CI runs the example's full integration suite on every host change so any host-side change that breaks the plugin contract fails loudly, in one place, against real Postgres + real Hono router.
The example also doubles as the canonical onboarding doc: when a plugin author asks "how do I use capability X?", the answer is "look at how `api-plugin-example` uses it."
## What changes
- ADDED: A complete `plugin.json` exercising every top-level key (`requiredCapabilities`, `optionalCapabilities`, `isolation`, `trustLevel`, `permissions`, `dependsOn`, `routes`, `graphql`, `websockets`, `sse`, `sagas`, `twoPhaseCommit`, `database`, `jobs`, `events`, `secrets`, `config`, `scopes`, `telemetry`, `openapi`, `errors`, `uninstall`).
- ADDED: `src/index.ts` exporting `definePlugin({ ... })` with:
- REST routes covering every Section 6 step: public, private, validated, streaming, idempotent, rate-limited, RBAC-gated.
- GraphQL types + resolvers + subscriptions (federated alongside a second tiny plugin in the integration test).
- WebSocket endpoint with subscription fan-out (single-host bus AND multi-host bus exercised).
- SSE endpoint with `Last-Event-Id` resume.
- Owned saga (3-step) with compensations + 2PC participation flag.
- Scheduled cron jobs (`reconcile-invoices`, `send-monthly-statements` per-tenant) + queued jobs (`send-receipt`).
- Event publish + subscribe (`example.invoice.created`, subscribes to `users.user.deleted`).
- Custom error codes (`PLUGIN.EXAMPLE.*`) with manifest-mapped statuses + redaction rules.
- Custom metrics declared in manifest (`example.invoices_finalised_total`, `example.stripe.call_duration_ms`, `example.outstanding_invoice_count`).
- Multiple secrets declared.
- ADDED: Migrations across two `schemaVersion` bumps in `migrations/postgres/` (and surrealdb / sqlite mirrors) to exercise upgrade-on-reload AND auto-rollback when the second migration is forced to fail (`tests/integration/migration-rollback.integration.test.ts`).
- ADDED: Integration test (`tests/integration/full-flow.integration.test.ts`) that boots the example against:
1. `isolation: "trusted"`
2. `isolation: "isolated"` (worker thread + worker-rpc)
3. `isolation: "sandboxed"` with vm (`api-sandbox-vm`)
4. `isolation: "sandboxed"` with WASM (`api-sandbox-wasm`)
and exercises the full surface in each tier. Real Postgres via `testing-integration-postgres`, real Hono router.
- ADDED: Intentional schema-violation route (`/_/test/fail-response?failResponse=1`) to integration-test response validation in `enforce` and `warn` modes.
## Out of scope
- Production-grade business logic — the example's routes do the minimum to exercise the surfaces. They are NOT a billing engine.
- Cross-plugin federation specifics beyond the second-plugin pair used for federation tests.
- A web UI for the example — covered by separate frontend work.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Reference plugin exercises every capability | §13.5 |
| Routes covering every Section 6 step | §13.5 |
| Migrations across two schema versions | §13.5 |
| Integration test against real Postgres + real Hono | §13.5 |
| Deploys against all three isolation tiers | §15 "What done looks like" |
## Verification approach
The example IS the integration test target. The spec is structured so every documented capability is a separate Requirement whose `Verified by:` cites a concrete integration-test `it(...)`. The integration tests live in this package's `tests/integration/` directory and run as part of host CI.
- `tests/integration/full-flow.integration.test.ts` — runs the full surface against all four isolation modes (parameterised over `["trusted", "isolated", "sandboxed-vm", "sandboxed-wasm"]`).
- `tests/integration/migration-rollback.integration.test.ts` — schema-bump upgrade + forced-failure rollback.
- `tests/integration/response-validation.integration.test.ts` — `?failResponse=1` in `enforce` vs `warn` modes.
- `tests/integration/graphql-federation.integration.test.ts` — federated query across this plugin and a sibling test plugin.
- `tests/integration/ws-broadcast.integration.test.ts` — single-host and multi-host fan-out.
- `tests/integration/saga-2pc.integration.test.ts` — owned saga full happy + compensated paths; 2PC vote/commit/abort cycle.
## Migration impact
None — additive only. The example serves as the canonical integration target for host CI.

View File

@@ -0,0 +1,267 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-plugin-example (api-plugin-example)
**Package**: `@suaveplan/api-plugin-example`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. This spec doubles as an integration-test specification — every requirement names a concrete integration `it(...)`.
## ADDED Requirements
### Requirement: Plugin deploys cleanly under every isolation tier
The example plugin SHALL boot to `started` state under `isolation: "trusted"`, `isolation: "isolated"`, `isolation: "sandboxed"` (vm), AND `isolation: "sandboxed"` (wasm) configurations. The full integration suite runs in each.
#### Scenario: trusted-tier boot
- WHEN the host installs the plugin with `isolation: "trusted"`
- THEN the plugin reaches `started` AND a request to `/example/health` returns 200
#### Scenario: isolated-tier boot
- WHEN the host installs the plugin with `isolation: "isolated"`
- THEN the worker thread starts AND a request to `/example/health` returns 200
#### Scenario: sandboxed-vm boot
- WHEN the host installs the plugin with `isolation: "sandboxed"` and `host.defaultSandbox = "vm"`
- THEN the plugin runs inside `api-sandbox-vm` AND a request to `/example/health` returns 200
#### Scenario: sandboxed-wasm boot
- WHEN the host installs the plugin with `isolation: "sandboxed"` and `host.defaultSandbox = "wasm"`
- THEN the plugin runs inside `api-sandbox-wasm` AND a request to `/example/health` returns 200
**Verified by:** `tests/integration/full-flow.integration.test.ts::"plugin boots and serves /example/health in <mode>"` (parameterised over four modes)
---
### Requirement: REST surface covers every Section 6 pipeline step
The plugin SHALL expose routes that exercise every step of the per-request pipeline: public, private+RBAC-gated, validated, streaming, idempotent, rate-limited.
#### Scenario: public route bypasses auth
- WHEN a request hits `/example/public/ping`
- THEN response is 200 with no `auth.session` resolution required
#### Scenario: private RBAC-gated route enforces scope
- WHEN a request hits `/example/invoices` with a principal lacking `example:read`
- THEN response is 403 with `code === "RBAC_MISSING_SCOPE"`
#### Scenario: validated body rejects malformed payload
- WHEN a request hits `/example/invoices` with `body.amountCents` missing
- THEN response is 400 with the Zod-validated error envelope
#### Scenario: streaming route streams chunks
- WHEN a request hits `/example/stream/large-report`
- THEN response uses chunked transfer-encoding AND every chunk is delivered before the handler returns
#### Scenario: idempotency replays original response
- WHEN two requests with the same `Idempotency-Key` hit `/example/invoices`
- THEN the second response is byte-identical to the first AND only one DB row is created
#### Scenario: rate-limited route returns 429 with Retry-After
- WHEN burst is exceeded on `/example/rate-limited`
- THEN response is 429 with `Retry-After` set
**Verified by:** `tests/integration/full-flow.integration.test.ts::"REST surface — public bypasses auth"` AND `...::"REST surface — RBAC denies missing scope"` AND `...::"REST surface — Zod rejects malformed body"` AND `...::"REST surface — streaming delivers chunked"` AND `...::"REST surface — idempotency replays response"` AND `...::"REST surface — rate-limited returns 429 with Retry-After"`
---
### Requirement: GraphQL surface — types, resolvers, federation, subscription
The plugin SHALL contribute GraphQL types AND resolvers AND a subscription channel. A sibling test plugin SHALL be installed in the integration test to exercise federation.
#### Scenario: federated query crosses plugins
- WHEN a GraphQL query touches types from both `api-plugin-example` and the sibling plugin
- THEN the result contains both AND no field is `null` due to federation failure
#### Scenario: GraphQL subscription delivers events
- WHEN a subscription to `exampleInvoiceCreated` is open AND an invoice is created via REST
- THEN the subscription delivers exactly one message
**Verified by:** `tests/integration/graphql-federation.integration.test.ts::"federated query crosses plugins"` AND `tests/integration/graphql-federation.integration.test.ts::"GraphQL subscription delivers exampleInvoiceCreated"`
---
### Requirement: WebSocket surface — subscription fan-out single + multi-host
The plugin SHALL expose `/example/ws` with subscription fan-out. The integration test exercises single-host fan-out AND a multi-host topology via the broker adapter.
#### Scenario: single-host fan-out delivers to every subscriber
- WHEN two subscribers connect to `/example/ws` and the plugin publishes `example.invoice.created`
- THEN both subscribers receive the message exactly once
#### Scenario: multi-host fan-out via broker
- WHEN the broker is wired across two host instances AND a subscriber connects to host A
- THEN a publish on host B reaches the subscriber on host A
**Verified by:** `tests/integration/ws-broadcast.integration.test.ts::"single-host fan-out delivers to every subscriber"` AND `tests/integration/ws-broadcast.integration.test.ts::"multi-host fan-out via broker"`
---
### Requirement: SSE surface with Last-Event-Id resume
The plugin SHALL expose an SSE endpoint that supports `Last-Event-Id` resume per design doc §11.6.
#### Scenario: client reconnects with Last-Event-Id
- WHEN a client reconnects to `/example/sse` with `Last-Event-Id: 42`
- THEN the server replays events with id > 42
**Verified by:** `tests/integration/full-flow.integration.test.ts::"SSE replays events after Last-Event-Id"`
---
### Requirement: Owned saga with compensations + 2PC participation
The plugin SHALL own a 3-step saga AND participate in a host-managed 2PC transaction. Both happy and failure paths are exercised.
#### Scenario: saga happy path commits every step
- WHEN the saga runs to completion
- THEN every step's effect is observable AND no compensations run
#### Scenario: saga failure triggers compensations
- WHEN step 2 throws
- THEN step 1's compensation runs AND no step 3 effect is observable
#### Scenario: 2PC prepare→commit cycle
- WHEN the host issues prepare to the plugin AND every other participant votes yes
- THEN the host issues commit AND the plugin observes commit before responding
#### Scenario: 2PC abort cycle
- WHEN any participant votes no
- THEN the plugin observes abort AND no committed state is observable
**Verified by:** `tests/integration/saga-2pc.integration.test.ts::"saga happy path commits every step"` AND `...::"saga failure triggers compensations"` AND `...::"2PC prepare→commit cycle"` AND `...::"2PC abort cycle"`
---
### Requirement: Background jobs — cron, per-tenant cron, queued
The plugin SHALL register a global cron job, a per-tenant cron job, and a queued job. The integration test advances `FakeClock` to fire them.
#### Scenario: global cron fires on schedule
- WHEN `FakeClock` advances past the cron expression
- THEN `reconcile-invoices` runs exactly once for that tick
#### Scenario: per-tenant cron fans out
- WHEN three tenants exist AND `FakeClock` advances past the schedule
- THEN `send-monthly-statements` runs three times — once per tenant — each with a tenant-scoped context
#### Scenario: queued job retries on failure up to maxAttempts
- WHEN `send-receipt` throws on attempt 1
- THEN attempt 2 runs after backoff AND after `maxAttempts: 5` the job is parked in DLQ
**Verified by:** `tests/integration/full-flow.integration.test.ts::"cron fires on schedule"` AND `...::"per-tenant cron fans out"` AND `...::"queued job retries up to maxAttempts then DLQ"`
---
### Requirement: Event publish + subscribe
The plugin SHALL publish `example.invoice.created` AND subscribe to `users.user.deleted`.
#### Scenario: publishing increments the event bus
- WHEN an invoice is created
- THEN exactly one `example.invoice.created` event is observed on the bus
#### Scenario: subscriber handler runs on incoming event
- WHEN `users.user.deleted` is published by a sibling plugin
- THEN the example's handler runs exactly once for that delivery
**Verified by:** `tests/integration/full-flow.integration.test.ts::"publish increments event bus"` AND `tests/integration/full-flow.integration.test.ts::"subscriber handler runs on incoming event"`
---
### Requirement: Custom error codes mapped to HTTP statuses
The plugin SHALL declare `PLUGIN.EXAMPLE.*` codes in `error-codes.ts` AND map them in `errors.statusMap` in the manifest. Each code MUST be thrown by at least one production path AND surface with the manifested HTTP status.
#### Scenario: SUBSCRIPTION_INACTIVE returns 409
- WHEN a request triggers `PLUGIN.EXAMPLE.SUBSCRIPTION_INACTIVE`
- THEN response status is 409 AND error envelope includes that code
**Verified by:** `tests/integration/full-flow.integration.test.ts::"custom error code surfaces with manifest-mapped status"`
---
### Requirement: Custom metrics declared in manifest emit values
The plugin SHALL declare `example.invoices_finalised_total`, `example.stripe.call_duration_ms`, and `example.outstanding_invoice_count`. Each MUST emit values during the integration test, asserted against a real in-memory `MeterProvider`.
#### Scenario: counter emits on invoice finalise
- WHEN an invoice is finalised
- THEN `example.invoices_finalised_total` increments by 1
**Verified by:** `tests/integration/full-flow.integration.test.ts::"custom metrics emit values against MeterProvider"`
---
### Requirement: Multiple secrets read at runtime
The plugin SHALL declare ≥3 secrets in `secrets[]` AND read each at runtime via `ctx.secrets`.
#### Scenario: every declared secret reads successfully
- WHEN the plugin starts
- THEN each declared secret returns its configured test value AND no `SECRETS_*_NOT_ALLOWED` error is raised
**Verified by:** `tests/integration/full-flow.integration.test.ts::"every declared secret reads successfully"`
---
### Requirement: Schema migrations across two versions with auto-rollback
The plugin SHALL ship migrations at `schemaVersion: 1` and `schemaVersion: 2`. The integration test forces the v2 migration to fail and asserts the host auto-rolls back to v1.
#### Scenario: v1 → v2 upgrade succeeds on happy path
- WHEN a host with v1 reloads to v2 with the v2 migration green
- THEN the plugin reaches `started` with v2 schema active
#### Scenario: v2 migration failure auto-rolls back
- WHEN the v2 migration throws
- THEN the host transitions the plugin to `rolled_back` AND the prior known-good v1 module serves traffic AND a structured audit log entry is emitted
**Verified by:** `tests/integration/migration-rollback.integration.test.ts::"v1 → v2 upgrade succeeds on happy path"` AND `tests/integration/migration-rollback.integration.test.ts::"v2 migration failure auto-rolls back to v1"`
---
### Requirement: Intentional schema-violation route exercises response validation
The plugin SHALL expose `/_/test/fail-response?failResponse=1` returning a body that violates the declared response schema. The integration test runs it in `enforce` (expects 500) and `warn` (expects body + log + counter increment).
#### Scenario: enforce mode returns 500
- WHEN `host.responseValidation = "enforce"` AND the route is hit with `?failResponse=1`
- THEN response is 500
#### Scenario: warn mode returns body but logs and counts
- WHEN `host.responseValidation = "warn"` AND the route is hit with `?failResponse=1`
- THEN response carries the (invalid) body AND `api.response_validation.failures_total` increments by 1
**Verified by:** `tests/integration/response-validation.integration.test.ts::"enforce mode returns 500"` AND `tests/integration/response-validation.integration.test.ts::"warn mode returns body but increments counter"`

View File

@@ -0,0 +1,78 @@
# Tasks — `@suaveplan/api-plugin-example`
> Every checkbox MUST reflect work actually done in this change.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [x] 1.1 Read design doc (§13.5, §15)
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
- [x] 1.3 Draft `proposal.md`
- [x] 1.4 Draft `specs/api-plugin-example/spec.md`
- [x] 1.5 Every requirement carries `**Verified by:**`
## 2. Implementation
- [ ] 2.1 Scaffold `packages/api/api-plugin-example/`
- [ ] 2.2 `plugin.json` — every top-level key populated
- [ ] 2.3 `src/index.ts` — `definePlugin({ ... })` with full surface
- [ ] 2.4 `src/routes/` — public, private, validated, streaming, idempotent, rate-limited, RBAC-gated
- [ ] 2.5 `src/graphql/` — types, resolvers, subscription
- [ ] 2.6 `src/ws/` — connection lifecycle + subscription fan-out
- [ ] 2.7 `src/sse/` — endpoint with Last-Event-Id resume
- [ ] 2.8 `src/saga/` — owned saga (3-step) + compensations
- [ ] 2.9 `src/twopc/` — 2PC participant hooks
- [ ] 2.10 `src/jobs/` — scheduled (incl. per-tenant) + queued
- [ ] 2.11 `src/events/` — publish + subscribe handlers
- [ ] 2.12 `src/error-codes.ts` — `PLUGIN.EXAMPLE.*` registry
- [ ] 2.13 `src/instruments/` — counter/histogram/gauge instances
- [ ] 2.14 `migrations/postgres/0001-init.sql` + `0002-bump.sql` + sqlite + surrealdb mirrors
- [ ] 2.15 WASM-build variant for `sandboxed-wasm` mode
- [ ] 2.16 `src/test/fail-response/` — `?failResponse=1` schema-violation route
## 3. Tests
- [ ] 3.1 `tests/integration/full-flow.integration.test.ts` — parameterised over 4 isolation modes
- [ ] 3.2 `tests/integration/migration-rollback.integration.test.ts`
- [ ] 3.3 `tests/integration/response-validation.integration.test.ts`
- [ ] 3.4 `tests/integration/graphql-federation.integration.test.ts`
- [ ] 3.5 `tests/integration/ws-broadcast.integration.test.ts`
- [ ] 3.6 `tests/integration/saga-2pc.integration.test.ts`
- [ ] 3.7 `vitest.config.ts` (unit) + `vitest.integration.config.ts` (integration via `testing-integration-postgres`)
- [ ] 3.8 Every `it(...)` carries `{ timeout: 5000 }` for unit, `{ timeout: 30000 }` for integration
## 4. Documentation
- [ ] 4.1 Sibling `.md` per implementation `.ts`
- [ ] 4.2 `README.md` — full capability map + run instructions
- [ ] 4.3 Update `CLAUDE.md` package listing
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check`
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin-example`
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-plugin-example` — 100/100/100/100
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-plugin-example`
- [ ] 5.5 `INTEGRATION=1 bunx turbo test --filter=@suaveplan/api-plugin-example` — all 6 integration suites green across 4 isolation modes
## 6. Pre-archive 7-row checklist
- [ ] 6.1 Generics threaded; `definePlugin` typed against `requiredCapabilities`
- [ ] 6.2 Zod schemas wired on every route, every event, every job payload
- [ ] 6.3 Every `PLUGIN.EXAMPLE.*` code thrown by a production path AND mapped in `errors.statusMap`
- [ ] 6.4 Strict-mode behaviours: response validation `enforce` returns 500 on violation; `warn` logs + counter
- [ ] 6.5 OTel — manifest-declared counters/histograms/gauges asserted with real `MeterProvider`
- [ ] 6.6 Lifecycle dedup — install / start / stop / uninstall idempotent on re-entry
- [ ] 6.7 No retroactive tick
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/api-plugin-example/spec.md`
- [ ] 7.2 Move folder to archive
- [ ] 7.3 Tick `8.17` in umbrella tasks AND Stage 8 ✓ in `## 2. Stage rollup`
- [ ] 7.4 Refresh `MANIFEST.md`