Initial Commit
This commit is contained in:
83
openspec/archive/2026-05-25-api-ws/proposal.md
Normal file
83
openspec/archive/2026-05-25-api-ws/proposal.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `@suaveplan/api-ws` — abstract WebSocket Connection, Registry, Bus, Broker contract
|
||||
|
||||
**Package**: `@suaveplan/api-ws`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Persistent connections are first-class in the API foundation (design §11). Plugins declare WebSocket endpoints in their manifest; the host runs the engine, manages connection lifecycle, fans out subscriptions. Concrete engines (`api-ws-bun`) and multi-host brokers (`api-ws-broker-redis`, `api-ws-broker-in-memory`) plug behind a stable contract. This package owns that contract: `Connection`, `ConnectionRegistry`, `SubscriptionBus`, `WsBroker`, plus the cross-tenant gate that requires `ws.broadcast-cross-tenant` capability.
|
||||
|
||||
Per design §11.2: "`conn.subscribe(topic, { tenant?, filter? })` registers the connection with the host's subscription bus." Per §11.3: brokers make `bus.broadcast` visible across host replicas without migrating connections. Both shapes live here.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package. Contract types + Zod schemas for the wire envelope + the cross-tenant capability gate + the conformance harness.
|
||||
|
||||
- ADDED: `./connection` — abstract `WsConnection`: `principal`, `tenantId`, `subscribe(topic, opts)`, `unsubscribe(topic)`, `send(message)`, `close(code, reason)`, `ack(id)`, `id`, `lastEventId`, `state` (`open` | `closing` | `closed`).
|
||||
- ADDED: `./connection-registry` — abstract `ConnectionRegistry`: `add(conn)`, `remove(conn)`, `lookupByTenant(tenant)`, `lookupByTopic(topic, tenant?)`, `countByTenant(tenant)`. Backed by a multi-index.
|
||||
- ADDED: `./subscription-bus` — abstract `SubscriptionBus`: `subscribe(conn, topic, { tenant?, filter? })`, `unsubscribe(conn, topic)`, `broadcast(topic, { tenant, payload, filter? })`, `asyncIterator(topic, opts)`.
|
||||
- ADDED: `./broker` — abstract `WsBroker` for multi-host fan-out: `publish(topic, envelope)`, `subscribe(topic, handler)`, `dispose()`. Connections are NOT migrated across hosts — the broker only makes broadcasts visible.
|
||||
- ADDED: `./envelope` — Zod schema for the wire `WsEnvelope` (`{ id, type, topic?, payload? }`); type-narrowing helpers.
|
||||
- ADDED: `./backpressure` — `BackpressurePolicy`: bounded send queue (`maxQueuedMessages`, default 256); eviction emits `BACKPRESSURE_EVICTED` close.
|
||||
- ADDED: `./heartbeat` — `HeartbeatPolicy`: `heartbeatMs` (default 30000), two missed pongs → close `ABNORMAL`.
|
||||
- ADDED: `./reconnect-token` — `ReconnectToken` HMAC envelope (`{ tenant, principalId, lastEventId, exp }`); 5-min TTL constant.
|
||||
- ADDED: `./capability-id` — adds `"ws"` AND `"ws.broadcast-cross-tenant"` to the `CapabilityId` union via type-level augmentation.
|
||||
- ADDED: `./error-codes` — frozen `WS_CODES`: `HANDSHAKE_FAILED`, `PRINCIPAL_EXPIRED`, `BACKPRESSURE_EVICTED`, `HEARTBEAT_FAILED`, `CONNECTION_CAP_EXCEEDED`, `CROSS_TENANT_BROADCAST_DENIED`, `RECONNECT_TOKEN_INVALID`, `BROKER_UNAVAILABLE`.
|
||||
- ADDED: `./testing` — `runWsEngineConformance(factory)` reusable harness.
|
||||
|
||||
## CapabilityId additions
|
||||
|
||||
This package contributes `"ws"` and `"ws.broadcast-cross-tenant"` to the `CapabilityId` union. The cross-tenant id is a separate capability so the manifest declares it explicitly and the host audit-logs every use (design §11.2 "audit-logged on every use").
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Concrete engines (`api-ws-bun`) — Stage 7.
|
||||
- Concrete brokers (`api-ws-broker-redis`, `api-ws-broker-in-memory`) — Stages 5 and 7.
|
||||
- The `graphql-ws` protocol wiring (subscriptions over WebSocket) — that lives in `api-graphql` engine adapters, which read this package's `SubscriptionBus`.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| `WsConnection` shape + handshake flow | §11.1 |
|
||||
| `SubscriptionBus.broadcast` + filter predicate | §11.2 |
|
||||
| Multi-host fan-out via broker | §11.3 |
|
||||
| Backpressure (`maxQueuedMessages: 256`) | §11.4 |
|
||||
| Heartbeat (`heartbeatMs: 30000`, two missed pongs) | §11.4 |
|
||||
| Per-tenant caps (`maxConnectionsPerTenant`) | §11.4 |
|
||||
| Reconnect tokens (HMAC, 5-min TTL) | §11.5 |
|
||||
| Cross-tenant gate (`ws.broadcast-cross-tenant`) | §11.2 |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `runWsEngineConformance` harness | Stage-5 deliverable (§13.4). |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/connection/connection.spec-conformance.test.ts` — `expectTypeOf<WsConnection>()` declares every documented method.
|
||||
- `src/connection-registry/connection-registry.test.ts` — add/remove/lookup; multi-index correctness; count-by-tenant.
|
||||
- `src/subscription-bus/subscription-bus.test.ts` — subscribe/unsubscribe; broadcast fans out to matching `(topic, tenant)`; filter predicate applied per-connection.
|
||||
- `src/subscription-bus/cross-tenant.test.ts` — broadcast WITHOUT `ws.broadcast-cross-tenant` is rejected with `WS.CROSS_TENANT_BROADCAST_DENIED` when `tenant` arg is omitted or differs from emitter's tenant.
|
||||
- `src/broker/broker.spec-conformance.test.ts` — `WsBroker` interface declared.
|
||||
- `src/envelope/envelope.test.ts` — Zod accept/reject suite over the wire envelope.
|
||||
- `src/backpressure/backpressure.test.ts` — queue saturation triggers `BACKPRESSURE_EVICTED`.
|
||||
- `src/heartbeat/heartbeat.test.ts` — two missed pongs → close `ABNORMAL`.
|
||||
- `src/reconnect-token/reconnect-token.test.ts` — HMAC verify, TTL boundary.
|
||||
- `src/capability-id/capability-id.spec-conformance.test.ts` — both ids in `CapabilityId`.
|
||||
- `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`. No transport deps.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
- (none) — [x] no action needed (reason: net-new; engines + brokers land Stage 5/7)
|
||||
Reference in New Issue
Block a user