@zero-server/webrtc 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/webrtc/bot.js CHANGED
@@ -1,18 +1,10 @@
1
1
  /**
2
2
  * @module webrtc/bot
3
3
  * @description Server-side WebRTC peer ("bot") built on the `wrtc`
4
- * peerDependency.
5
- *
6
- * `spawnBotPeer({hub, room, ...})` attaches an in-process peer to a
7
- * {@link SignalingHub}, joins a room, and drives a real
8
- * {@link RTCPeerConnection} per remote peer (using the Node.js `wrtc`
9
- * binding). It implements the standard JSEP perfect-negotiation
10
- * pattern and is designed for headless workloads such as recording,
11
- * transcription, AI agents, and SFU verification harnesses.
12
- *
13
- * The `wrtc` peerDependency is loaded lazily; in production any of
14
- * `wrtc` or `@roamhq/wrtc` is acceptable. Tests inject a fake via
15
- * `opts.wrtc`.
4
+ * peerDependency. `spawnBotPeer({ hub, room, ... })` attaches an
5
+ * in-process peer that joins a room and drives a real
6
+ * `RTCPeerConnection` per remote peer. Bidirectional use for
7
+ * recording, transcription, AI participants, or SFU verification.
16
8
  */
17
9
  'use strict';
18
10
 
@@ -43,6 +35,58 @@ const { WebRTCError } = require('../errors');
43
35
  * ready: Promise<{ peerId: string }>,
44
36
  * close: () => void,
45
37
  * }}
38
+ *
39
+ * @example | Recording bot: dump every inbound audio track to a file
40
+ * const { spawnBotPeer } = require('@zero-server/webrtc');
41
+ * const { RTCAudioSink } = require('@roamhq/wrtc').nonstandard;
42
+ * const fs = require('node:fs');
43
+ *
44
+ * const bot = spawnBotPeer({
45
+ * hub,
46
+ * room: 'standup',
47
+ * user: { id: 'recorder' },
48
+ * iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
49
+ * onTrack: (track, _streams, fromPeerId) => {
50
+ * if (track.kind !== 'audio') return;
51
+ * const sink = new RTCAudioSink(track);
52
+ * const out = fs.createWriteStream(`./recordings/${fromPeerId}.pcm`);
53
+ * sink.ondata = ({ samples }) => out.write(Buffer.from(samples.buffer));
54
+ * track.onended = () => { sink.stop(); out.end(); };
55
+ * },
56
+ * });
57
+ *
58
+ * await bot.ready; // resolves with { peerId } once the hub welcomes us
59
+ * process.on('SIGTERM', () => bot.close());
60
+ *
61
+ * @example | AI participant: push synthesized audio back to the room
62
+ * const { spawnBotPeer } = require('@zero-server/webrtc');
63
+ * const { RTCAudioSource } = require('@roamhq/wrtc').nonstandard;
64
+ *
65
+ * const source = new RTCAudioSource();
66
+ * const track = source.createTrack();
67
+ *
68
+ * const bot = spawnBotPeer({
69
+ * hub, room: 'lounge', user: { id: 'ai-host' },
70
+ * onPeerJoin: (remoteId) => {
71
+ * const pc = bot.getPeerConnection(remoteId);
72
+ * if (pc) pc.addTrack(track); // perfect-negotiation will renegotiate
73
+ * },
74
+ * });
75
+ *
76
+ * // Feed synthesized PCM frames every 10ms.
77
+ * setInterval(() => source.onData({
78
+ * samples: ttsNextFrame(), // Int16Array(160)
79
+ * sampleRate: 16000,
80
+ * bitsPerSample: 16,
81
+ * channelCount: 1,
82
+ * numberOfFrames: 160,
83
+ * }), 10);
84
+ *
85
+ * @example | Inject a fake `wrtc` for unit tests
86
+ * const bot = spawnBotPeer({ hub, room: 'test', wrtc: fakeWrtc });
87
+ * await bot.ready;
88
+ * expect(hub.room('test').size).toBe(1);
89
+ * bot.close();
46
90
  */
47
91
  function spawnBotPeer(opts)
48
92
  {
package/lib/webrtc/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @module webrtc/cli
3
- * @description CLI subcommands for the `zh webrtc:*` namespace.
3
+ * @description CLI subcommands for the `zs webrtc:*` namespace.
4
4
  *
5
5
  * Pure-function entry point `runWebRTCCommand(subcmd, flags, deps)` so
6
6
  * the dispatch can be exercised in tests without spawning a child
@@ -10,11 +10,11 @@
10
10
  *
11
11
  * @example
12
12
  * // From the shell, via the top-level CLI:
13
- * // npx zh webrtc:stun --host stun.l.google.com --port 19302
14
- * // npx zh webrtc:turn-creds --secret $SECRET --user alice \
13
+ * // npx zs webrtc:stun --host stun.l.google.com --port 19302
14
+ * // npx zs webrtc:turn-creds --secret $SECRET --user alice \
15
15
  * // --servers turn:turn.example.com:3478
16
- * // npx zh webrtc:join-token --secret $JT_SECRET --room lobby --sub u1
17
- * // npx zh webrtc:verify-token --secret $JT_SECRET --token $TOKEN
16
+ * // npx zs webrtc:join-token --secret $JT_SECRET --room lobby --sub u1
17
+ * // npx zs webrtc:verify-token --secret $JT_SECRET --token $TOKEN
18
18
  *
19
19
  * // Programmatically:
20
20
  * const { runWebRTCCommand } = require('@zero-server/webrtc/cli');
@@ -168,7 +168,7 @@ function runVerifyToken(flags, { out })
168
168
  function helpText()
169
169
  {
170
170
  return [
171
- 'zh webrtc:* - WebRTC tooling',
171
+ 'zs webrtc:* - WebRTC tooling',
172
172
  '',
173
173
  'Subcommands:',
174
174
  ' webrtc:stun --host H [--port 3478] [--timeout 1000] [--retries 1]',
@@ -1,22 +1,10 @@
1
1
  /**
2
2
  * @module webrtc/cluster
3
- * @description Cluster adapter for the WebRTC signaling hub.
4
- *
5
- * `useCluster(hub, adapter)` glues a `SignalingHub` to any pub/sub
6
- * adapter that implements `{ publish(channel, message), subscribe(
7
- * channel, cb) -> unsubscribe }`. Once attached:
8
- *
9
- * - Every local `join` / `leave` is announced cluster-wide so other
10
- * nodes can resolve a `peer.id` to its owning node.
11
- * - Every `room.broadcast(...)` is mirrored to peers in the same room
12
- * on other nodes.
13
- * - Direct frames (`offer`, `answer`, `ice`) addressed to a peer that
14
- * lives on a different node are forwarded to that node's inbox.
15
- *
16
- * The adapter itself is intentionally tiny so production deployments
17
- * can wire it up to Redis, NATS, Kafka, or any in-house bus. A
18
- * `MemoryClusterAdapter` is provided for tests and single-process
19
- * simulations.
3
+ * @description Cluster adapter for the signaling hub. `useCluster(hub,
4
+ * adapter)` glues a `SignalingHub` to any `{ publish, subscribe }`
5
+ * pub/sub bus so joins, leaves, broadcasts, and direct frames flow
6
+ * across nodes. Ships with `MemoryClusterAdapter`; wire to Redis, NATS,
7
+ * Kafka, or any in-house bus in production.
20
8
  *
21
9
  * @section Cluster
22
10
  */
@@ -1,18 +1,10 @@
1
1
  /**
2
2
  * @module webrtc/e2ee
3
- * @description End-to-end-encrypted key relay channel for WebRTC.
4
- *
5
- * The hub never sees plaintext SFrame / Insertable-Streams keys.
6
- * Publishers wrap each rotation in a sealed envelope (X25519 ECDH +
7
- * HKDF-SHA-256 + AES-256-GCM) and broadcast it via the `e2ee-key`
8
- * wire message; subscribers in the same room receive the sealed
9
- * payload and decrypt locally with their private key.
10
- *
11
- * For deployments that use a different sealing primitive (NaCl
12
- * `crypto_box_seal`, libsignal, etc.) the {@link E2eeChannel} works
13
- * with any opaque `Buffer` - the {@link sealKey} / {@link openSealedKey}
14
- * helpers are provided as a zero-dependency default that satisfies the
15
- * HIPAA / FINRA "server is opaque" requirement.
3
+ * @description End-to-end-encrypted key relay for WebRTC. Publishers wrap
4
+ * SFrame/Insertable-Streams keys in sealed envelopes (X25519 + HKDF-SHA-256
5
+ * + AES-256-GCM) and broadcast via `e2ee-key`; the hub stays opaque.
6
+ * `E2eeChannel` accepts any opaque `Buffer`, so libsignal or NaCl-sealed
7
+ * payloads work out of the box.
16
8
  *
17
9
  * @section E2EE
18
10
  */
package/lib/webrtc/ice.js CHANGED
@@ -1,16 +1,9 @@
1
1
  /**
2
2
  * @module webrtc/ice
3
3
  * @description Zero-dependency ICE candidate parser, serializer, and address
4
- * classifiers (private / loopback / link-local / mDNS), plus a
5
- * `filterCandidates` helper used by `SignalingHub` to enforce
6
- * privacy-preserving policies on relayed offers/answers.
7
- *
8
- * Candidate grammar follows RFC 8839 §5.1 / RFC 5245 §15.1:
9
- *
10
- * candidate-attribute = "candidate" ":" foundation SP component-id
11
- * SP transport SP priority SP connection-address
12
- * SP port SP cand-type [SP rel-addr] [SP rel-port]
13
- * *(SP extension-att-name SP extension-att-value)
4
+ * classifiers (private / loopback / link-local / mDNS) per RFC 8839,
5
+ * plus a `filterCandidates` helper used by `SignalingHub` to enforce
6
+ * privacy-preserving policies on relayed offers/answers.
14
7
  *
15
8
  * @see https://datatracker.ietf.org/doc/html/rfc8839
16
9
  * @see https://datatracker.ietf.org/doc/html/rfc5245
@@ -1,14 +1,91 @@
1
1
  /**
2
2
  * @module @zero-server/webrtc
3
- * @description First-class WebRTC support for Zero Server.
3
+ * @description First-class, batteries-included WebRTC support for Zero Server.
4
4
  *
5
- * Signaling hub, room / peer orchestration, RFC 8489 STUN client,
6
- * RFC 7635 TURN credential issuance, optional embedded TURN server,
7
- * SFrame E2EE key relay, and a pluggable SFU adapter interface.
5
+ * `@zero-server/webrtc` is a complete signaling + NAT-traversal toolkit you
6
+ * can drop into any Zero Server app. Everything is pure JavaScript with
7
+ * zero hard dependencies bring your own media engine (`wrtc`, browsers,
8
+ * `mediasoup`, LiveKit, ...) and the library handles the rest.
8
9
  *
9
- * Implementation is landing PR-by-PR per `.myshit/WEBRTC-ROADMAP.md`.
10
- * Real exports already live in this barrel; the rest throw
11
- * `WEBRTC_NOT_IMPLEMENTED` so accidental production use fails loud.
10
+ * Surface area:
11
+ *
12
+ * - **Signaling** {@link SignalingHub}, {@link Room}, {@link Peer}. A
13
+ * transport-agnostic WS broker that owns the room registry, validates
14
+ * JSEP traffic (offer / answer / ICE / `e2ee-key`), enforces per-peer
15
+ * and per-IP rate limits, supports policy gates (`require()`,
16
+ * `canPublish()`), and emits lifecycle events.
17
+ * - **JSEP parsing** — {@link parseSdp}, {@link stringifySdp},
18
+ * {@link parseCandidate}, {@link stringifyCandidate},
19
+ * {@link filterCandidates}. RFC 8866 / 8839 compliant pure-JS codecs.
20
+ * - **STUN client** — {@link stunBinding} (RFC 5389 / 8489) for public-IP
21
+ * discovery, plus low-level attribute encoders.
22
+ * - **TURN** — {@link issueTurnCredentials} (RFC 7635 ephemeral creds) and
23
+ * a full embedded {@link TurnServer} that speaks STUN/TURN over UDP.
24
+ * - **Join tokens** — {@link signJoinToken} / {@link verifyJoinToken}: HS256
25
+ * JWTs scoped to `room:<name>` with publish / subscribe claims.
26
+ * - **End-to-end encryption** — {@link E2eeChannel}, {@link attachE2ee},
27
+ * {@link generateE2eeKeyPair}, {@link sealKey}, {@link openSealedKey}.
28
+ * SFrame-compatible key-relay primitives; the hub never sees media.
29
+ * - **SFU adapters** — {@link SfuAdapter} interface plus first-party
30
+ * {@link MemorySfuAdapter} (tests), {@link MediasoupSfuAdapter},
31
+ * {@link LiveKitSfuAdapter}. Pluggable via {@link loadSfuAdapter}.
32
+ * - **Cluster** — {@link useCluster}, {@link ClusterCoordinator}, and the
33
+ * {@link MemoryClusterAdapter} so multiple hub instances can share a
34
+ * room registry behind a load balancer.
35
+ * - **Server-side peer** — {@link spawnBotPeer} for headless recorders,
36
+ * transcribers, or AI participants using `node-wrtc`.
37
+ * - **Observability** — {@link bindObservability} wires Prometheus
38
+ * counters / histograms and structured logs into a hub.
39
+ * - **CLI** — {@link runWebRTCCommand} powers `zs webrtc:*` for STUN
40
+ * probes, TURN credential issuance, and join-token sign / verify.
41
+ *
42
+ * @example | Bind a signaling hub to a Zero Server `app.ws()` route
43
+ * const { createApp } = require('@zero-server/sdk');
44
+ * const { SignalingHub, bindObservability } = require('@zero-server/webrtc');
45
+ *
46
+ * const app = createApp();
47
+ * const hub = new SignalingHub({
48
+ * joinTokenSecret: process.env.WEBRTC_JWT_SECRET,
49
+ * ipAttachRate: 60, // max 60 attaches / IP / min
50
+ * maxSdpSize: 64 * 1024,
51
+ * });
52
+ *
53
+ * bindObservability(hub, { app }); // exposes /metrics for Prometheus
54
+ *
55
+ * app.ws('/rtc', (ws, req) =>
56
+ * {
57
+ * const peer = hub.attach(ws, {
58
+ * user: req.user,
59
+ * ip: req.ip,
60
+ * origin: req.headers.origin,
61
+ * });
62
+ * ws.on('close', () => peer.close());
63
+ * });
64
+ *
65
+ * app.listen(3000);
66
+ *
67
+ * @example | Issue a join token and a TURN credential to a browser
68
+ * const {
69
+ * signJoinToken, issueTurnCredentials,
70
+ * } = require('@zero-server/webrtc');
71
+ *
72
+ * app.get('/rtc/session/:room', (req, res) =>
73
+ * {
74
+ * const token = signJoinToken({
75
+ * secret: process.env.WEBRTC_JWT_SECRET,
76
+ * room: req.params.room,
77
+ * userId: req.user.id,
78
+ * publish: req.user.isHost,
79
+ * ttlSec: 60 * 30,
80
+ * });
81
+ * const turn = issueTurnCredentials({
82
+ * secret: process.env.TURN_SHARED_SECRET,
83
+ * userId: req.user.id,
84
+ * ttlSec: 60 * 60,
85
+ * uris: ['turn:turn.example.com:3478?transport=udp'],
86
+ * });
87
+ * res.json({ token, iceServers: turn.iceServers });
88
+ * });
12
89
  */
13
90
 
14
91
  'use strict';
@@ -49,30 +126,31 @@ const { spawnBotPeer } = require('./bot');
49
126
 
50
127
  /**
51
128
  * @private
52
- * Sentinel for surfaces that have not yet been implemented. Each PR in the
53
- * roadmap replaces one of these with a real function/class export.
129
+ * Sentinel for surfaces that are intentionally not exported yet. Reserved
130
+ * for future top-level shortcuts; current consumers should construct a
131
+ * `SignalingHub` directly and use `bindObservability(hub, { app })`.
54
132
  */
55
133
  const notImplemented = (name) =>
56
134
  {
57
135
  throw new WebRTCError(
58
- `${name} is not implemented yet - see .myshit/WEBRTC-ROADMAP.md for the implementation plan.`,
136
+ `${name} is not implemented yet - construct \`new SignalingHub(opts)\` directly and wire it via \`app.ws()\`.`,
59
137
  { code: 'WEBRTC_NOT_IMPLEMENTED' },
60
138
  );
61
139
  };
62
140
 
63
141
  module.exports = {
64
- // Signaling - landing in a later PR
142
+ // Signaling
65
143
  createWebRTC: () => notImplemented('createWebRTC'),
66
144
  SignalingHub,
67
145
  Room,
68
146
  Peer,
69
147
  PEER_STATE,
70
148
 
71
- // SDP - PR 1
149
+ // SDP / JSEP
72
150
  parseSdp,
73
151
  stringifySdp,
74
152
 
75
- // ICE - PR 1
153
+ // ICE candidate utilities
76
154
  parseCandidate,
77
155
  stringifyCandidate,
78
156
  filterCandidates,
@@ -83,7 +161,7 @@ module.exports = {
83
161
  CANDIDATE_TYPES,
84
162
  TCP_TYPES,
85
163
 
86
- // NAT traversal - later PRs
164
+ // STUN client + low-level codecs
87
165
  stunBinding,
88
166
  encodeBindingRequest,
89
167
  decodeMessage,
@@ -93,10 +171,12 @@ module.exports = {
93
171
  STUN_METHOD,
94
172
  STUN_CLASS,
95
173
  STUN_ATTR,
174
+
175
+ // TURN
96
176
  issueTurnCredentials,
97
177
  TurnServer,
98
178
 
99
- // SFU + tokens - later PRs
179
+ // SFU adapters + tokens
100
180
  SfuAdapter,
101
181
  MemorySfuAdapter,
102
182
  MediasoupSfuAdapter,
@@ -105,20 +185,20 @@ module.exports = {
105
185
  signJoinToken,
106
186
  verifyJoinToken,
107
187
 
108
- // Server-side WebRTC peer (wrtc bot)
188
+ // Server-side WebRTC peer (node-wrtc bot)
109
189
  spawnBotPeer,
110
190
 
111
- // Observability - PR 6
191
+ // Observability
112
192
  bindObservability,
113
193
 
114
- // E2EE key relay - PR 7
194
+ // SFrame E2EE key relay
115
195
  E2eeChannel,
116
196
  attachE2ee,
117
197
  generateE2eeKeyPair,
118
198
  sealKey,
119
199
  openSealedKey,
120
200
 
121
- // Cluster - PR 8
201
+ // Cluster coordination
122
202
  useCluster,
123
203
  ClusterCoordinator,
124
204
  MemoryClusterAdapter,
@@ -1,11 +1,28 @@
1
1
  /**
2
2
  * @module webrtc/joinToken
3
- * @description Signed, short-TTL join tokens that authenticate a peer's
4
- * right to join a specific room. JWT-shaped (HS256 by default)
5
- * and audience-scoped to `room:<name>` so a token leaked from
6
- * one channel cannot be replayed against another.
3
+ * @description Signed, short-TTL JWT join tokens scoped to `room:<name>`.
4
+ * HS256 by default, RS256 supported. Verification is constant-time and
5
+ * surfaces every failure mode as `WebRTCError({ code: 'INVALID_TOKEN' })`.
6
+ * The hub auto-enforces tokens when constructed with `joinTokenSecret`.
7
7
  *
8
- * Reuses the canonical sign/verify primitives from `lib/auth/jwt.js`.
8
+ * @example | Browser receives a token from a regular HTTP route
9
+ * // server.js
10
+ * const { signJoinToken } = require('@zero-server/webrtc');
11
+ * app.get('/rtc/token/:room', (req, res) => {
12
+ * const token = signJoinToken({
13
+ * secret: process.env.WEBRTC_JWT_SECRET,
14
+ * user: req.user, // { id, name, role }
15
+ * room: req.params.room,
16
+ * ttl: 300, // 5 minute window
17
+ * claims: { publish: req.user.isHost === true },
18
+ * });
19
+ * res.json({ wsUrl: '/rtc', token });
20
+ * });
21
+ *
22
+ * // client.js (pseudo)
23
+ * const { wsUrl, token } = await fetch(`/rtc/token/${room}`).then(r => r.json());
24
+ * ws = new WebSocket(wsUrl);
25
+ * ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room, token }));
9
26
  */
10
27
 
11
28
  'use strict';
@@ -27,15 +44,34 @@ const { SignalingError, WebRTCError } = require('../errors');
27
44
  * @param {object} [opts.claims] - Additional claims merged into the payload.
28
45
  * @returns {string} Compact JWT.
29
46
  *
30
- * @example
47
+ * @example | Simple HS256 token with a user object
31
48
  * const token = signJoinToken({
32
49
  * secret: process.env.JOIN_SECRET,
33
- * user: req.user,
50
+ * user: req.user, // { id: 'u_42', name: 'Ada', role: 'host' }
34
51
  * room: 'boardroom',
35
52
  * ttl: 300,
36
53
  * });
37
54
  * res.json({ wsUrl: '/rtc', token });
38
55
  *
56
+ * @example | Embed publish / subscribe permissions as custom claims
57
+ * const token = signJoinToken({
58
+ * secret: process.env.JOIN_SECRET,
59
+ * user: { id: 'guest_' + crypto.randomUUID() },
60
+ * room: 'webinar-42',
61
+ * ttl: 60 * 30, // 30 minute viewer session
62
+ * claims: { publish: false, subscribe: true, tier: 'free' },
63
+ * });
64
+ *
65
+ * @example | RS256 with a per-tenant key id
66
+ * const token = signJoinToken({
67
+ * secret: fs.readFileSync('./keys/tenant-A.private.pem'),
68
+ * algorithm: 'RS256',
69
+ * user: req.user,
70
+ * room: 'tenantA:lobby',
71
+ * ttl: 300,
72
+ * claims: { kid: 'tenantA-2025-01' },
73
+ * });
74
+ *
39
75
  * @section Signaling
40
76
  */
41
77
  function signJoinToken(opts = {})
@@ -80,6 +116,25 @@ function signJoinToken(opts = {})
80
116
  * @param {number} [opts.clockTolerance=0]
81
117
  * @returns {object} Verified payload.
82
118
  *
119
+ * @example | Manually verify a token (most apps never need this — the hub does it)
120
+ * try {
121
+ * const payload = verifyJoinToken(req.body.token, {
122
+ * secret: process.env.WEBRTC_JWT_SECRET,
123
+ * room: 'boardroom',
124
+ * });
125
+ * console.log('peer is', payload.user.id, 'publish =', payload.publish);
126
+ * } catch (err) {
127
+ * // err.code === 'INVALID_TOKEN'
128
+ * res.status(401).json({ error: err.message });
129
+ * }
130
+ *
131
+ * @example | Allow a 30-second clock skew between issuer and verifier
132
+ * const payload = verifyJoinToken(token, {
133
+ * secret: sharedSecret,
134
+ * room: 'lobby',
135
+ * clockTolerance: 30,
136
+ * });
137
+ *
83
138
  * @section Signaling
84
139
  */
85
140
  function verifyJoinToken(token, opts = {})
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * @module webrtc/observe
3
- * @description Optional metrics + tracing wiring for a {@link SignalingHub}.
4
- * Pass a `MetricsRegistry`, a `Tracer`, or both; the binder
5
- * subscribes to the hub's lifecycle events and exports the six
6
- * standard `zs_webrtc_*` Prometheus series plus per-operation
7
- * OpenTelemetry-compatible spans.
3
+ * @description Optional metrics + tracing wiring for a `SignalingHub`.
4
+ * `bindObservability(hub, { metrics, tracer })` exports the standard
5
+ * `zs_webrtc_*` Prometheus series plus OTel-compatible spans. Both
6
+ * adapters are duck-typed and opt-in.
8
7
  */
9
8
 
10
9
  'use strict';
@@ -215,9 +214,41 @@ function _bindTracing(hub, tracer)
215
214
  * @param {Tracer} [opts.tracer] - Tracer for span emission.
216
215
  * @returns {SignalingHub} The same hub, for chaining.
217
216
  *
218
- * @example | Plug into an existing app
217
+ * @example | Wire metrics + tracing from Zero Server's observe scope
218
+ * const { createApp } = require('@zero-server/sdk');
219
+ * const { Tracer } = require('@zero-server/sdk/observe');
220
+ * const { SignalingHub, bindObservability } = require('@zero-server/webrtc');
221
+ *
222
+ * const app = createApp();
223
+ * const hub = new SignalingHub({ joinTokenSecret: process.env.JWT });
224
+ *
225
+ * bindObservability(hub, {
226
+ * metrics: app.metrics, // exposes /metrics for Prometheus scraping
227
+ * tracer: new Tracer(), // OTel-shaped tracer
228
+ * });
229
+ *
230
+ * app.ws('/rtc', (ws, req) => hub.attach(ws, { user: req.user, ip: req.ip }));
231
+ *
232
+ * @example | Metrics only (no tracer)
219
233
  * const hub = new SignalingHub();
220
- * bindObservability(hub, { metrics: app.metrics() });
234
+ * bindObservability(hub, { metrics: app.metrics });
235
+ * // Scrape /metrics:
236
+ * // zs_webrtc_peers_active{room="lobby"} 3
237
+ * // zs_webrtc_rooms_active 1
238
+ * // zs_webrtc_signaling_messages_total{type="offer",direction="in",result="ok"} 17
239
+ * // zs_webrtc_offer_duration_ms_bucket{room="lobby",le="250"} 5
240
+ *
241
+ * @example | Custom prom-client registry adapter
242
+ * const client = require('prom-client');
243
+ * const reg = new client.Registry();
244
+ * const adapter = {
245
+ * counter: ({ name, help, labels }) => new client.Counter({ name, help, labelNames: labels, registers: [reg] }),
246
+ * gauge: ({ name, help, labels }) => new client.Gauge ({ name, help, labelNames: labels, registers: [reg] }),
247
+ * histogram: ({ name, help, labels, buckets }) =>
248
+ * new client.Histogram({ name, help, labelNames: labels, buckets, registers: [reg] }),
249
+ * };
250
+ * bindObservability(hub, { metrics: adapter });
251
+ * app.get('/metrics', async (_req, res) => res.type(reg.contentType).send(await reg.metrics()));
221
252
  */
222
253
  function bindObservability(hub, opts = {})
223
254
  {
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * @module webrtc/peer
3
- * @description Per-connection state machine for a WebRTC signaling peer.
3
+ * @description Per-connection state machine for a signaling peer. Wraps a
4
+ * `{ send, on, close }` transport (typically `app.ws()`'s
5
+ * `WebSocketConnection`) and exposes JSEP message types as events.
6
+ * Constructed by `hub.attach(transport, info)` — not directly.
4
7
  *
5
- * A `Peer` wraps a transport object (typically a `WebSocketConnection`
6
- * but any duck-typed `{ send, on, close }` works - see the unit tests
7
- * for a minimal in-memory transport) and exposes the JSEP message
8
- * types as ordinary events. The hub is responsible for routing.
8
+ * @example | Inspect every newly-attached peer
9
+ * hub.on('join', ({ peer, room }) => {
10
+ * console.log(
11
+ * 'peer', peer.id,
12
+ * 'user=', peer.user && peer.user.id,
13
+ * 'ip=', peer.ip,
14
+ * 'joined', room.name,
15
+ * 'roster size=', room.size,
16
+ * );
17
+ * });
9
18
  */
10
19
 
11
20
  'use strict';
@@ -25,14 +34,32 @@ const PEER_STATE = Object.freeze({
25
34
  /**
26
35
  * One signaling peer attached to a `SignalingHub` via some transport.
27
36
  *
37
+ * `Peer` is created by `hub.attach(transport, info)` and lives until the
38
+ * underlying transport closes (or the hub kicks the peer for protocol
39
+ * abuse). All event listeners are owned by the hub; application code
40
+ * subscribes to hub-level events such as `join`, `leave`, and the
41
+ * room-level `peer-joined` / `peer-left`.
42
+ *
28
43
  * @class
29
44
  * @section Peers
30
45
  *
31
- * @example
46
+ * @example | Push a per-peer welcome and tap mute events
32
47
  * hub.on('join', ({ peer, room }) => {
33
- * peer.send('welcome', { roomSize: room.size });
34
- * peer.on('mute', ev => audit('mute', peer.id, ev.kind));
48
+ * peer.send('welcome', { roomSize: room.size, you: peer.id });
49
+ * peer.on?.('mute', ev => audit('mute', peer.id, ev.kind));
35
50
  * });
51
+ *
52
+ * @example | Forcefully disconnect a peer that fails an auth refresh
53
+ * async function rotateAuth(peer) {
54
+ * const ok = await refreshSession(peer.user);
55
+ * if (!ok) peer.close(4401, 'auth-expired');
56
+ * }
57
+ *
58
+ * @example | Send a typed error frame instead of throwing inside a handler
59
+ * if (msg.bytes.length > MAX_BYTES) {
60
+ * peer.sendError('PAYLOAD_TOO_LARGE', 'frame > 64 KiB');
61
+ * return;
62
+ * }
36
63
  */
37
64
  class Peer
38
65
  {
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * @module webrtc/room
3
- * @description Room / channel abstraction for the WebRTC signaling hub.
4
- *
5
- * A `Room` holds a set of `Peer`s plus a list of `require()` policy gates
6
- * that decide whether a given peer may join. Membership is in-process;
7
- * the cluster adapter (PR 8) will fan room state out via pub/sub.
3
+ * @description Room / channel abstraction for the signaling hub. Holds a
4
+ * set of peers plus a fluent chain of policy gates (`require()`,
5
+ * `canPublish()`, `canSubscribe()`). Constructed lazily via
6
+ * `hub.room(name)`; pair with `useCluster()` for multi-node membership.
8
7
  */
9
8
 
10
9
  'use strict';
@@ -20,11 +19,25 @@ const { SignalingError } = require('../errors');
20
19
  * @class
21
20
  * @section Rooms
22
21
  *
23
- * @example
22
+ * @example | Open a public lobby that anyone with a token may join
24
23
  * hub.room('lobby').open();
24
+ *
25
+ * @example | Gate a room with role + per-publisher policies
25
26
  * hub.room('boardroom')
26
27
  * .require(peer => peer.user && peer.user.role === 'exec')
27
- * .canPublish(peer => peer.user.isHost);
28
+ * .canPublish(peer => peer.user.isHost)
29
+ * .canSubscribe(peer => peer.user.verified === true)
30
+ * .open();
31
+ *
32
+ * @example | Programmatic room metrics
33
+ * const r = hub.room('webinar-42');
34
+ * setInterval(() => log.info({ room: r.name, size: r.size }), 10_000);
35
+ *
36
+ * @example | Broadcast a system announcement from outside the message handler
37
+ * hub.room('lobby').broadcast('notice', { text: 'Server restart in 60s' });
38
+ *
39
+ * @example | Boot the room and disconnect everyone gracefully
40
+ * hub.room('lobby').close('shutdown');
28
41
  */
29
42
  class Room
30
43
  {
package/lib/webrtc/sdp.js CHANGED
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * @module webrtc/sdp
3
- * @description Zero-dependency RFC 8866 Session Description Protocol parser and
4
- * serializer, with WebRTC-specific attribute extraction (JSEP per
5
- * RFC 8829: ice-ufrag, ice-pwd, fingerprint, setup, mid, rtcp-mux,
6
- * direction, rtpmap, fmtp, rid, simulcast, ssrc, extmap, candidate).
7
- *
8
- * The parser deliberately does NOT validate semantics that belong
9
- * to a SignalingHub policy layer (codec allowlists, mDNS blocking,
10
- * etc.) - it only structures the document. Policy lives in
11
- * `lib/webrtc/signaling.js`.
3
+ * @description Zero-dependency RFC 8866 SDP parser and serializer with
4
+ * WebRTC-specific attribute extraction per RFC 8829 (ice-ufrag, ice-pwd,
5
+ * fingerprint, setup, mid, rtcp-mux, rtpmap, fmtp, ssrc, etc.). Pure
6
+ * structure policy lives in the signaling layer.
12
7
  *
13
8
  * @see https://datatracker.ietf.org/doc/html/rfc8866
14
9
  * @see https://datatracker.ietf.org/doc/html/rfc8829