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

8.6 KiB
Raw Permalink Blame History

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 z from "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 === 3 AND lookupByTenant("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.created for tenant T
  • THEN broadcast("orders.created", { tenant: T, payload }) reaches both

Scenario: filter excludes non-matching connections

  • WHEN filter returns false for 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 Caps does NOT include ws.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_EVICTED is 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" AND WS.HEARTBEAT_FAILED is 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>() AND expectTypeOf<"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"