@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.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  <p align="center">
14
14
  <a href="https://github.com/tonywied17/zero-server/actions"><img src="https://img.shields.io/github/actions/workflow/status/tonywied17/zero-server/ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&label=CI" alt="CI"></a>
15
- <a href="https://github.com/tonywied17/zero-server/actions"><img src="https://img.shields.io/badge/tests-7767%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white" alt="tests"></a>
15
+ <a href="https://github.com/tonywied17/zero-server/actions"><img src="https://img.shields.io/badge/tests-7777%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white" alt="tests"></a>
16
16
  <a href="https://github.com/tonywied17/zero-server"><img src="https://img.shields.io/badge/coverage-95.85%25-brightgreen?style=flat-square&logo=vitest&logoColor=white" alt="coverage"></a>
17
17
  <a href="https://z-server.dev"><img src="https://img.shields.io/badge/docs-z--server.dev-00d8e0?style=flat-square&logo=readthedocs&logoColor=white" alt="docs"></a>
18
18
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-00d8e0?style=flat-square&logo=opensourceinitiative&logoColor=white" alt="MIT"></a>
@@ -55,13 +55,14 @@ npm install @zero-server/core @zero-server/body @zero-server/middleware
55
55
  | `@zero-server/auth` | `jwt`, `session`, `oauth`, `authorize`, `twoFactor`, `webauthn`, `trustedDevice`, `enrollment` |
56
56
  | `@zero-server/orm` | `Database`, `Model`, `Query`, `TYPES`, migrations, seeders, replicas, search, geo, tenancy, audit |
57
57
  | `@zero-server/realtime` | `WebSocketConnection`, `WebSocketPool`, `SSEStream` |
58
+ | `@zero-server/webrtc` | signaling hub, rooms/peers, STUN/TURN, SFU adapters (memory, mediasoup, LiveKit), join tokens, E2EE, `spawnBotPeer` |
58
59
  | `@zero-server/grpc` | gRPC server, client, codec, status, metadata, framing, health, reflection, balancer |
59
60
  | `@zero-server/observe` | `MetricsRegistry`, `Tracer`, structured `Logger`, health checks |
60
61
  | `@zero-server/lifecycle` | `LifecycleManager`, `ClusterManager`, `clusterize` |
61
62
  | `@zero-server/env` | typed `.env` loader |
62
63
  | `@zero-server/fetch` | server-side `fetch` client |
63
64
  | `@zero-server/errors` | every typed `HttpError` class plus ORM/framework errors |
64
- | `@zero-server/cli` | programmatic `CLI` / `runCLI` entry points for `zh` / `zs` |
65
+ | `@zero-server/cli` | programmatic `CLI` / `runCLI` entry points for `zs` |
65
66
 
66
67
  > Each scoped package is fully standalone at runtime - its own `index.js`, its own bundled lib, its own types. Mix and match freely; versions stay aligned across the `@zero-server/*` release set.
67
68
 
@@ -219,16 +220,16 @@ docker run -d -p 9090:9090 -v ./prometheus.yml:/etc/prometheus/prometheus.yml pr
219
220
 
220
221
  ### CLI
221
222
 
222
- Scaffolding and database management via `npx zh`:
223
+ Scaffolding and database management via `npx zs`:
223
224
 
224
225
  ```bash
225
- npx zh migrate # run pending migrations
226
- npx zh migrate:rollback # rollback last migration
227
- npx zh migrate:status # show migration status
228
- npx zh seed # run seeders
229
- npx zh make:model User # scaffold a model
230
- npx zh make:migration name # create migration file
231
- npx zh make:seeder User # create seeder file
226
+ npx zs migrate # run pending migrations
227
+ npx zs migrate:rollback # rollback last migration
228
+ npx zs migrate:status # show migration status
229
+ npx zs seed # run seeders
230
+ npx zs make:model User # scaffold a model
231
+ npx zs make:migration name # create migration file
232
+ npx zs make:seeder User # create seeder file
232
233
  ```
233
234
 
234
235
  ### Environment Config
package/lib/cli.js CHANGED
@@ -18,13 +18,13 @@
18
18
  * };
19
19
  *
20
20
  * // Run via npx (no global install needed):
21
- * // npx zh migrate
22
- * // npx zh migrate:rollback
23
- * // npx zh migrate:status
24
- * // npx zh seed
25
- * // npx zh make:model User
26
- * // npx zh make:migration create_posts
27
- * // npx zh make:seeder Users
21
+ * // npx zs migrate
22
+ * // npx zs migrate:rollback
23
+ * // npx zs migrate:status
24
+ * // npx zs seed
25
+ * // npx zs make:model User
26
+ * // npx zs make:migration create_posts
27
+ * // npx zs make:seeder Users
28
28
  *
29
29
  * // Or programmatically:
30
30
  * const { runCLI } = require('@zero-server/sdk');
@@ -215,7 +215,7 @@ class CLI
215
215
  throw new Error(
216
216
  'No configuration file found.\n' +
217
217
  'Create a zero.config.js with database and migration settings.\n' +
218
- 'See "zh help" for examples.'
218
+ 'See "zs help" for examples.'
219
219
  );
220
220
  }
221
221
 
@@ -431,7 +431,7 @@ class CLI
431
431
  if (status.executed.includes(lastMigration.name))
432
432
  {
433
433
  console.error(red(`Cannot remove "${lastMigration.name}" - it has already been applied.`));
434
- console.error(dim('Run "zh migrate:rollback" first, then try again.'));
434
+ console.error(dim('Run "zs migrate:rollback" first, then try again.'));
435
435
  process.exitCode = 1;
436
436
  await db.close();
437
437
  return;
@@ -519,7 +519,7 @@ class CLI
519
519
  const name = this.args.find(a => !a.startsWith('-'));
520
520
  if (!name)
521
521
  {
522
- console.error(red('Usage: zh make:model <Name>'));
522
+ console.error(red('Usage: zs make:model <Name>'));
523
523
  process.exitCode = 1;
524
524
  return;
525
525
  }
@@ -575,7 +575,7 @@ module.exports = ${className};
575
575
  const name = this.args.find(a => !a.startsWith('-'));
576
576
  if (!name)
577
577
  {
578
- console.error(red('Usage: zh make:migration <name>'));
578
+ console.error(red('Usage: zs make:migration <name>'));
579
579
  process.exitCode = 1;
580
580
  return;
581
581
  }
@@ -701,7 +701,7 @@ module.exports = {
701
701
  const name = this.args.find(a => !a.startsWith('-'));
702
702
  if (!name)
703
703
  {
704
- console.error(red('Usage: zh make:seeder <name>'));
704
+ console.error(red('Usage: zs make:seeder <name>'));
705
705
  process.exitCode = 1;
706
706
  return;
707
707
  }
@@ -751,9 +751,9 @@ module.exports = ${className};
751
751
  _help()
752
752
  {
753
753
  console.log(`
754
- ${bold('zh CLI')} - zero-server ORM tooling
754
+ ${bold('zs CLI')} - zero-server ORM tooling
755
755
 
756
- ${bold('Usage:')} npx zh <command> [options]
756
+ ${bold('Usage:')} npx zs <command> [options]
757
757
 
758
758
  ${bold('Commands:')}
759
759
 
@@ -803,19 +803,19 @@ ${bold('Config file:')} ${dim('zero.config.js (or .zero-server.js / legacy .zero
803
803
 
804
804
  ${bold('Auto-generated migrations:')}
805
805
 
806
- ${dim('$')} npx zh make:migration create_users ${dim('# detects new User model → generates CREATE TABLE')}
807
- ${dim('$')} npx zh make:migration add_email ${dim('# detects new email column → generates ADD COLUMN')}
808
- ${dim('$')} npx zh make:migration --empty init ${dim('# blank migration (manual mode)')}
809
- ${dim('$')} npx zh migrate ${dim('# apply pending migrations')}
810
- ${dim('$')} npx zh migrate:remove ${dim('# undo last make:migration')}
806
+ ${dim('$')} npx zs make:migration create_users ${dim('# detects new User model → generates CREATE TABLE')}
807
+ ${dim('$')} npx zs make:migration add_email ${dim('# detects new email column → generates ADD COLUMN')}
808
+ ${dim('$')} npx zs make:migration --empty init ${dim('# blank migration (manual mode)')}
809
+ ${dim('$')} npx zs migrate ${dim('# apply pending migrations')}
810
+ ${dim('$')} npx zs migrate:remove ${dim('# undo last make:migration')}
811
811
 
812
812
  ${bold('Examples:')}
813
813
 
814
- ${dim('$')} npx zh make:model User ${dim('# creates models/User.js')}
815
- ${dim('$')} npx zh make:migration create_users ${dim('# auto-generates from models')}
816
- ${dim('$')} npx zh migrate ${dim('# runs all pending migrations')}
817
- ${dim('$')} npx zh migrate --config=db.config.js
818
- ${dim('$')} npx zh seed ${dim('# runs all seeders')}
814
+ ${dim('$')} npx zs make:model User ${dim('# creates models/User.js')}
815
+ ${dim('$')} npx zs make:migration create_users ${dim('# auto-generates from models')}
816
+ ${dim('$')} npx zs migrate ${dim('# runs all pending migrations')}
817
+ ${dim('$')} npx zs migrate --config=db.config.js
818
+ ${dim('$')} npx zs seed ${dim('# runs all seeders')}
819
819
  `);
820
820
  }
821
821
 
@@ -825,7 +825,7 @@ ${bold('Examples:')}
825
825
  _version()
826
826
  {
827
827
  const pkg = require('../package.json');
828
- console.log(`zh v${pkg.version} (zero-server)`);
828
+ console.log(`zs v${pkg.version} (zero-server)`);
829
829
  }
830
830
  }
831
831
 
@@ -269,7 +269,7 @@ function generateMigrationCode(migrationName, changes, currentSnap)
269
269
 
270
270
  /**
271
271
  * Auto-generated migration - ${migrationName}
272
- * Generated by: npx zh make:migration
272
+ * Generated by: npx zs make:migration
273
273
  */
274
274
  module.exports = {
275
275
  name: '${migrationName}',
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 = {})