5.1 KiB
Capability Spec Delta: api-plugin-example (dual-shape)
Package: @suaveplan/api-plugin-example
Status: PROPOSED
ADDED Requirements
Requirement: The package SHALL expose a ./wasm subpath surfacing a WASI-stdio-shaped plugin
@suaveplan/api-plugin-example/wasm SHALL export const plugin whose shape is { routes: readonly SerializedRoute[]; handleRequest(req: SerializedRequest): Promise<SerializedResponse>; start?(): void | Promise<void>; dispose?(): void | Promise<void> }. The wire types are imported from @suaveplan/api-types/sandbox-bridge.
Scenario: wasm subpath imports cleanly under browser-target bundling
- WHEN
Bun.build({ entrypoints: ["packages/api-stack/api/api-plugin-example/src/wasm/index.ts"], target: "browser", format: "esm" })runs - THEN the build succeeds AND the produced ESM bundle imports no
node:*specifier AND no Hono/OTel-SDK specifier
Verified by: tests/wasm/browser-bundle.test.ts::"wasm subpath bundles for browser target"
Requirement: The ./wasm subpath SHALL have a constrained dependency graph
A new lint script bun scripts/lint/check-wasm-subpath-deps.ts MUST walk every static import from src/wasm/index.ts transitively and reject any specifier matching the wasm-tier ban list: @suaveplan/api-host, hono, @hono/*, @opentelemetry/sdk-metrics, @opentelemetry/sdk-trace-base, @opentelemetry/sdk-node, any node:* specifier, and any package whose own dependency tree drags one of the above. The lint MUST be wired into bunx turbo lint so CI rejects accidental dependency creep.
Scenario: Adding import "hono" inside src/wasm/ fails the lint
- WHEN a contributor adds
import { Hono } from "hono"to any file reachable fromsrc/wasm/index.ts - THEN
bun scripts/lint/check-wasm-subpath-deps.tsexits non-zero with a message naminghonoand the importing file
Verified by: scripts/lint/check-wasm-subpath-deps.test.ts::"rejects banned specifier in wasm subpath"
Requirement: Trusted-tier and wasm-tier surfaces SHALL share a single domain core
The package SHALL contain a src/domain/ directory with pure (no IO, no Hono, no OTel SDK, no Request/Response) modules: invoice.types.ts, invoice-rules.ts, in-memory-store.ts. Both the trusted-tier routes (src/trusted/) AND the wasm-tier handlers (src/wasm/) MUST call the same rule functions from src/domain/invoice-rules.ts for any business decision (state transitions, validation, etc.).
Scenario: Trusted and wasm handlers produce identical bodies for the same input
- WHEN the same
POST /invoicespayload is dispatched through the trusted handler AND throughplugin.handleRequest(...) - THEN both produce the same HTTP status AND the same JSON response body
Verified by: tests/wasm/shape-conformance.test.ts::"trusted vs wasm parity for every shared route"
Requirement: The existing root export surface SHALL remain unchanged
The shipped @suaveplan/api-plugin-example exports (examplePluginModule, buildRoutes, EXAMPLE_CODES, error class) MUST keep their existing types and runtime semantics after the refactor. The internal git mv from src/{routes,plugin,install,events}/ to src/trusted/{routes,plugin,install,events}/ MUST be invisible to consumers — the root barrel re-exports the same names from the new locations.
Scenario: api-host's existing consumer of api-plugin-example still passes
- WHEN
bunx --bun turbo test:coverage --filter=@suaveplan/api-hostruns after the refactor - THEN every test that imports
@suaveplan/api-plugin-examplestill passes AND no test file inapi-host/requires source modification
Verified by: tests/trusted/root-export-shape.test.ts::"root exports preserve existing types and names"
Requirement: The wasm subpath SHALL omit endpoints that cannot run under the sandbox tier
The wasm-tier routes array MUST NOT include endpoints that require streaming responses, server-sent events, or any other transport pattern incompatible with the synchronous (SerializedRequest) => SerializedResponse contract. The trusted-tier streamLargeReport endpoint is the canonical example — it stays in the trusted-tier exports and is documented in src/wasm/index.md as a deliberate omission.
Scenario: Wasm routes contain no streaming endpoint
- WHEN the wasm
routesarray is inspected - THEN no entry's
pathmatches/stream/*or any documented streaming pattern
Verified by: src/wasm/routes.test.ts::"wasm routes omit streaming endpoints"
Requirement: Lifecycle hooks SHALL initialise and tear down the in-memory store deterministically
plugin.start() SHALL prime the module-singleton InMemoryStore with the same seed data the trusted-tier install hook uses. plugin.dispose() SHALL clear the store. Repeated calls to either MUST be idempotent (the second call MUST NOT throw).
Scenario: Dispose then start re-primes the store
- WHEN
plugin.dispose()runs, thenplugin.start()runs - THEN the wasm-tier
GET /invoicesroute returns the seeded list of invoices
Verified by: src/wasm/start.test.ts::"start re-primes after dispose"