8.6 KiB
Capability Spec Delta: api-ws (api-ws)
Package: @suaveplan/api-ws
Status: ACTIVE
Every requirement carries a
**Verified by:**line.
ADDED Requirements
Requirement: WsConnection contract
The package SHALL export WsConnection with: id (ULID), principal: Principal, tenantId: TenantId, lastEventId: string | null, state: "open" | "closing" | "closed", methods subscribe(topic, opts?), unsubscribe(topic), send(message), close(code, reason?), ack(id). The principal MUST be frozen at handshake for the connection's lifetime; token expiry mid-connection fires connection.principal_expired (the host emits this; the contract surfaces it).
Scenario: every method present
- WHEN
expectTypeOf<WsConnection>()is checked - THEN every documented method is declared with the documented signature
Verified by: src/connection/connection.spec-conformance.test.ts::"WsConnection declares every documented method"
Requirement: import z from "zod" everywhere
Every Zod import MUST use the default form.
Scenario: no named import of z
- WHEN the test greps
import zfrom"zod" - THEN zero matches
Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"
Requirement: ConnectionRegistry multi-index
The package SHALL export ConnectionRegistry with O(1) lookup by tenantId and by (topic, tenantId). add, remove, lookupByTenant(tenant), lookupByTopic(topic, tenant?), countByTenant(tenant). remove MUST be idempotent (removing an absent connection is a no-op).
Scenario: lookup by tenant returns matching set
- WHEN 3 connections are added for tenant T1 and 2 for tenant T2
- THEN
lookupByTenant("T1").length === 3ANDlookupByTenant("T2").length === 2
Scenario: remove is idempotent
- WHEN
remove(conn)is called twice - THEN the second call returns without throwing
Verified by: src/connection-registry/connection-registry.test.ts::"lookupByTenant returns matching set" AND src/connection-registry/connection-registry.test.ts::"remove is idempotent"
Requirement: SubscriptionBus broadcast with filter
The package SHALL export SubscriptionBus.broadcast(topic, { tenant, payload, filter? }) which delivers the payload to every connection registered against (topic, tenant) whose optional filter(payload) returns true. The payload MUST be encoded once and sent to each matching connection (no per-connection re-encoding).
Scenario: broadcast reaches matching connections
- WHEN two connections subscribe to
orders.createdfor tenant T - THEN
broadcast("orders.created", { tenant: T, payload })reaches both
Scenario: filter excludes non-matching connections
- WHEN filter returns
falsefor one of two matching connections - THEN only the connection where filter returns true receives the payload
Verified by: src/subscription-bus/subscription-bus.test.ts::"broadcast reaches all matching connections" AND src/subscription-bus/subscription-bus.test.ts::"filter predicate excludes non-matching connections"
Requirement: Cross-tenant broadcast requires explicit capability
SubscriptionBus.broadcast(topic, { tenant, payload }) where tenant differs from the caller's principal.tenantId (or tenant is the wildcard "*") MUST throw WS.CROSS_TENANT_BROADCAST_DENIED unless the caller's Caps includes "ws.broadcast-cross-tenant". Default is deny.
Scenario: same-tenant broadcast succeeds without capability
- WHEN caller's tenant is T AND broadcast is for tenant T
- THEN broadcast succeeds
Scenario: cross-tenant denied by default
- WHEN caller's tenant is T AND broadcast is for tenant T2 AND
Capsdoes NOT includews.broadcast-cross-tenant - THEN broadcast throws
WS.CROSS_TENANT_BROADCAST_DENIED
Verified by: src/subscription-bus/cross-tenant.test.ts::"same-tenant broadcast succeeds without capability" AND src/subscription-bus/cross-tenant.test.ts::"cross-tenant denied without explicit capability"
Requirement: WsBroker contract
The package SHALL export WsBroker for multi-host fan-out: publish(topic, envelope), subscribe(topic, handler), dispose(). The broker MUST NOT migrate connections — it only makes broadcasts visible across host replicas. dispose() MUST close every subscription cleanly.
Scenario: every method present
- WHEN
expectTypeOf<WsBroker>()is checked - THEN all three methods are declared
Verified by: src/broker/broker.spec-conformance.test.ts::"WsBroker declares publish subscribe dispose"
Requirement: Envelope Zod schema
The package SHALL export wsEnvelopeSchema: z.ZodSchema<WsEnvelope> describing the wire envelope: { id: string, type: string, topic?: string, payload?: unknown, ts?: string }. Unknown top-level keys MUST be rejected.
Scenario: minimal envelope parses
- WHEN
{ id: "x", type: "msg" }is parsed - THEN it succeeds
Scenario: extra key rejected
- WHEN
{ id: "x", type: "msg", extra: 1 }is parsed - THEN Zod reports an issue at
extra
Verified by: src/envelope/envelope.test.ts::"minimal envelope parses" AND src/envelope/envelope.test.ts::"unknown keys rejected"
Requirement: Backpressure policy
The package SHALL export BackpressurePolicy with default maxQueuedMessages = 256. When a connection's send queue exceeds the cap, the connection MUST receive a BACKPRESSURE_EVICTED close frame and be dropped from the registry.
Scenario: queue saturation triggers eviction
- WHEN 257 messages are queued for a connection with default policy
- THEN the eviction handler fires AND
WS.BACKPRESSURE_EVICTEDis the close code
Verified by: src/backpressure/backpressure.test.ts::"queue saturation triggers BACKPRESSURE_EVICTED"
Requirement: Heartbeat policy
The package SHALL export HeartbeatPolicy with default heartbeatMs = 30000. Two missed pongs MUST trigger onClose with code ABNORMAL.
Scenario: two missed pongs trigger close
- WHEN a fake clock advances past 2 × heartbeatMs without pong
- THEN the close handler fires with
code === "ABNORMAL"ANDWS.HEARTBEAT_FAILEDis emitted
Verified by: src/heartbeat/heartbeat.test.ts::"two missed pongs trigger ABNORMAL close"
Requirement: Reconnect-token shape
The package SHALL export ReconnectToken HMAC envelope carrying { tenant, principalId, lastEventId, exp } with default 5-minute TTL. The HMAC verifier MUST reject expired tokens (WS.RECONNECT_TOKEN_INVALID) and tokens with bad MAC.
Scenario: valid token verifies
- WHEN a token signed within TTL is verified
- THEN the verifier returns the parsed payload
Scenario: expired token rejected
- WHEN the fake clock advances past TTL
- THEN the verifier throws
WS.RECONNECT_TOKEN_INVALID
Verified by: src/reconnect-token/reconnect-token.test.ts::"valid token verifies" AND src/reconnect-token/reconnect-token.test.ts::"expired token rejected"
Requirement: CapabilityId augmentation contributes "ws" and "ws.broadcast-cross-tenant"
The package SHALL augment the CapabilityId union to include both ids.
Scenario: both ids assignable to CapabilityId
- WHEN
expectTypeOf<"ws">().toMatchTypeOf<CapabilityId>()ANDexpectTypeOf<"ws.broadcast-cross-tenant">().toMatchTypeOf<CapabilityId>()are checked - THEN both pass
Verified by: src/capability-id/capability-id.spec-conformance.test.ts::"ws and ws.broadcast-cross-tenant are valid CapabilityIds"
Requirement: Error codes registry frozen
The package SHALL export WS_CODES containing exactly: HANDSHAKE_FAILED, PRINCIPAL_EXPIRED, BACKPRESSURE_EVICTED, HEARTBEAT_FAILED, CONNECTION_CAP_EXCEEDED, CROSS_TENANT_BROADCAST_DENIED, RECONNECT_TOKEN_INVALID, BROKER_UNAVAILABLE.
Scenario: every code reachable
- WHEN the test enumerates
WS_CODES - THEN every code has a production throw site
Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every WS_CODES entry thrown by production path"
Requirement: Conformance harness for engines
The package SHALL export runWsEngineConformance({ factory, label }). Every concrete engine + broker pair MUST pass.
Scenario: harness covers handshake, subscribe, broadcast, backpressure, heartbeat, reconnect
- WHEN the harness is run against a stub engine
- THEN test categories exist for every documented behavior
Verified by: src/testing/conformance.test.ts::"harness covers handshake subscribe broadcast backpressure heartbeat reconnect"