# Dual-shape api-plugin-example (trusted tier + WASI-stdio wasm tier) **Package**: `@suaveplan/api-plugin-example` (refactor + additive subpath) **Status**: PROPOSED **Type**: in-package refactor + new `./wasm` subpath export **Blocks**: `wasm-compiler-javy` (keystone test needs this) ## Why The wasm-compiler-javy build agent (2026-05-27 attempt 2) stopped with an architectural blocker: `@suaveplan/api-plugin-example` is a trusted-tier api-host plugin (Hono routes, OTel SDK Node, streaming responses, closure-injected ledger / publisher, `Request`-typed handlers) and cannot be compiled to WASM via Javy. Three independent walls: 1. Plugin shape: exports `examplePluginModule: PluginModule` and `buildRoutes({ ledger, publisher })` — not `{ routes, handleRequest(SerializedRequest) }`. 2. Bundling: transitive deps drag `@hono/node-server`, OTel SDK Node bits, `node:*` specifiers; `Bun.build({ target: "browser" })` trips its own gate. 3. Runtime: Javy v8.1.1's QuickJS lacks usable `ReadableStream`/`WritableStream`, fetch, OTel SDK Node-isms. Three options were considered (full transcript in the agent's stop report); the orchestrator chose **dual-shape**: keep the existing trusted-tier reference plugin AND add a WASI-stdio surface on the SAME package via a new `./wasm` subpath. This preserves the single reference-plugin name (`api-plugin-example` continues to be THE reference) and gives both tiers concrete deploy-targets that share a domain core. The wasm subpath has zero runtime overlap with the trusted exports — its dependency tree is disjoint from Hono/OTel-SDK/streaming. ## Concrete consumer - `@suaveplan/wasm-compiler-javy` (next-up build) — keystone test compiles `@suaveplan/api-plugin-example/wasm` (NOT the root entry), produces a WASI-stdio `.wasm`, deploys it through `api-sandbox-wasm-host-bridge` with `abiVariant: "wasi-stdio"`. - `e2e/foundation-stack-full` Stage-8 closure smoke (gate 2.8) — exercises `api-plugin-example` under trusted + vm tiers AND `api-plugin-example/wasm` under wasm tier. All three isolation tiers covered by ONE reference name. ## Concrete host bridge `@suaveplan/api-sandbox-wasm-host-bridge` (already shipped) — receives the produced `.wasm`, no changes needed. ## End-to-end test - `packages/api-stack/api/api-plugin-example/tests/wasm/shape-conformance.test.ts` — boots the wasm subpath under a host-side WASI runner, exercises every route via `{kind:"request"}` envelopes, asserts each response matches the trusted-tier output for the equivalent endpoint (so the dual-shape isn't divergent semantics — it's two surfaces over one domain core). ## What changes ### Source layout (refactor) ```text packages/api-stack/api/api-plugin-example/src/ index.ts ← unchanged trusted-tier barrel (preserves existing exports) domain/ ← NEW: pure, dependency-free invoice domain invoice.types.ts invoice.types.md invoice-rules.ts ← business rules (state transitions, validation) invoice-rules.md invoice-rules.test.ts in-memory-store.ts ← simple Map-backed store; no IO; deterministic in-memory-store.md in-memory-store.test.ts trusted/ ← existing routes/, plugin/, install/, events/ moved here index.ts ← internal barrel routes.ts ← references domain/ for business rules; Hono-tier handlers plugin.ts ← api-host PluginModule with hooks install.ts events.ts wasm/ ← NEW: WASI-stdio surface index.ts ← export const plugin = { routes, handleRequest, start, dispose } routes.ts ← export const routes: readonly SerializedRoute[] handle-request.ts ← (req: SerializedRequest) => SerializedResponse handle-request.md handle-request.test.ts start.ts ← () => void; primes in-memory store dispose.ts ← () => void; clears store wasm-state.ts ← module-singleton InMemoryStore instance ``` The existing `error-codes/`, `instruments/`, `schemas/`, `events/` stay at the package root because they're shared between trusted and wasm tiers (only the trusted-tier instruments register against the OTel SDK; the wasm tier defines them as no-op shims to keep the API surface uniform). ### Public surface (additive — existing entry unchanged) ```jsonc // package.json exports (only the additions shown): { "./wasm": { "import": { "types": "./dist/wasm/index.d.ts", "default": "./dist/wasm/index.js" } }, "./domain": { "import": { "types": "./dist/domain/index.d.ts", "default": "./dist/domain/index.js" } } } ``` ```ts import { plugin } from "@suaveplan/api-plugin-example/wasm"; plugin.routes; // readonly SerializedRoute[] await plugin.handleRequest(req); // SerializedRequest -> SerializedResponse plugin.start?.(); // optional plugin.dispose?.(); // optional ``` ### Dependency constraint on `wasm/` The `wasm/` directory MUST NOT directly OR transitively import: - `@suaveplan/api-host`, `hono`, `@hono/*` - `@opentelemetry/sdk-metrics`, `@opentelemetry/sdk-trace-base`, `@opentelemetry/sdk-node` - Any `node:*` specifier - Any package whose dependency tree drags the above (transitive ban) A new lint script `bun scripts/lint/check-wasm-subpath-deps.ts` (added as part of this change) enforces this by walking the imports from `wasm/index.ts` and rejecting any forbidden specifier. The script is wired into `bunx turbo lint`. The `wasm/` directory MAY use `@opentelemetry/api` (types-only, browser-safe) for OTel context propagation if needed. It MAY use `zod` for envelope validation. It MAY use `@suaveplan/api-types/sandbox-bridge` for the wire-protocol types. ### Domain parity The `tests/wasm/shape-conformance.test.ts` integration test asserts that for every route shared between trusted and wasm surfaces, calling the trusted handler with `Request("/foo", { method: "POST", body: JSON.stringify(x) })` and calling the wasm handler with `{ method: "POST", url: "/foo", headers: [...], bodyBase64: base64(JSON.stringify(x)), ... }` produces the SAME response status and the SAME response body. The shared `domain/` module is what guarantees this; the test verifies it stays true. ## Out of scope - Streaming routes. The trusted-tier example has a `streamLargeReport` endpoint that returns a chunked `Response`. The wasm tier OMITS this endpoint — it's documented in `wasm/index.md` as "streaming endpoints don't apply under the wasm tier; use a polling endpoint or pagination instead". The shape-conformance test excludes it. - Plugin lifecycle hooks beyond `start`/`dispose`. The trusted tier's `health` / `uninstall` hooks have no wasm-tier analogue. - Changes to the existing trusted-tier exports. Anything imported from `@suaveplan/api-plugin-example` (root) keeps its current shape and dependency graph.