Files
2026-05-27 23:29:53 +01:00

12 KiB

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"