@zero-server/sdk 0.9.5 → 0.9.7
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 -64
- 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 +19 -4
- 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 +2 -2
- 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 +361 -0
- package/lib/webrtc/cli.js +182 -0
- package/lib/webrtc/cluster.js +350 -0
- package/lib/webrtc/e2ee.js +282 -0
- package/lib/webrtc/ice.js +370 -0
- package/lib/webrtc/index.js +132 -0
- package/lib/webrtc/joinToken.js +116 -0
- package/lib/webrtc/observe.js +229 -0
- package/lib/webrtc/peer.js +116 -0
- package/lib/webrtc/room.js +171 -0
- package/lib/webrtc/sdp.js +508 -0
- package/lib/webrtc/sfu/index.js +201 -0
- package/lib/webrtc/sfu/livekit.js +301 -0
- package/lib/webrtc/sfu/mediasoup.js +317 -0
- package/lib/webrtc/sfu/memory.js +204 -0
- package/lib/webrtc/signaling.js +546 -0
- package/lib/webrtc/stun.js +492 -0
- package/lib/webrtc/turn/codec.js +370 -0
- package/lib/webrtc/turn/credentials.js +141 -0
- package/lib/webrtc/turn/server.js +633 -0
- package/package.json +2 -2
- package/types/body.d.ts +1 -1
- package/types/cli.d.ts +1 -1
- package/types/index.d.ts +16 -4
- package/types/middleware.d.ts +1 -1
- package/types/orm.d.ts +3 -3
- package/types/request.d.ts +3 -3
- package/types/webrtc.d.ts +501 -0
package/lib/orm/seed/fake.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* @description Extensible fake data generator.
|
|
6
6
|
*
|
|
7
7
|
* Key capabilities:
|
|
8
|
-
* • Seeded / reproducible output
|
|
9
|
-
* • Guaranteed-unique values
|
|
10
|
-
* • Multi-locale names
|
|
11
|
-
* • Rich phone formats
|
|
12
|
-
* • Configurable emails
|
|
13
|
-
* • Flexible usernames
|
|
14
|
-
* • Fixed-length numeric strings
|
|
8
|
+
* • Seeded / reproducible output - Fake.seed(42)
|
|
9
|
+
* • Guaranteed-unique values - Fake.unique(() => Fake.email())
|
|
10
|
+
* • Multi-locale names - Fake.firstName({ locale: 'ja', sex: 'female' })
|
|
11
|
+
* • Rich phone formats - Fake.phone({ countryCode: 'DE', format: 'international' })
|
|
12
|
+
* • Configurable emails - Fake.email({ provider: 'company.io' })
|
|
13
|
+
* • Flexible usernames - Fake.username({ style: 'underscore' })
|
|
14
|
+
* • Fixed-length numeric strings - Fake.numericString(8)
|
|
15
15
|
* • Person: job titles, bio, zodiac, gender, prefix/suffix
|
|
16
16
|
* • Location: city, country, state, address, lat/lng, timezone
|
|
17
17
|
* • Commerce: product, company, category, price, industry
|
|
@@ -928,7 +928,7 @@ class Fake
|
|
|
928
928
|
// -- Internet & Network -------------------------------------------------
|
|
929
929
|
|
|
930
930
|
/**
|
|
931
|
-
* Random email address (backward-compatible shorthand
|
|
931
|
+
* Random email address (backward-compatible shorthand - same as email()).
|
|
932
932
|
* Accepts no arguments for historical use.
|
|
933
933
|
*/
|
|
934
934
|
|
|
@@ -1051,7 +1051,7 @@ class Fake
|
|
|
1051
1051
|
static userAgent() { return _pick(USER_AGENTS); }
|
|
1052
1052
|
|
|
1053
1053
|
/**
|
|
1054
|
-
* Random password-like string. NOT suitable for real passwords
|
|
1054
|
+
* Random password-like string. NOT suitable for real passwords - uses a
|
|
1055
1055
|
* PRNG seeded from Math.random, not a CSPRNG.
|
|
1056
1056
|
*
|
|
1057
1057
|
* @param {object} [options] - Configuration options.
|
|
@@ -1180,7 +1180,7 @@ class Fake
|
|
|
1180
1180
|
}
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
-
// Static uniqueness tracker
|
|
1183
|
+
// Static uniqueness tracker - shared across all calls in the process lifetime
|
|
1184
1184
|
Fake._tracker = new UniqueTracker();
|
|
1185
1185
|
|
|
1186
1186
|
module.exports = { Fake };
|
package/lib/orm/seed/index.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* @description Public API for the seed subsystem.
|
|
6
6
|
*
|
|
7
7
|
* Re-exports:
|
|
8
|
-
* - `Fake`
|
|
9
|
-
* - `Factory`
|
|
10
|
-
* - `Seeder`
|
|
11
|
-
* - `SeederRunner`
|
|
8
|
+
* - `Fake` - static fake-data generator
|
|
9
|
+
* - `Factory` - model factory for defining / creating test fixtures
|
|
10
|
+
* - `Seeder` - base class for database seeders
|
|
11
|
+
* - `SeederRunner` - orchestrates running multiple seeders
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const { Fake } = require('./fake');
|
package/lib/orm/seed/rng.js
CHANGED
package/lib/orm/snapshot.js
CHANGED
|
@@ -234,7 +234,7 @@ function generateMigrationCode(migrationName, changes, currentSnap)
|
|
|
234
234
|
for (const table of changes.tables.dropped)
|
|
235
235
|
{
|
|
236
236
|
upLines.push(` await db.adapter.dropTable('${table}');`);
|
|
237
|
-
// down recreates
|
|
237
|
+
// down recreates - but we need the previous snapshot's schema for that
|
|
238
238
|
// This is handled via the `prev` reference embedded in the dropped table
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -268,7 +268,7 @@ function generateMigrationCode(migrationName, changes, currentSnap)
|
|
|
268
268
|
return `'use strict';
|
|
269
269
|
|
|
270
270
|
/**
|
|
271
|
-
* Auto-generated migration
|
|
271
|
+
* Auto-generated migration - ${migrationName}
|
|
272
272
|
* Generated by: npx zh make:migration
|
|
273
273
|
*/
|
|
274
274
|
module.exports = {
|
package/lib/orm/tenancy.js
CHANGED
|
@@ -43,8 +43,8 @@ class TenantContext
|
|
|
43
43
|
/**
|
|
44
44
|
* Multi-tenancy manager.
|
|
45
45
|
* Supports two strategies:
|
|
46
|
-
* - `'row'`
|
|
47
|
-
* - `'schema'`
|
|
46
|
+
* - `'row'` - adds a tenant column to every query (row-level isolation)
|
|
47
|
+
* - `'schema'` - uses separate database schemas per tenant (PostgreSQL)
|
|
48
48
|
*/
|
|
49
49
|
class TenantManager
|
|
50
50
|
{
|
|
@@ -242,7 +242,7 @@ class TenantManager
|
|
|
242
242
|
const origCount = ModelClass.count.bind(ModelClass);
|
|
243
243
|
const origExists = ModelClass.exists.bind(ModelClass);
|
|
244
244
|
|
|
245
|
-
// Patch query()
|
|
245
|
+
// Patch query() - all query builder paths
|
|
246
246
|
ModelClass.query = function ()
|
|
247
247
|
{
|
|
248
248
|
const q = origQuery();
|
|
@@ -251,7 +251,7 @@ class TenantManager
|
|
|
251
251
|
return q;
|
|
252
252
|
};
|
|
253
253
|
|
|
254
|
-
// Patch create()
|
|
254
|
+
// Patch create() - inject tenant column
|
|
255
255
|
ModelClass.create = async function (data)
|
|
256
256
|
{
|
|
257
257
|
const tid = manager.getCurrentTenant();
|
|
@@ -283,7 +283,7 @@ class TenantManager
|
|
|
283
283
|
return origFindOne(conditions);
|
|
284
284
|
};
|
|
285
285
|
|
|
286
|
-
// Patch findById()
|
|
286
|
+
// Patch findById() - still applies tenant scope
|
|
287
287
|
ModelClass.findById = async function (id)
|
|
288
288
|
{
|
|
289
289
|
const tid = manager.getCurrentTenant();
|
|
@@ -344,7 +344,7 @@ class TenantManager
|
|
|
344
344
|
throw new Error('Schema-based tenancy requires a SQL adapter');
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// Sanitize schema name
|
|
347
|
+
// Sanitize schema name - only allow alphanumerics and underscores
|
|
348
348
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schema))
|
|
349
349
|
{
|
|
350
350
|
throw new Error(`Invalid schema name: "${schema}"`);
|
package/lib/orm/views.js
CHANGED
|
@@ -293,7 +293,7 @@ class DatabaseView
|
|
|
293
293
|
const descriptor = this._query.build();
|
|
294
294
|
const table = descriptor.table;
|
|
295
295
|
|
|
296
|
-
// Validate field names and table
|
|
296
|
+
// Validate field names and table - identifier-safe only
|
|
297
297
|
const idRe = /^[a-zA-Z_][a-zA-Z0-9_.*]*$/;
|
|
298
298
|
const fields = descriptor.fields
|
|
299
299
|
? descriptor.fields.filter(f => idRe.test(f)).join(', ') || '*'
|
package/lib/router/index.js
CHANGED
|
@@ -298,56 +298,56 @@ class Router
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* @see Router#add
|
|
301
|
+
* @see Router#add - shortcut for GET requests.
|
|
302
302
|
* @param {string} path - Route pattern.
|
|
303
303
|
* @param {...Function} fns - Handler functions.
|
|
304
304
|
* @returns {Router} `this` for chaining.
|
|
305
305
|
*/
|
|
306
306
|
get(path, ...fns) { const o = this._extractOpts(fns); this.add('GET', path, fns, o); return this; }
|
|
307
307
|
/**
|
|
308
|
-
* @see Router#add
|
|
308
|
+
* @see Router#add - shortcut for POST requests.
|
|
309
309
|
* @param {string} path - Route pattern.
|
|
310
310
|
* @param {...Function} fns - Handler functions.
|
|
311
311
|
* @returns {Router} `this` for chaining.
|
|
312
312
|
*/
|
|
313
313
|
post(path, ...fns) { const o = this._extractOpts(fns); this.add('POST', path, fns, o); return this; }
|
|
314
314
|
/**
|
|
315
|
-
* @see Router#add
|
|
315
|
+
* @see Router#add - shortcut for PUT requests.
|
|
316
316
|
* @param {string} path - Route pattern.
|
|
317
317
|
* @param {...Function} fns - Handler functions.
|
|
318
318
|
* @returns {Router} `this` for chaining.
|
|
319
319
|
*/
|
|
320
320
|
put(path, ...fns) { const o = this._extractOpts(fns); this.add('PUT', path, fns, o); return this; }
|
|
321
321
|
/**
|
|
322
|
-
* @see Router#add
|
|
322
|
+
* @see Router#add - shortcut for DELETE requests.
|
|
323
323
|
* @param {string} path - Route pattern.
|
|
324
324
|
* @param {...Function} fns - Handler functions.
|
|
325
325
|
* @returns {Router} `this` for chaining.
|
|
326
326
|
*/
|
|
327
327
|
delete(path, ...fns) { const o = this._extractOpts(fns); this.add('DELETE', path, fns, o); return this; }
|
|
328
328
|
/**
|
|
329
|
-
* @see Router#add
|
|
329
|
+
* @see Router#add - shortcut for PATCH requests.
|
|
330
330
|
* @param {string} path - Route pattern.
|
|
331
331
|
* @param {...Function} fns - Handler functions.
|
|
332
332
|
* @returns {Router} `this` for chaining.
|
|
333
333
|
*/
|
|
334
334
|
patch(path, ...fns) { const o = this._extractOpts(fns); this.add('PATCH', path, fns, o); return this; }
|
|
335
335
|
/**
|
|
336
|
-
* @see Router#add
|
|
336
|
+
* @see Router#add - shortcut for OPTIONS requests.
|
|
337
337
|
* @param {string} path - Route pattern.
|
|
338
338
|
* @param {...Function} fns - Handler functions.
|
|
339
339
|
* @returns {Router} `this` for chaining.
|
|
340
340
|
*/
|
|
341
341
|
options(path, ...fns) { const o = this._extractOpts(fns); this.add('OPTIONS', path, fns, o); return this; }
|
|
342
342
|
/**
|
|
343
|
-
* @see Router#add
|
|
343
|
+
* @see Router#add - shortcut for HEAD requests.
|
|
344
344
|
* @param {string} path - Route pattern.
|
|
345
345
|
* @param {...Function} fns - Handler functions.
|
|
346
346
|
* @returns {Router} `this` for chaining.
|
|
347
347
|
*/
|
|
348
348
|
head(path, ...fns) { const o = this._extractOpts(fns); this.add('HEAD', path, fns, o); return this; }
|
|
349
349
|
/**
|
|
350
|
-
* @see Router#add
|
|
350
|
+
* @see Router#add - matches every HTTP method.
|
|
351
351
|
* @param {string} path - Route pattern.
|
|
352
352
|
* @param {...Function} fns - Handler functions.
|
|
353
353
|
* @returns {Router} `this` for chaining.
|
|
@@ -355,7 +355,7 @@ class Router
|
|
|
355
355
|
all(path, ...fns) { const o = this._extractOpts(fns); this.add('ALL', path, fns, o); return this; }
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
-
* Chainable route builder
|
|
358
|
+
* Chainable route builder - register multiple methods on the same path.
|
|
359
359
|
*
|
|
360
360
|
* @example
|
|
361
361
|
* router.route('/users')
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/bot
|
|
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`.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { EventEmitter } = require('node:events');
|
|
20
|
+
const { WebRTCError } = require('../errors');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Spawn a server-side bot peer that joins `room` on the given hub.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {object} opts.hub The {@link SignalingHub} instance.
|
|
27
|
+
* @param {string} opts.room Room name to join.
|
|
28
|
+
* @param {*} [opts.user] Opaque user object attached to the peer.
|
|
29
|
+
* @param {string} [opts.ip='127.0.0.1'] IP recorded on the attached peer.
|
|
30
|
+
* @param {string} [opts.joinToken] Optional join token forwarded to the hub.
|
|
31
|
+
* @param {Array} [opts.iceServers=[]] RTCConfiguration.iceServers.
|
|
32
|
+
* @param {object} [opts.rtcConfig] Additional RTCConfiguration fields.
|
|
33
|
+
* @param {object} [opts.wrtc] Injected `wrtc` module (testing).
|
|
34
|
+
* @param {Function} [opts.onTrack] (track, streams, fromPeerId) => void
|
|
35
|
+
* @param {Function} [opts.onDataChannel] (channel, fromPeerId) => void
|
|
36
|
+
* @param {Function} [opts.onPeerJoin] (remotePeerId) => void
|
|
37
|
+
* @param {Function} [opts.onPeerLeave] (remotePeerId) => void
|
|
38
|
+
* @param {Function} [opts.onError] (err) => void (non-fatal errors)
|
|
39
|
+
* @returns {{
|
|
40
|
+
* peer: object,
|
|
41
|
+
* peerConnections: Map<string, object>,
|
|
42
|
+
* getPeerConnection: (remotePeerId: string) => object | undefined,
|
|
43
|
+
* ready: Promise<{ peerId: string }>,
|
|
44
|
+
* close: () => void,
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
function spawnBotPeer(opts)
|
|
48
|
+
{
|
|
49
|
+
const o = opts || {};
|
|
50
|
+
if (!o.hub || typeof o.hub.attach !== 'function')
|
|
51
|
+
{
|
|
52
|
+
throw new WebRTCError('spawnBotPeer requires { hub }', { code: 'WEBRTC_BOT_INVALID_CONFIG' });
|
|
53
|
+
}
|
|
54
|
+
if (!o.room || typeof o.room !== 'string')
|
|
55
|
+
{
|
|
56
|
+
throw new WebRTCError('spawnBotPeer requires { room }', { code: 'WEBRTC_BOT_INVALID_CONFIG' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const wrtc = o.wrtc || _tryRequireWrtc();
|
|
60
|
+
const { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } = wrtc;
|
|
61
|
+
if (typeof RTCPeerConnection !== 'function')
|
|
62
|
+
{
|
|
63
|
+
throw new WebRTCError(
|
|
64
|
+
"spawnBotPeer: provided 'wrtc' module is missing RTCPeerConnection",
|
|
65
|
+
{ code: 'WEBRTC_BOT_INVALID_WRTC' },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rtcConfig = {
|
|
70
|
+
iceServers: Array.isArray(o.iceServers) ? o.iceServers : [],
|
|
71
|
+
...(o.rtcConfig || {}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onTrack = typeof o.onTrack === 'function' ? o.onTrack : null;
|
|
75
|
+
const onDataChannel = typeof o.onDataChannel === 'function' ? o.onDataChannel : null;
|
|
76
|
+
const onPeerJoin = typeof o.onPeerJoin === 'function' ? o.onPeerJoin : null;
|
|
77
|
+
const onPeerLeave = typeof o.onPeerLeave === 'function' ? o.onPeerLeave : null;
|
|
78
|
+
const onError = typeof o.onError === 'function' ? o.onError : (() => {});
|
|
79
|
+
|
|
80
|
+
// In-process transport that satisfies the hub's contract.
|
|
81
|
+
const transport = new BotTransport();
|
|
82
|
+
|
|
83
|
+
const pcs = new Map(); // remotePeerId -> RTCPeerConnection
|
|
84
|
+
let myPeerId = null;
|
|
85
|
+
let closed = false;
|
|
86
|
+
let resolveReady;
|
|
87
|
+
let rejectReady;
|
|
88
|
+
const ready = new Promise((res, rej) => { resolveReady = res; rejectReady = rej; });
|
|
89
|
+
|
|
90
|
+
function pushToHub(msg)
|
|
91
|
+
{
|
|
92
|
+
if (closed) return;
|
|
93
|
+
try { transport._inject(msg); }
|
|
94
|
+
catch (err) { onError(err); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getOrCreatePc(remoteId)
|
|
98
|
+
{
|
|
99
|
+
let pc = pcs.get(remoteId);
|
|
100
|
+
if (pc) return pc;
|
|
101
|
+
pc = new RTCPeerConnection(rtcConfig);
|
|
102
|
+
pcs.set(remoteId, pc);
|
|
103
|
+
|
|
104
|
+
pc.onicecandidate = (ev) =>
|
|
105
|
+
{
|
|
106
|
+
if (ev && ev.candidate && ev.candidate.candidate)
|
|
107
|
+
{
|
|
108
|
+
pushToHub({
|
|
109
|
+
type: 'ice',
|
|
110
|
+
target: remoteId,
|
|
111
|
+
candidate: ev.candidate.candidate,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
if (onTrack)
|
|
116
|
+
{
|
|
117
|
+
pc.ontrack = (ev) =>
|
|
118
|
+
{
|
|
119
|
+
try { onTrack(ev.track, ev.streams || [], remoteId); }
|
|
120
|
+
catch (err) { onError(err); }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (onDataChannel)
|
|
124
|
+
{
|
|
125
|
+
pc.ondatachannel = (ev) =>
|
|
126
|
+
{
|
|
127
|
+
try { onDataChannel(ev.channel, remoteId); }
|
|
128
|
+
catch (err) { onError(err); }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return pc;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function offerTo(remoteId)
|
|
135
|
+
{
|
|
136
|
+
try
|
|
137
|
+
{
|
|
138
|
+
const pc = getOrCreatePc(remoteId);
|
|
139
|
+
const offer = await pc.createOffer();
|
|
140
|
+
await pc.setLocalDescription(offer);
|
|
141
|
+
pushToHub({ type: 'offer', target: remoteId, sdp: pc.localDescription.sdp });
|
|
142
|
+
}
|
|
143
|
+
catch (err) { onError(err); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function answerTo(remoteId, sdp)
|
|
147
|
+
{
|
|
148
|
+
try
|
|
149
|
+
{
|
|
150
|
+
const pc = getOrCreatePc(remoteId);
|
|
151
|
+
await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp }));
|
|
152
|
+
const answer = await pc.createAnswer();
|
|
153
|
+
await pc.setLocalDescription(answer);
|
|
154
|
+
pushToHub({ type: 'answer', target: remoteId, sdp: pc.localDescription.sdp });
|
|
155
|
+
}
|
|
156
|
+
catch (err) { onError(err); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function applyAnswer(remoteId, sdp)
|
|
160
|
+
{
|
|
161
|
+
try
|
|
162
|
+
{
|
|
163
|
+
const pc = pcs.get(remoteId);
|
|
164
|
+
if (!pc) return;
|
|
165
|
+
await pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp }));
|
|
166
|
+
}
|
|
167
|
+
catch (err) { onError(err); }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function applyIce(remoteId, candidate)
|
|
171
|
+
{
|
|
172
|
+
try
|
|
173
|
+
{
|
|
174
|
+
const pc = pcs.get(remoteId);
|
|
175
|
+
if (!pc || !candidate) return;
|
|
176
|
+
await pc.addIceCandidate(new RTCIceCandidate({ candidate, sdpMid: '0', sdpMLineIndex: 0 }));
|
|
177
|
+
}
|
|
178
|
+
catch (err) { onError(err); }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function dropPc(remoteId)
|
|
182
|
+
{
|
|
183
|
+
const pc = pcs.get(remoteId);
|
|
184
|
+
if (!pc) return;
|
|
185
|
+
try { pc.close(); } catch (_) { /* noop */ }
|
|
186
|
+
pcs.delete(remoteId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The hub calls `transport.send(json)` for every outbound message.
|
|
190
|
+
// We intercept those, parse them, and drive the negotiation state machine.
|
|
191
|
+
transport._onOutbound = (data) =>
|
|
192
|
+
{
|
|
193
|
+
let msg;
|
|
194
|
+
try { msg = JSON.parse(data); }
|
|
195
|
+
catch (err) { onError(err); return; }
|
|
196
|
+
|
|
197
|
+
switch (msg.type)
|
|
198
|
+
{
|
|
199
|
+
case 'hello':
|
|
200
|
+
myPeerId = msg.peerId;
|
|
201
|
+
pushToHub({ type: 'join', room: o.room, token: o.joinToken });
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'joined':
|
|
205
|
+
if (resolveReady)
|
|
206
|
+
{
|
|
207
|
+
resolveReady({ peerId: myPeerId });
|
|
208
|
+
resolveReady = null;
|
|
209
|
+
rejectReady = null;
|
|
210
|
+
}
|
|
211
|
+
// Existing peers in the room - bot is the newcomer, so it offers first.
|
|
212
|
+
// The hub's `peers` list includes the bot itself; skip self.
|
|
213
|
+
if (Array.isArray(msg.peers))
|
|
214
|
+
{
|
|
215
|
+
for (const id of msg.peers)
|
|
216
|
+
{
|
|
217
|
+
if (id !== myPeerId) offerTo(id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'peer-joined':
|
|
223
|
+
if (onPeerJoin)
|
|
224
|
+
{
|
|
225
|
+
try { onPeerJoin(msg.id); }
|
|
226
|
+
catch (err) { onError(err); }
|
|
227
|
+
}
|
|
228
|
+
// New peer joined after us - they are the newcomer and will offer; we wait.
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'peer-left':
|
|
232
|
+
dropPc(msg.id);
|
|
233
|
+
if (onPeerLeave)
|
|
234
|
+
{
|
|
235
|
+
try { onPeerLeave(msg.id); }
|
|
236
|
+
catch (err) { onError(err); }
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'offer':
|
|
241
|
+
answerTo(msg.from, msg.sdp);
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case 'answer':
|
|
245
|
+
applyAnswer(msg.from, msg.sdp);
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case 'ice':
|
|
249
|
+
applyIce(msg.from, msg.candidate);
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case 'error':
|
|
253
|
+
if (rejectReady)
|
|
254
|
+
{
|
|
255
|
+
rejectReady(new WebRTCError(
|
|
256
|
+
`bot peer error: ${msg.message || msg.code}`,
|
|
257
|
+
{ code: msg.code || 'WEBRTC_BOT_HUB_ERROR' },
|
|
258
|
+
));
|
|
259
|
+
rejectReady = null;
|
|
260
|
+
resolveReady = null;
|
|
261
|
+
}
|
|
262
|
+
onError(new WebRTCError(msg.message || msg.code, { code: msg.code || 'WEBRTC_BOT_HUB_ERROR' }));
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
default:
|
|
266
|
+
// Unhandled message types are passed through silently; tests / consumers
|
|
267
|
+
// can subscribe to `peer` events on the hub if they need them.
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Attach AFTER the outbound handler is wired so that the synchronous
|
|
273
|
+
// `hello` frame the hub sends inside attach() is delivered to us.
|
|
274
|
+
const peer = o.hub.attach(transport, { user: o.user || null, ip: o.ip || '127.0.0.1' });
|
|
275
|
+
|
|
276
|
+
function close()
|
|
277
|
+
{
|
|
278
|
+
if (closed) return;
|
|
279
|
+
closed = true;
|
|
280
|
+
for (const id of Array.from(pcs.keys())) dropPc(id);
|
|
281
|
+
try { transport.close(1000, 'bot-close'); } catch (_) { /* noop */ }
|
|
282
|
+
if (rejectReady)
|
|
283
|
+
{
|
|
284
|
+
rejectReady(new WebRTCError('bot peer closed before ready', { code: 'WEBRTC_BOT_CLOSED' }));
|
|
285
|
+
rejectReady = null;
|
|
286
|
+
resolveReady = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
peer,
|
|
292
|
+
peerConnections: pcs,
|
|
293
|
+
getPeerConnection: (remoteId) => pcs.get(remoteId),
|
|
294
|
+
ready,
|
|
295
|
+
close,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @private
|
|
301
|
+
* In-process transport that bridges the hub <-> bot peer.
|
|
302
|
+
*
|
|
303
|
+
* The hub calls `send(string)` for every outbound message; the bot
|
|
304
|
+
* sets `_onOutbound` to receive those messages. The bot uses
|
|
305
|
+
* `_inject(obj)` to push inbound messages back to the hub (which
|
|
306
|
+
* listens via the standard `'message'` event).
|
|
307
|
+
*/
|
|
308
|
+
class BotTransport extends EventEmitter
|
|
309
|
+
{
|
|
310
|
+
constructor()
|
|
311
|
+
{
|
|
312
|
+
super();
|
|
313
|
+
this.closed = false;
|
|
314
|
+
this._onOutbound = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
send(data)
|
|
318
|
+
{
|
|
319
|
+
if (this.closed) return;
|
|
320
|
+
if (typeof this._onOutbound === 'function')
|
|
321
|
+
{
|
|
322
|
+
try { this._onOutbound(data); }
|
|
323
|
+
catch (_) { /* swallow */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_inject(obj)
|
|
328
|
+
{
|
|
329
|
+
if (this.closed) return;
|
|
330
|
+
const data = typeof obj === 'string' ? obj : JSON.stringify(obj);
|
|
331
|
+
this.emit('message', data);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
close(code, reason)
|
|
335
|
+
{
|
|
336
|
+
if (this.closed) return;
|
|
337
|
+
this.closed = true;
|
|
338
|
+
this.emit('close', code || 1000, reason || '');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @private
|
|
344
|
+
* Try to `require('wrtc')` then `require('@roamhq/wrtc')`.
|
|
345
|
+
* Throws a clean `WEBRTC_BOT_NOT_INSTALLED` error if neither is present.
|
|
346
|
+
*/
|
|
347
|
+
function _tryRequireWrtc()
|
|
348
|
+
{
|
|
349
|
+
const tried = [];
|
|
350
|
+
for (const name of ['wrtc', '@roamhq/wrtc'])
|
|
351
|
+
{
|
|
352
|
+
try { return require(name); }
|
|
353
|
+
catch (err) { tried.push(`${name} (${err.code || err.message})`); }
|
|
354
|
+
}
|
|
355
|
+
throw new WebRTCError(
|
|
356
|
+
`spawnBotPeer requires the 'wrtc' (or '@roamhq/wrtc') peerDependency: npm install wrtc - tried: ${tried.join(', ')}`,
|
|
357
|
+
{ code: 'WEBRTC_BOT_NOT_INSTALLED' },
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = { spawnBotPeer };
|