@zero-server/sdk 0.9.7 → 0.9.8

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.
@@ -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
@@ -1,12 +1,28 @@
1
1
  /**
2
2
  * @module webrtc/sfu
3
- * @description SFU adapter base interface and discovery loader.
3
+ * @description Pluggable Selective Forwarding Unit (SFU) adapter interface.
4
+ * The signaling hub stays media-agnostic; the adapter owns routers,
5
+ * transports, producers, and consumers. Ships with `MemorySfuAdapter`,
6
+ * `MediasoupSfuAdapter`, and `LiveKitSfuAdapter`. Subclass `SfuAdapter`
7
+ * for custom backends.
4
8
  *
5
- * `SfuAdapter` defines the contract every backend (memory / mediasoup /
6
- * livekit / custom) must implement. `loadSfuAdapter()` resolves either
7
- * a pre-constructed instance, a known name ('memory', 'mediasoup',
8
- * 'livekit'), or a duck-typed object into a concrete adapter, throwing
9
- * `WEBRTC_SFU_NOT_INSTALLED` when a native peerDep is missing.
9
+ * @example | Select an adapter at boot via env
10
+ * const { loadSfuAdapter } = require('@zero-server/webrtc');
11
+ * const sfu = loadSfuAdapter(process.env.SFU_BACKEND || 'memory', {
12
+ * // adapter-specific options forwarded verbatim
13
+ * workerSettings: { logLevel: 'warn' },
14
+ * });
15
+ *
16
+ * sfu.onEvent((event, payload) => log.debug({ event, payload }, 'sfu'));
17
+ *
18
+ * @example | Wire an SFU into the signaling hub per room
19
+ * const router = await sfu.createRouter({ room: 'lobby' });
20
+ *
21
+ * hub.on('join', async ({ peer, room }) => {
22
+ * if (room.name !== 'lobby') return;
23
+ * const transport = await sfu.createTransport(router, peer);
24
+ * peer.send('sfu-transport', { iceParameters: transport.iceParameters });
25
+ * });
10
26
  */
11
27
  'use strict';
12
28
 
@@ -20,11 +36,30 @@ const { WebRTCError } = require('../../errors');
20
36
  * The interface is intentionally tiny so a backend can be written in a
21
37
  * single file:
22
38
  *
23
- * class MyAdapter extends SfuAdapter {
24
- * async createRouter(opts) { ... }
25
- * async createTransport(router, peer) { ... }
26
- * ...
27
- * }
39
+ * @example | Skeleton adapter
40
+ * class MyAdapter extends SfuAdapter {
41
+ * async createRouter(opts) { return { id: cuid(), opts }; }
42
+ * async createTransport(router, peer) { return { id: cuid(), router, peer }; }
43
+ * async produce(transport, kind, rtp) {
44
+ * const id = cuid();
45
+ * this._emit('producer-new', { id, kind, transportId: transport.id });
46
+ * return { id, kind };
47
+ * }
48
+ * async consume(transport, producerId, _rtpCaps) {
49
+ * return { id: cuid(), producerId, transportId: transport.id };
50
+ * }
51
+ * async pauseProducer(id) { this._emit('producer-pause', { id }); }
52
+ * async resumeProducer(id) { this._emit('producer-resume', { id }); }
53
+ * async closeRouter(id) { this._emit('router-close', { id }); }
54
+ * async stats(_scope) { return { ts: Date.now() }; }
55
+ * }
56
+ *
57
+ * @example | Subscribe to adapter events
58
+ * const off = sfu.onEvent((event, payload) => {
59
+ * if (event === 'producer-new') metrics.producers.inc();
60
+ * if (event === 'producer-pause') log.info({ id: payload.id }, 'paused');
61
+ * });
62
+ * // later: off();
28
63
  */
29
64
  class SfuAdapter
30
65
  {
@@ -117,6 +152,21 @@ class SfuAdapter
117
152
  * - 'memory' | 'mediasoup' | 'livekit' | adapter package id.
118
153
  * @param {object} [opts] - constructor options forwarded to the adapter.
119
154
  * @returns {SfuAdapter}
155
+ *
156
+ * @example | Built-in adapter by name
157
+ * const sfu = loadSfuAdapter('mediasoup', {
158
+ * numWorkers: 4,
159
+ * workerSettings: { rtcMinPort: 40000, rtcMaxPort: 49999 },
160
+ * });
161
+ *
162
+ * @example | Pre-constructed instance (e.g. shared singleton in tests)
163
+ * const fake = new MemorySfuAdapter();
164
+ * const sfu = loadSfuAdapter(fake);
165
+ * expect(sfu).toBe(fake);
166
+ *
167
+ * @example | Third-party adapter package
168
+ * // npm i @acme/zero-server-sfu-janus
169
+ * const sfu = loadSfuAdapter('@acme/zero-server-sfu-janus', { wsUrl: 'ws://janus/ws' });
120
170
  */
121
171
  function loadSfuAdapter(spec, opts)
122
172
  {
@@ -1,31 +1,34 @@
1
1
  /**
2
2
  * @module webrtc/sfu/livekit
3
- * @description LiveKit-backed SFU adapter (peerDependency on `livekit-server-sdk`).
3
+ * @description LiveKit-backed SFU adapter (peerDependency on
4
+ * `livekit-server-sdk`). Maps the `SfuAdapter` contract onto LiveKit's
5
+ * remote media plane: `createTransport` mints an AccessToken, mute/close
6
+ * delegate to `RoomServiceClient`, and produce/consume are local
7
+ * bookkeeping while LiveKit handles media client-side.
4
8
  *
5
- * LiveKit's media plane is controlled remotely: rooms live on the
6
- * LiveKit server, participants connect directly with a signed JWT, and
7
- * the server SDK exposes a control-plane REST API. This adapter maps
8
- * the {@link SfuAdapter} contract onto that model:
9
+ * @example | Cloud LiveKit project
10
+ * // npm install livekit-server-sdk
11
+ * const { LiveKitSfuAdapter } = require('@zero-server/webrtc');
12
+ * const sfu = new LiveKitSfuAdapter({
13
+ * host: 'https://my-project.livekit.cloud',
14
+ * apiKey: process.env.LIVEKIT_API_KEY,
15
+ * apiSecret: process.env.LIVEKIT_API_SECRET,
16
+ * tokenTtl: '30m',
17
+ * });
9
18
  *
10
- * - createRouter(opts) -> RoomServiceClient.createRoom(...)
11
- * - createTransport(router,peer) -> mints an AccessToken (the "transport"
12
- * handle is the URL + JWT the peer
13
- * uses to connect to LiveKit directly)
14
- * - produce / consume -> local bookkeeping; LiveKit handles
15
- * the actual media plane client-side
16
- * - pauseProducer / resume -> RoomServiceClient.mutePublishedTrack()
17
- * when the producer was registered
18
- * with a `{room, identity, trackSid}`
19
- * hint; otherwise emits the event
20
- * without touching the server
21
- * - closeRouter(routerId) -> RoomServiceClient.deleteRoom(...)
22
- * - stats() -> RoomServiceClient.listRooms() /
23
- * listParticipants(...) plus local
24
- * counters
19
+ * @example | Self-hosted LiveKit + handing the JWT to a browser
20
+ * const router = await sfu.createRouter({ room: 'standup' });
21
+ * const transport = await sfu.createTransport(router, {
22
+ * id: peer.id,
23
+ * user: { id: peer.user.id, name: peer.user.name },
24
+ * });
25
+ * peer.send('livekit', { url: transport.url, token: transport.token });
25
26
  *
26
- * `livekit-server-sdk` is loaded lazily. Tests inject a stub via
27
- * `opts.livekit`; in production the constructor `require`s the package
28
- * and throws `WEBRTC_SFU_NOT_INSTALLED` if it is missing.
27
+ * @example | Mute a noisy publisher (REST passthrough)
28
+ * await sfu.pauseProducer(producer.id);
29
+ * // adapter records the producerId; if it was registered with a
30
+ * // { room, identity, trackSid } hint it issues
31
+ * // RoomServiceClient.mutePublishedTrack() against LiveKit.
29
32
  */
30
33
  'use strict';
31
34
 
@@ -1,15 +1,55 @@
1
1
  /**
2
2
  * @module webrtc/sfu/mediasoup
3
- * @description mediasoup-backed SFU adapter (peerDependency on `mediasoup`).
3
+ * @description mediasoup-backed SFU adapter (peerDependency on
4
+ * `mediasoup`). Wraps a `Worker` plus one `Router` per `createRouter()`
5
+ * call and delegates produce/consume/pause/resume/close/stats to the
6
+ * native objects. Adapter events are uniform via `onEvent`.
4
7
  *
5
- * Wraps a single mediasoup `Worker` and one `Router` per createRouter()
6
- * call. WebRTC transports are created with `router.createWebRtcTransport()`
7
- * and produce / consume / pause / resume / close / stats all delegate to
8
- * the native mediasoup objects.
8
+ * @example | Production setup with custom RTP port range and announced IP
9
+ * // npm install mediasoup
10
+ * const { MediasoupSfuAdapter } = require('@zero-server/webrtc');
9
11
  *
10
- * `mediasoup` is loaded lazily. Tests inject a stub via `opts.mediasoup`;
11
- * in production the constructor `require('mediasoup')`s the real package
12
- * and throws `WEBRTC_SFU_NOT_INSTALLED` if it is missing.
12
+ * const sfu = new MediasoupSfuAdapter({
13
+ * workerSettings: {
14
+ * logLevel: 'warn',
15
+ * rtcMinPort: 40000,
16
+ * rtcMaxPort: 49999,
17
+ * },
18
+ * webRtcTransportOptions: {
19
+ * listenIps: [{ ip: '0.0.0.0', announcedIp: process.env.PUBLIC_IP }],
20
+ * enableUdp: true,
21
+ * enableTcp: true,
22
+ * preferUdp: true,
23
+ * initialAvailableOutgoingBitrate: 800_000,
24
+ * },
25
+ * });
26
+ *
27
+ * @example | One router per room, lazy on first join
28
+ * const routersByRoom = new Map();
29
+ *
30
+ * async function getRouter(roomName) {
31
+ * let r = routersByRoom.get(roomName);
32
+ * if (!r) {
33
+ * r = await sfu.createRouter({ room: roomName });
34
+ * routersByRoom.set(roomName, r);
35
+ * }
36
+ * return r;
37
+ * }
38
+ *
39
+ * hub.on('join', async ({ peer, room }) => {
40
+ * const router = await getRouter(room.name);
41
+ * const transport = await sfu.createTransport(router, peer);
42
+ * peer.send('sfu-ready', { transportId: transport.id });
43
+ * });
44
+ *
45
+ * @example | Inject a stub for unit tests
46
+ * const stubMediasoup = {
47
+ * createWorker: async () => ({
48
+ * createRouter: async () => fakeRouter,
49
+ * close: () => {},
50
+ * }),
51
+ * };
52
+ * const sfu = new MediasoupSfuAdapter({ mediasoup: stubMediasoup });
13
53
  */
14
54
  'use strict';
15
55
 
@@ -1,16 +1,33 @@
1
1
  /**
2
2
  * @module webrtc/sfu/memory
3
- * @description In-process "memory" SFU adapter.
3
+ * @description In-process "memory" SFU adapter. Bookkeeping-only
4
+ * passthrough that records producers/consumers and emits adapter events
5
+ * without forwarding media packets. Ideal for unit tests, CI, and local
6
+ * dev — use a native adapter for real media plane work.
4
7
  *
5
- * A passthrough router that never touches the network: every produce()
6
- * call records a logical producer, every consume() call records a
7
- * logical consumer, and events are emitted via {@link SfuAdapter#onEvent}.
8
- * Perfect for unit tests, ≤ 4-peer audio-only rooms, and local dev
9
- * where the cost of running mediasoup or LiveKit is unjustified.
8
+ * @example | Use the memory adapter inside a vitest suite
9
+ * const { MemorySfuAdapter } = require('@zero-server/webrtc');
10
+ * const sfu = new MemorySfuAdapter();
10
11
  *
11
- * The adapter does NOT decode or forward media packets - it models
12
- * bookkeeping only. Real packet forwarding lives in native adapters
13
- * (mediasoup, LiveKit).
12
+ * const events = [];
13
+ * sfu.onEvent((event, payload) => events.push({ event, payload }));
14
+ *
15
+ * const router = await sfu.createRouter({ room: 'lobby' });
16
+ * const transport = await sfu.createTransport(router, { id: 'peer-1' });
17
+ * const producer = await sfu.produce(transport, 'audio', { codec: 'opus' });
18
+ * const consumer = await sfu.consume(transport, producer.id, {});
19
+ *
20
+ * expect(events).toEqual([
21
+ * { event: 'router-new', payload: { routerId: router.id } },
22
+ * { event: 'transport-new', payload: expect.any(Object) },
23
+ * { event: 'producer-new', payload: expect.objectContaining({ kind: 'audio' }) },
24
+ * { event: 'consumer-new', payload: expect.objectContaining({ producerId: producer.id }) },
25
+ * ]);
26
+ *
27
+ * @example | Drive it through `loadSfuAdapter`
28
+ * const sfu = loadSfuAdapter('memory');
29
+ * const stats = await sfu.stats();
30
+ * console.log(stats); // { routers, transports, producers, consumers }
14
31
  */
15
32
  'use strict';
16
33
 
@@ -1,13 +1,37 @@
1
1
  /**
2
2
  * @module webrtc/signaling
3
- * @description WebRTC signaling hub - the central WS broker that owns the
4
- * room registry, attaches peers, validates JSEP messages, and
5
- * routes offer / answer / ICE traffic between participants.
3
+ * @description WebRTC signaling hub. Central WS broker that owns the room
4
+ * registry, attaches peers, validates JSEP messages, and routes
5
+ * offer / answer / ICE traffic. Transport-agnostic — bind to `app.ws()`
6
+ * in production, an `EventEmitter` shim in tests.
6
7
  *
7
- * The hub itself is transport-agnostic: anything that exposes a
8
- * `{ send(string), on('message'|'close', cb), close(code?, reason?) }`
9
- * surface is acceptable. `createWebRTC(app, opts)` (PR 9 wiring layer)
10
- * binds an `app.ws()` upgrade handler to a `SignalingHub`.
8
+ * @example | Bind a hub to an `app.ws()` route with all production knobs
9
+ * const app = createApp();
10
+ * const hub = new SignalingHub({
11
+ * joinTokenSecret: process.env.WEBRTC_JWT_SECRET,
12
+ * maxSdpSize: 64 * 1024,
13
+ * maxCandidatesPerOffer: 20,
14
+ * peerMessageRate: 30,
15
+ * maxProtocolErrors: 5,
16
+ * ipAttachRate: 60,
17
+ * originAllowlist: ['https://meet.acme.com'],
18
+ * autoCreateRooms: false,
19
+ * });
20
+ *
21
+ * hub.room('lobby').open();
22
+ *
23
+ * app.ws('/rtc', (ws, req) => {
24
+ * hub.attach(ws, {
25
+ * user: req.user,
26
+ * ip: req.ip,
27
+ * origin: req.headers.origin,
28
+ * });
29
+ * });
30
+ *
31
+ * @example | Observe lifecycle events
32
+ * hub.on('join', ({ peer, room }) => log.info({ peer: peer.id, room: room.name }, 'joined'));
33
+ * hub.on('leave', ({ peer, room }) => log.info({ peer: peer.id, room: room.name }, 'left'));
34
+ * hub.on('wireError', ({ peer, code }) => log.warn({ peer: peer.id, code }, 'wire-error'));
11
35
  */
12
36
 
13
37
  'use strict';
@@ -187,13 +211,33 @@ class SignalingHub extends EventEmitter
187
211
 
188
212
  /**
189
213
  * Attach a transport (WS connection or mock) as a new signaling peer.
190
- * Wires up message and close handlers; returns the `Peer`.
214
+ * Wires up message and close handlers, performs origin / IP-rate
215
+ * pre-checks, sends a `hello` frame with the new peer id, and returns
216
+ * the `Peer`. The returned peer is also registered in `hub.size`.
217
+ *
218
+ * Called from your `app.ws()` upgrade handler in production:
219
+ *
220
+ * ```js
221
+ * app.ws('/rtc', (ws, req) => hub.attach(ws, {
222
+ * user: req.user,
223
+ * ip: req.ip,
224
+ * origin: req.headers.origin,
225
+ * }));
226
+ * ```
191
227
  *
192
228
  * @param {object} transport - `{ send, on, close }`-shaped object.
193
229
  * @param {object} [info]
194
- * @param {*} [info.user] - Authenticated user.
195
- * @param {string} [info.ip] - Remote IP.
230
+ * @param {*} [info.user] - Authenticated user (forwarded to room gates).
231
+ * @param {string} [info.ip] - Remote IP for audit + `ipAttachRate`.
232
+ * @param {string} [info.origin] - Origin header for `originAllowlist`.
196
233
  * @returns {Peer}
234
+ *
235
+ * @example | Test harness: attach a mock transport
236
+ * const mock = new EventEmitter();
237
+ * mock.send = (frame) => mock.emit('out', frame);
238
+ * mock.close = () => mock.emit('close');
239
+ * const peer = hub.attach(mock, { user: { id: 'u1' }, ip: '127.0.0.1' });
240
+ * mock.emit('message', JSON.stringify({ type: 'join', room: 'lobby' }));
197
241
  */
198
242
  attach(transport, info = {})
199
243
  {
@@ -1,19 +1,11 @@
1
1
  /**
2
2
  * @module webrtc/stun
3
- * @description Zero-dependency RFC 8489 STUN (Session Traversal Utilities for
4
- * NAT) client. Sends a Binding Request over UDP, parses the
5
- * Binding Response, and returns the server-reflexive address
6
- * decoded from XOR-MAPPED-ADDRESS (or, as a fallback, the
7
- * deprecated MAPPED-ADDRESS attribute used by some legacy servers).
8
- *
9
- * Only the parts of RFC 8489 we need for NAT discovery are
10
- * implemented: BINDING method, REQUEST and SUCCESS classes,
11
- * XOR-MAPPED-ADDRESS / MAPPED-ADDRESS attributes for IPv4 + IPv6.
12
- * Authenticated TURN allocations are handled in
13
- * `lib/webrtc/turn/`.
3
+ * @description Zero-dependency RFC 8489 STUN client. Sends a Binding Request
4
+ * over UDP, parses the Binding Response, and returns the server-reflexive
5
+ * address from XOR-MAPPED-ADDRESS (or legacy MAPPED-ADDRESS). NAT-discovery
6
+ * subset only; TURN allocations live in `lib/webrtc/turn/`.
14
7
  *
15
8
  * @see https://datatracker.ietf.org/doc/html/rfc8489
16
- * @see https://datatracker.ietf.org/doc/html/rfc5769 (test vectors)
17
9
  */
18
10
 
19
11
  'use strict';
@@ -1,19 +1,10 @@
1
1
  /**
2
2
  * @file lib/webrtc/turn/credentials.js
3
3
  * @module @zero-server/webrtc/turn/credentials
4
- * @description RFC 7635 ephemeral TURN credentials.
5
- *
6
- * Generates time-limited username / credential pairs that any
7
- * RFC 7635-compatible TURN server (notably `coturn` with
8
- * `use-auth-secret` + `static-auth-secret=<S>`) will accept.
9
- *
10
- * Wire format (RFC 7635 §6.2):
11
- * username = "<unix-expiry>:<userId>"
12
- * credential = base64( HMAC-SHA1( <secret>, username ) )
13
- *
14
- * The returned object is shaped like an `RTCIceServer` entry so it can
15
- * be embedded straight into the ICE-server list a signaling endpoint
16
- * serves to browsers.
4
+ * @description RFC 7635 ephemeral TURN credentials. Generates short-TTL
5
+ * username/credential pairs accepted by any compliant TURN server
6
+ * (notably `coturn` with `use-auth-secret`). Returns an `RTCIceServer`-
7
+ * shaped object ready to hand to browsers.
17
8
  */
18
9
 
19
10
  'use strict';
@@ -53,14 +44,38 @@ const VALID_SCHEMES = ['turn:', 'turns:', 'stun:', 'stuns:'];
53
44
  * @returns {TurnCredentials}
54
45
  * @throws {TurnError} On missing / invalid input.
55
46
  *
56
- * @example
47
+ * @example | Hand a fresh credential to a browser before it joins a room
48
+ * // coturn.conf:
49
+ * // use-auth-secret
50
+ * // static-auth-secret=<TURN_SHARED_SECRET>
51
+ * // realm=turn.example.com
52
+ * app.get('/rtc/turn', (req, res) => {
53
+ * const creds = issueTurnCredentials({
54
+ * secret: process.env.TURN_SHARED_SECRET,
55
+ * userId: req.user.id,
56
+ * ttl: '20m',
57
+ * servers: ['turn:turn.example.com:3478?transport=udp'],
58
+ * });
59
+ * res.json(creds); // { urls, username, credential, ttl }
60
+ * });
61
+ *
62
+ * @example | Multiple URLs (UDP, TCP, TLS) for failover
57
63
  * const creds = issueTurnCredentials({
58
64
  * secret: process.env.TURN_SHARED_SECRET,
59
- * userId: req.user.id,
60
- * ttl: '20m',
61
- * servers: ['turn:turn.example.com:3478?transport=udp'],
65
+ * userId: 'bot-42',
66
+ * ttl: 3600,
67
+ * servers: [
68
+ * 'turn:turn.example.com:3478?transport=udp',
69
+ * 'turn:turn.example.com:3478?transport=tcp',
70
+ * 'turns:turn.example.com:5349',
71
+ * 'stun:turn.example.com:3478',
72
+ * ],
73
+ * });
74
+ *
75
+ * @example | Use directly in an RTCPeerConnection (browser side)
76
+ * const pc = new RTCPeerConnection({
77
+ * iceServers: [creds], // { urls, username, credential } is RTCIceServer-shaped
62
78
  * });
63
- * res.json(creds);
64
79
  *
65
80
  * @section ICE & TURN
66
81
  */