@zero-server/sdk 0.9.6 → 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 +54 -53
- package/index.js +116 -4
- package/lib/app.js +22 -22
- package/lib/auth/authorize.js +11 -11
- package/lib/auth/enrollment.js +5 -5
- package/lib/auth/jwt.js +9 -9
- package/lib/auth/oauth.js +1 -1
- package/lib/auth/session.js +5 -5
- package/lib/auth/trustedDevice.js +2 -2
- package/lib/auth/twoFactor.js +11 -11
- package/lib/auth/webauthn.js +6 -6
- package/lib/body/json.js +1 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/rawBuffer.js +1 -1
- package/lib/body/text.js +1 -1
- package/lib/body/urlencoded.js +3 -3
- package/lib/cli.js +43 -28
- package/lib/cluster.js +3 -3
- package/lib/debug.js +10 -10
- package/lib/env/index.js +11 -11
- package/lib/errors.js +131 -16
- package/lib/fetch/index.js +1 -1
- package/lib/grpc/call.js +14 -14
- package/lib/grpc/client.js +4 -4
- package/lib/grpc/codec.js +7 -7
- package/lib/grpc/credentials.js +2 -2
- package/lib/grpc/frame.js +2 -2
- package/lib/grpc/health.js +3 -3
- package/lib/grpc/index.js +3 -3
- package/lib/grpc/metadata.js +3 -3
- package/lib/grpc/proto.js +5 -5
- package/lib/grpc/reflection.js +2 -2
- package/lib/grpc/server.js +3 -3
- package/lib/grpc/status.js +2 -2
- package/lib/grpc/watch.js +1 -1
- package/lib/http/request.js +13 -13
- package/lib/http/response.js +2 -2
- package/lib/lifecycle.js +5 -5
- package/lib/middleware/compress.js +4 -4
- package/lib/observe/health.js +1 -1
- package/lib/observe/index.js +1 -1
- package/lib/observe/logger.js +3 -3
- package/lib/observe/metrics.js +4 -4
- package/lib/observe/tracing.js +4 -4
- package/lib/orm/adapters/json.js +1 -1
- package/lib/orm/adapters/memory.js +2 -2
- package/lib/orm/adapters/mongo.js +2 -2
- package/lib/orm/adapters/mysql.js +2 -2
- package/lib/orm/adapters/postgres.js +2 -2
- package/lib/orm/adapters/sqlite.js +3 -3
- package/lib/orm/audit.js +1 -1
- package/lib/orm/index.js +7 -7
- package/lib/orm/migrate.js +1 -1
- package/lib/orm/model.js +15 -15
- package/lib/orm/procedures.js +1 -1
- package/lib/orm/profiler.js +1 -1
- package/lib/orm/query.js +9 -9
- package/lib/orm/schema.js +1 -1
- package/lib/orm/seed/data/person.js +1 -1
- package/lib/orm/seed/fake.js +10 -10
- package/lib/orm/seed/index.js +4 -4
- package/lib/orm/seed/rng.js +1 -1
- package/lib/orm/snapshot.js +3 -3
- package/lib/orm/tenancy.js +6 -6
- package/lib/orm/views.js +1 -1
- package/lib/router/index.js +9 -9
- package/lib/webrtc/bot.js +405 -0
- package/lib/webrtc/cli.js +182 -0
- package/lib/webrtc/cluster.js +338 -0
- package/lib/webrtc/e2ee.js +274 -0
- package/lib/webrtc/ice.js +363 -0
- package/lib/webrtc/index.js +212 -0
- package/lib/webrtc/joinToken.js +171 -0
- package/lib/webrtc/observe.js +260 -0
- package/lib/webrtc/peer.js +143 -0
- package/lib/webrtc/room.js +184 -0
- package/lib/webrtc/sdp.js +503 -0
- package/lib/webrtc/sfu/index.js +251 -0
- package/lib/webrtc/sfu/livekit.js +304 -0
- package/lib/webrtc/sfu/mediasoup.js +357 -0
- package/lib/webrtc/sfu/memory.js +221 -0
- package/lib/webrtc/signaling.js +590 -0
- package/lib/webrtc/stun.js +484 -0
- package/lib/webrtc/turn/codec.js +370 -0
- package/lib/webrtc/turn/credentials.js +156 -0
- package/lib/webrtc/turn/server.js +648 -0
- package/package.json +2 -2
- package/types/body.d.ts +82 -14
- package/types/cli.d.ts +40 -2
- package/types/index.d.ts +19 -6
- package/types/middleware.d.ts +18 -72
- package/types/orm.d.ts +4 -13
- package/types/request.d.ts +3 -3
- package/types/webrtc.d.ts +501 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/ice
|
|
3
|
+
* @description Zero-dependency ICE candidate parser, serializer, and address
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8839
|
|
9
|
+
* @see https://datatracker.ietf.org/doc/html/rfc5245
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { IceError } = require('../errors');
|
|
15
|
+
|
|
16
|
+
// -- Constants -----------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recognised ICE candidate types (RFC 5245).
|
|
20
|
+
* @type {ReadonlyArray<string>}
|
|
21
|
+
*/
|
|
22
|
+
const CANDIDATE_TYPES = Object.freeze(['host', 'srflx', 'prflx', 'relay']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recognised TCP candidate types (RFC 6544 §4.5).
|
|
26
|
+
* @type {ReadonlyArray<string>}
|
|
27
|
+
*/
|
|
28
|
+
const TCP_TYPES = Object.freeze(['active', 'passive', 'so']);
|
|
29
|
+
|
|
30
|
+
// -- Public types --------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} IceCandidate
|
|
34
|
+
* @property {string} foundation
|
|
35
|
+
* @property {number} component
|
|
36
|
+
* @property {string} transport - 'udp' or 'tcp' (lowercased).
|
|
37
|
+
* @property {number} priority
|
|
38
|
+
* @property {string} address - IPv4, IPv6, or mDNS hostname.
|
|
39
|
+
* @property {number} port
|
|
40
|
+
* @property {string} type - One of CANDIDATE_TYPES.
|
|
41
|
+
* @property {string} [relatedAddress] - From `raddr`.
|
|
42
|
+
* @property {number} [relatedPort] - From `rport`.
|
|
43
|
+
* @property {string} [tcpType] - From `tcptype` (active/passive/so).
|
|
44
|
+
* @property {Object<string,string>} extensions - All other key/value pairs, insertion-ordered.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// =================================================================
|
|
48
|
+
// Parser
|
|
49
|
+
// =================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a single ICE candidate line.
|
|
53
|
+
*
|
|
54
|
+
* Accepts inputs with or without the `a=` SDP-attribute prefix. Returns
|
|
55
|
+
* a plain object; throws `IceError` on any structural problem.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} line - Candidate line, e.g.
|
|
58
|
+
* `candidate:842163049 1 udp 1677729535 192.168.1.5 50000 typ host`.
|
|
59
|
+
* @returns {IceCandidate} Parsed candidate.
|
|
60
|
+
* @throws {IceError} On malformed input.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const c = parseCandidate('candidate:1 1 udp 2122194687 1.2.3.4 50001 typ srflx raddr 192.168.1.5 rport 50000');
|
|
64
|
+
* if (c.type === 'relay') { console.log('relay candidate'); }
|
|
65
|
+
*
|
|
66
|
+
* @section ICE & TURN
|
|
67
|
+
*/
|
|
68
|
+
function parseCandidate(line)
|
|
69
|
+
{
|
|
70
|
+
if (typeof line !== 'string')
|
|
71
|
+
throw new IceError('parseCandidate: input must be a string');
|
|
72
|
+
|
|
73
|
+
let s = line.trim();
|
|
74
|
+
if (s.startsWith('a=')) s = s.slice(2);
|
|
75
|
+
if (!s.startsWith('candidate:'))
|
|
76
|
+
throw new IceError('parseCandidate: missing "candidate:" prefix', { candidate: line });
|
|
77
|
+
s = s.slice('candidate:'.length);
|
|
78
|
+
|
|
79
|
+
const tok = s.split(/\s+/);
|
|
80
|
+
if (tok.length < 8)
|
|
81
|
+
throw new IceError('parseCandidate: too few tokens', { candidate: line });
|
|
82
|
+
|
|
83
|
+
const [foundation, componentStr, transportRaw, priorityStr,
|
|
84
|
+
address, portStr, typKw, type, ...rest] = tok;
|
|
85
|
+
|
|
86
|
+
if (typKw !== 'typ')
|
|
87
|
+
throw new IceError('parseCandidate: expected "typ" keyword', { candidate: line });
|
|
88
|
+
if (!CANDIDATE_TYPES.includes(type))
|
|
89
|
+
throw new IceError(`parseCandidate: unknown type "${type}"`, { candidate: line });
|
|
90
|
+
|
|
91
|
+
const component = Number(componentStr);
|
|
92
|
+
const priority = Number(priorityStr);
|
|
93
|
+
const port = Number(portStr);
|
|
94
|
+
if (!Number.isInteger(component) || component < 0)
|
|
95
|
+
throw new IceError('parseCandidate: invalid component', { candidate: line });
|
|
96
|
+
if (!Number.isFinite(priority))
|
|
97
|
+
throw new IceError('parseCandidate: invalid priority', { candidate: line });
|
|
98
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
99
|
+
throw new IceError('parseCandidate: invalid port', { candidate: line });
|
|
100
|
+
|
|
101
|
+
/** @type {IceCandidate} */
|
|
102
|
+
const out = {
|
|
103
|
+
foundation,
|
|
104
|
+
component,
|
|
105
|
+
transport: transportRaw.toLowerCase(),
|
|
106
|
+
priority,
|
|
107
|
+
address,
|
|
108
|
+
port,
|
|
109
|
+
type,
|
|
110
|
+
extensions: {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Walk remaining key/value pairs. raddr / rport / tcptype are lifted
|
|
114
|
+
// to named fields; everything else lands in `extensions` in input order.
|
|
115
|
+
for (let i = 0; i < rest.length - 1; i += 2)
|
|
116
|
+
{
|
|
117
|
+
const k = rest[i];
|
|
118
|
+
const v = rest[i + 1];
|
|
119
|
+
if (k === 'raddr') out.relatedAddress = v;
|
|
120
|
+
else if (k === 'rport') out.relatedPort = Number(v);
|
|
121
|
+
else if (k === 'tcptype') out.tcpType = v;
|
|
122
|
+
else out.extensions[k] = v;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =================================================================
|
|
129
|
+
// Serializer
|
|
130
|
+
// =================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Serialize a parsed candidate back to its canonical line format.
|
|
134
|
+
* Round-trips outputs of `parseCandidate` exactly, including the
|
|
135
|
+
* insertion order of `extensions`.
|
|
136
|
+
*
|
|
137
|
+
* @param {IceCandidate} c - Parsed candidate object.
|
|
138
|
+
* @returns {string} `candidate:...` line (no `a=` prefix).
|
|
139
|
+
* @throws {IceError} If required fields are missing.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const out = stringifyCandidate(parseCandidate(line));
|
|
143
|
+
*
|
|
144
|
+
* @section ICE & TURN
|
|
145
|
+
*/
|
|
146
|
+
function stringifyCandidate(c)
|
|
147
|
+
{
|
|
148
|
+
if (!c || typeof c !== 'object')
|
|
149
|
+
throw new IceError('stringifyCandidate: input must be an object');
|
|
150
|
+
const required = ['foundation', 'component', 'transport', 'priority', 'address', 'port', 'type'];
|
|
151
|
+
for (const k of required)
|
|
152
|
+
{
|
|
153
|
+
if (c[k] === undefined || c[k] === null)
|
|
154
|
+
throw new IceError(`stringifyCandidate: missing "${k}"`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let s = `candidate:${c.foundation} ${c.component} ${c.transport} ${c.priority} ${c.address} ${c.port} typ ${c.type}`;
|
|
158
|
+
if (c.relatedAddress !== undefined) s += ` raddr ${c.relatedAddress}`;
|
|
159
|
+
if (c.relatedPort !== undefined) s += ` rport ${c.relatedPort}`;
|
|
160
|
+
if (c.tcpType !== undefined) s += ` tcptype ${c.tcpType}`;
|
|
161
|
+
if (c.extensions)
|
|
162
|
+
{
|
|
163
|
+
for (const [k, v] of Object.entries(c.extensions))
|
|
164
|
+
s += ` ${k} ${v}`;
|
|
165
|
+
}
|
|
166
|
+
return s;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =================================================================
|
|
170
|
+
// Address classifiers
|
|
171
|
+
// =================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Test whether the address looks like an IPv4 string.
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
function _isIPv4(addr)
|
|
178
|
+
{
|
|
179
|
+
if (typeof addr !== 'string') return false;
|
|
180
|
+
const m = addr.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
181
|
+
if (!m) return false;
|
|
182
|
+
for (let i = 1; i <= 4; i++) if (Number(m[i]) > 255) return false;
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Test whether the address looks like an IPv6 string (very permissive).
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
function _isIPv6(addr)
|
|
191
|
+
{
|
|
192
|
+
if (typeof addr !== 'string') return false;
|
|
193
|
+
return addr.includes(':') && /^[0-9a-fA-F:]+$/.test(addr);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* True for RFC 1918, RFC 6598 (CGNAT), and IPv6 ULA (RFC 4193) addresses.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} addr - Address to classify.
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*
|
|
202
|
+
* @section ICE & TURN
|
|
203
|
+
*/
|
|
204
|
+
function isPrivateIp(addr)
|
|
205
|
+
{
|
|
206
|
+
if (_isIPv4(addr))
|
|
207
|
+
{
|
|
208
|
+
const [a, b] = addr.split('.').map(Number);
|
|
209
|
+
if (a === 10) return true;
|
|
210
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
211
|
+
if (a === 192 && b === 168) return true;
|
|
212
|
+
if (a === 100 && b >= 64 && b <= 127) return true; // RFC 6598 CGNAT
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (_isIPv6(addr))
|
|
216
|
+
{
|
|
217
|
+
// fc00::/7 ULA
|
|
218
|
+
const head = addr.toLowerCase().split(':')[0];
|
|
219
|
+
if (head.length === 0) return false;
|
|
220
|
+
const n = parseInt(head, 16);
|
|
221
|
+
return (n & 0xfe00) === 0xfc00;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* True for IPv4 127.0.0.0/8 and IPv6 ::1.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} addr - Address to classify.
|
|
230
|
+
* @returns {boolean}
|
|
231
|
+
*
|
|
232
|
+
* @section ICE & TURN
|
|
233
|
+
*/
|
|
234
|
+
function isLoopbackIp(addr)
|
|
235
|
+
{
|
|
236
|
+
if (_isIPv4(addr)) return addr.startsWith('127.');
|
|
237
|
+
if (_isIPv6(addr)) return addr === '::1' || /^0*:0*:0*:0*:0*:0*:0*:0*1$/.test(addr);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* True for IPv4 169.254/16 and IPv6 fe80::/10.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} addr - Address to classify.
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*
|
|
247
|
+
* @section ICE & TURN
|
|
248
|
+
*/
|
|
249
|
+
function isLinkLocalIp(addr)
|
|
250
|
+
{
|
|
251
|
+
if (_isIPv4(addr)) return addr.startsWith('169.254.');
|
|
252
|
+
if (_isIPv6(addr))
|
|
253
|
+
{
|
|
254
|
+
const head = addr.toLowerCase().split(':')[0];
|
|
255
|
+
if (head.length === 0) return false;
|
|
256
|
+
const n = parseInt(head, 16);
|
|
257
|
+
return (n & 0xffc0) === 0xfe80;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* True for mDNS `.local` hostnames used by browsers to avoid leaking
|
|
264
|
+
* local IPs (Chrome's mDNS ICE candidates - RFC 8624 / draft-ietf-mmusic-mdns-ice-candidates).
|
|
265
|
+
*
|
|
266
|
+
* @param {string} host - Hostname to test.
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*
|
|
269
|
+
* @section ICE & TURN
|
|
270
|
+
*/
|
|
271
|
+
function isMdnsHostname(host)
|
|
272
|
+
{
|
|
273
|
+
if (typeof host !== 'string') return false;
|
|
274
|
+
if (_isIPv4(host) || _isIPv6(host)) return false;
|
|
275
|
+
return host.toLowerCase().endsWith('.local');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// =================================================================
|
|
279
|
+
// Policy filter
|
|
280
|
+
// =================================================================
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @typedef {object} CandidateFilterPolicy
|
|
284
|
+
* @property {boolean} [blockPrivate=false] - Drop private / loopback / link-local addresses.
|
|
285
|
+
* @property {boolean} [blockMdns=false] - Drop `.local` (mDNS) hostnames.
|
|
286
|
+
* @property {boolean} [blockTcp=false] - Drop TCP-transport candidates.
|
|
287
|
+
* @property {ReadonlyArray<string>} [allowedTypes] - Whitelist of `type` values (host/srflx/prflx/relay).
|
|
288
|
+
* @property {number} [maxCandidates] - Cap the number of returned candidates.
|
|
289
|
+
* @property {(c:IceCandidate)=>boolean} [predicate] - Custom drop function (return false to drop).
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Filter an array of candidates (lines or parsed objects) against a policy.
|
|
294
|
+
*
|
|
295
|
+
* Returns the same shape it was given: if you pass strings you get strings
|
|
296
|
+
* back; if you pass parsed objects you get parsed objects back. Unparseable
|
|
297
|
+
* string lines are silently skipped so a single bad candidate never poisons
|
|
298
|
+
* the whole offer.
|
|
299
|
+
*
|
|
300
|
+
* @param {Array<string|IceCandidate>} candidates - Input list.
|
|
301
|
+
* @param {CandidateFilterPolicy} [policy={}] - Policy (all defaults are permissive).
|
|
302
|
+
* @returns {Array<string|IceCandidate>} Surviving candidates, same element shape as input.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* const safe = filterCandidates(offer.candidates, {
|
|
306
|
+
* blockPrivate: true,
|
|
307
|
+
* blockMdns: true,
|
|
308
|
+
* allowedTypes: ['srflx', 'relay'],
|
|
309
|
+
* });
|
|
310
|
+
*
|
|
311
|
+
* @section ICE & TURN
|
|
312
|
+
*/
|
|
313
|
+
function filterCandidates(candidates, policy = {})
|
|
314
|
+
{
|
|
315
|
+
if (!Array.isArray(candidates)) return [];
|
|
316
|
+
const {
|
|
317
|
+
blockPrivate = false,
|
|
318
|
+
blockMdns = false,
|
|
319
|
+
blockTcp = false,
|
|
320
|
+
allowedTypes,
|
|
321
|
+
maxCandidates,
|
|
322
|
+
predicate,
|
|
323
|
+
} = policy;
|
|
324
|
+
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const item of candidates)
|
|
327
|
+
{
|
|
328
|
+
const isString = typeof item === 'string';
|
|
329
|
+
let parsed;
|
|
330
|
+
try { parsed = isString ? parseCandidate(item) : item; }
|
|
331
|
+
catch { continue; }
|
|
332
|
+
if (!parsed) continue;
|
|
333
|
+
|
|
334
|
+
if (allowedTypes && !allowedTypes.includes(parsed.type)) continue;
|
|
335
|
+
if (blockTcp && parsed.transport === 'tcp') continue;
|
|
336
|
+
if (blockMdns && isMdnsHostname(parsed.address)) continue;
|
|
337
|
+
if (blockPrivate)
|
|
338
|
+
{
|
|
339
|
+
const a = parsed.address;
|
|
340
|
+
const r = parsed.relatedAddress;
|
|
341
|
+
const isLocal = (x) => x && (isPrivateIp(x) || isLoopbackIp(x) || isLinkLocalIp(x));
|
|
342
|
+
if (isLocal(a) || isLocal(r)) continue;
|
|
343
|
+
}
|
|
344
|
+
if (predicate && !predicate(parsed)) continue;
|
|
345
|
+
|
|
346
|
+
out.push(isString ? item : parsed);
|
|
347
|
+
if (maxCandidates && out.length >= maxCandidates) break;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
parseCandidate,
|
|
354
|
+
stringifyCandidate,
|
|
355
|
+
isPrivateIp,
|
|
356
|
+
isLoopbackIp,
|
|
357
|
+
isLinkLocalIp,
|
|
358
|
+
isMdnsHostname,
|
|
359
|
+
filterCandidates,
|
|
360
|
+
CANDIDATE_TYPES,
|
|
361
|
+
TCP_TYPES,
|
|
362
|
+
IceError,
|
|
363
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @zero-server/webrtc
|
|
3
|
+
* @description First-class, batteries-included WebRTC support for Zero Server.
|
|
4
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
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
|
+
* });
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
'use strict';
|
|
92
|
+
|
|
93
|
+
const {
|
|
94
|
+
WebRTCError, SignalingError, IceError, TurnError, SdpError,
|
|
95
|
+
} = require('../errors');
|
|
96
|
+
|
|
97
|
+
const { parseSdp, stringifySdp } = require('./sdp');
|
|
98
|
+
const {
|
|
99
|
+
parseCandidate, stringifyCandidate, filterCandidates,
|
|
100
|
+
isPrivateIp, isLoopbackIp, isLinkLocalIp, isMdnsHostname,
|
|
101
|
+
CANDIDATE_TYPES, TCP_TYPES,
|
|
102
|
+
} = require('./ice');
|
|
103
|
+
const {
|
|
104
|
+
stunBinding, encodeBindingRequest, decodeMessage,
|
|
105
|
+
encodeXorMappedAddress, decodeXorMappedAddress,
|
|
106
|
+
STUN_MAGIC_COOKIE, STUN_METHOD, STUN_CLASS, STUN_ATTR,
|
|
107
|
+
} = require('./stun');
|
|
108
|
+
const { issueTurnCredentials } = require('./turn/credentials');
|
|
109
|
+
const { TurnServer } = require('./turn/server');
|
|
110
|
+
const { SignalingHub, Room, Peer, PEER_STATE } = require('./signaling');
|
|
111
|
+
const { signJoinToken, verifyJoinToken } = require('./joinToken');
|
|
112
|
+
const { bindObservability } = require('./observe');
|
|
113
|
+
const {
|
|
114
|
+
E2eeChannel, attachE2ee,
|
|
115
|
+
generateE2eeKeyPair, sealKey, openSealedKey,
|
|
116
|
+
} = require('./e2ee');
|
|
117
|
+
const {
|
|
118
|
+
useCluster, ClusterCoordinator, MemoryClusterAdapter,
|
|
119
|
+
} = require('./cluster');
|
|
120
|
+
const { runWebRTCCommand } = require('./cli');
|
|
121
|
+
const { SfuAdapter, loadSfuAdapter } = require('./sfu');
|
|
122
|
+
const { MemorySfuAdapter } = require('./sfu/memory');
|
|
123
|
+
const { MediasoupSfuAdapter } = require('./sfu/mediasoup');
|
|
124
|
+
const { LiveKitSfuAdapter } = require('./sfu/livekit');
|
|
125
|
+
const { spawnBotPeer } = require('./bot');
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @private
|
|
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 })`.
|
|
132
|
+
*/
|
|
133
|
+
const notImplemented = (name) =>
|
|
134
|
+
{
|
|
135
|
+
throw new WebRTCError(
|
|
136
|
+
`${name} is not implemented yet - construct \`new SignalingHub(opts)\` directly and wire it via \`app.ws()\`.`,
|
|
137
|
+
{ code: 'WEBRTC_NOT_IMPLEMENTED' },
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
// Signaling
|
|
143
|
+
createWebRTC: () => notImplemented('createWebRTC'),
|
|
144
|
+
SignalingHub,
|
|
145
|
+
Room,
|
|
146
|
+
Peer,
|
|
147
|
+
PEER_STATE,
|
|
148
|
+
|
|
149
|
+
// SDP / JSEP
|
|
150
|
+
parseSdp,
|
|
151
|
+
stringifySdp,
|
|
152
|
+
|
|
153
|
+
// ICE candidate utilities
|
|
154
|
+
parseCandidate,
|
|
155
|
+
stringifyCandidate,
|
|
156
|
+
filterCandidates,
|
|
157
|
+
isPrivateIp,
|
|
158
|
+
isLoopbackIp,
|
|
159
|
+
isLinkLocalIp,
|
|
160
|
+
isMdnsHostname,
|
|
161
|
+
CANDIDATE_TYPES,
|
|
162
|
+
TCP_TYPES,
|
|
163
|
+
|
|
164
|
+
// STUN client + low-level codecs
|
|
165
|
+
stunBinding,
|
|
166
|
+
encodeBindingRequest,
|
|
167
|
+
decodeMessage,
|
|
168
|
+
encodeXorMappedAddress,
|
|
169
|
+
decodeXorMappedAddress,
|
|
170
|
+
STUN_MAGIC_COOKIE,
|
|
171
|
+
STUN_METHOD,
|
|
172
|
+
STUN_CLASS,
|
|
173
|
+
STUN_ATTR,
|
|
174
|
+
|
|
175
|
+
// TURN
|
|
176
|
+
issueTurnCredentials,
|
|
177
|
+
TurnServer,
|
|
178
|
+
|
|
179
|
+
// SFU adapters + tokens
|
|
180
|
+
SfuAdapter,
|
|
181
|
+
MemorySfuAdapter,
|
|
182
|
+
MediasoupSfuAdapter,
|
|
183
|
+
LiveKitSfuAdapter,
|
|
184
|
+
loadSfuAdapter,
|
|
185
|
+
signJoinToken,
|
|
186
|
+
verifyJoinToken,
|
|
187
|
+
|
|
188
|
+
// Server-side WebRTC peer (node-wrtc bot)
|
|
189
|
+
spawnBotPeer,
|
|
190
|
+
|
|
191
|
+
// Observability
|
|
192
|
+
bindObservability,
|
|
193
|
+
|
|
194
|
+
// SFrame E2EE key relay
|
|
195
|
+
E2eeChannel,
|
|
196
|
+
attachE2ee,
|
|
197
|
+
generateE2eeKeyPair,
|
|
198
|
+
sealKey,
|
|
199
|
+
openSealedKey,
|
|
200
|
+
|
|
201
|
+
// Cluster coordination
|
|
202
|
+
useCluster,
|
|
203
|
+
ClusterCoordinator,
|
|
204
|
+
MemoryClusterAdapter,
|
|
205
|
+
|
|
206
|
+
// CLI
|
|
207
|
+
runWebRTCCommand,
|
|
208
|
+
|
|
209
|
+
// Errors (re-exported from lib/errors.js so consumers can `instanceof` them
|
|
210
|
+
// through @zero-server/webrtc without also requiring @zero-server/errors).
|
|
211
|
+
WebRTCError, SignalingError, IceError, TurnError, SdpError,
|
|
212
|
+
};
|