@zero-server/sdk 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/README.md +11 -10
- package/lib/cli.js +25 -25
- package/lib/orm/snapshot.js +1 -1
- package/lib/webrtc/bot.js +56 -12
- package/lib/webrtc/cli.js +6 -6
- package/lib/webrtc/cluster.js +5 -17
- package/lib/webrtc/e2ee.js +5 -13
- package/lib/webrtc/ice.js +3 -10
- package/lib/webrtc/index.js +99 -19
- package/lib/webrtc/joinToken.js +62 -7
- package/lib/webrtc/observe.js +38 -7
- package/lib/webrtc/peer.js +35 -8
- package/lib/webrtc/room.js +20 -7
- package/lib/webrtc/sdp.js +4 -9
- package/lib/webrtc/sfu/index.js +61 -11
- package/lib/webrtc/sfu/livekit.js +26 -23
- package/lib/webrtc/sfu/mediasoup.js +48 -8
- package/lib/webrtc/sfu/memory.js +26 -9
- package/lib/webrtc/signaling.js +86 -13
- package/lib/webrtc/stun.js +4 -12
- package/lib/webrtc/turn/credentials.js +33 -18
- package/lib/webrtc/turn/server.js +32 -17
- package/package.json +1 -1
- package/types/body.d.ts +82 -14
- package/types/cli.d.ts +40 -2
- package/types/index.d.ts +3 -2
- package/types/middleware.d.ts +17 -71
- package/types/orm.d.ts +1 -10
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-
|
|
15
|
+
<a href="https://github.com/tonywied17/zero-server/actions"><img src="https://img.shields.io/badge/tests-7780%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 `
|
|
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
|
|
223
|
+
Scaffolding and database management via `npx zs`:
|
|
223
224
|
|
|
224
225
|
```bash
|
|
225
|
-
npx
|
|
226
|
-
npx
|
|
227
|
-
npx
|
|
228
|
-
npx
|
|
229
|
-
npx
|
|
230
|
-
npx
|
|
231
|
-
npx
|
|
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
|
|
22
|
-
* // npx
|
|
23
|
-
* // npx
|
|
24
|
-
* // npx
|
|
25
|
-
* // npx
|
|
26
|
-
* // npx
|
|
27
|
-
* // npx
|
|
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 "
|
|
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 "
|
|
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:
|
|
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:
|
|
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:
|
|
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('
|
|
754
|
+
${bold('zs CLI')} - zero-server ORM tooling
|
|
755
755
|
|
|
756
|
-
${bold('Usage:')} npx
|
|
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
|
|
807
|
-
${dim('$')} npx
|
|
808
|
-
${dim('$')} npx
|
|
809
|
-
${dim('$')} npx
|
|
810
|
-
${dim('$')} npx
|
|
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
|
|
815
|
-
${dim('$')} npx
|
|
816
|
-
${dim('$')} npx
|
|
817
|
-
${dim('$')} npx
|
|
818
|
-
${dim('$')} npx
|
|
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(`
|
|
828
|
+
console.log(`zs v${pkg.version} (zero-server)`);
|
|
829
829
|
}
|
|
830
830
|
}
|
|
831
831
|
|
package/lib/orm/snapshot.js
CHANGED
|
@@ -269,7 +269,7 @@ function generateMigrationCode(migrationName, changes, currentSnap)
|
|
|
269
269
|
|
|
270
270
|
/**
|
|
271
271
|
* Auto-generated migration - ${migrationName}
|
|
272
|
-
* Generated by: npx
|
|
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
|
-
* `
|
|
7
|
-
*
|
|
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 `
|
|
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
|
|
14
|
-
* // npx
|
|
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
|
|
17
|
-
* // npx
|
|
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
|
-
'
|
|
171
|
+
'zs webrtc:* - WebRTC tooling',
|
|
172
172
|
'',
|
|
173
173
|
'Subcommands:',
|
|
174
174
|
' webrtc:stun --host H [--port 3478] [--timeout 1000] [--retries 1]',
|
package/lib/webrtc/cluster.js
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module webrtc/cluster
|
|
3
|
-
* @description Cluster adapter for the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
*/
|
package/lib/webrtc/e2ee.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module webrtc/e2ee
|
|
3
|
-
* @description End-to-end-encrypted key relay
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
package/lib/webrtc/index.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
53
|
-
*
|
|
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 -
|
|
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
|
|
142
|
+
// Signaling
|
|
65
143
|
createWebRTC: () => notImplemented('createWebRTC'),
|
|
66
144
|
SignalingHub,
|
|
67
145
|
Room,
|
|
68
146
|
Peer,
|
|
69
147
|
PEER_STATE,
|
|
70
148
|
|
|
71
|
-
// SDP
|
|
149
|
+
// SDP / JSEP
|
|
72
150
|
parseSdp,
|
|
73
151
|
stringifySdp,
|
|
74
152
|
|
|
75
|
-
// ICE
|
|
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
|
-
//
|
|
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
|
|
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
|
|
191
|
+
// Observability
|
|
112
192
|
bindObservability,
|
|
113
193
|
|
|
114
|
-
// E2EE key relay
|
|
194
|
+
// SFrame E2EE key relay
|
|
115
195
|
E2eeChannel,
|
|
116
196
|
attachE2ee,
|
|
117
197
|
generateE2eeKeyPair,
|
|
118
198
|
sealKey,
|
|
119
199
|
openSealedKey,
|
|
120
200
|
|
|
121
|
-
// Cluster
|
|
201
|
+
// Cluster coordination
|
|
122
202
|
useCluster,
|
|
123
203
|
ClusterCoordinator,
|
|
124
204
|
MemoryClusterAdapter,
|
package/lib/webrtc/joinToken.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module webrtc/joinToken
|
|
3
|
-
* @description Signed, short-TTL join tokens
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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 = {})
|