@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.
Files changed (94) hide show
  1. package/README.md +54 -64
  2. package/index.js +116 -4
  3. package/lib/app.js +22 -22
  4. package/lib/auth/authorize.js +11 -11
  5. package/lib/auth/enrollment.js +5 -5
  6. package/lib/auth/jwt.js +9 -9
  7. package/lib/auth/oauth.js +1 -1
  8. package/lib/auth/session.js +5 -5
  9. package/lib/auth/trustedDevice.js +2 -2
  10. package/lib/auth/twoFactor.js +11 -11
  11. package/lib/auth/webauthn.js +6 -6
  12. package/lib/body/json.js +1 -1
  13. package/lib/body/raw.js +1 -1
  14. package/lib/body/rawBuffer.js +1 -1
  15. package/lib/body/text.js +1 -1
  16. package/lib/body/urlencoded.js +3 -3
  17. package/lib/cli.js +19 -4
  18. package/lib/cluster.js +3 -3
  19. package/lib/debug.js +10 -10
  20. package/lib/env/index.js +11 -11
  21. package/lib/errors.js +131 -16
  22. package/lib/fetch/index.js +1 -1
  23. package/lib/grpc/call.js +14 -14
  24. package/lib/grpc/client.js +4 -4
  25. package/lib/grpc/codec.js +7 -7
  26. package/lib/grpc/credentials.js +2 -2
  27. package/lib/grpc/frame.js +2 -2
  28. package/lib/grpc/health.js +3 -3
  29. package/lib/grpc/index.js +3 -3
  30. package/lib/grpc/metadata.js +3 -3
  31. package/lib/grpc/proto.js +5 -5
  32. package/lib/grpc/reflection.js +2 -2
  33. package/lib/grpc/server.js +3 -3
  34. package/lib/grpc/status.js +2 -2
  35. package/lib/grpc/watch.js +1 -1
  36. package/lib/http/request.js +13 -13
  37. package/lib/http/response.js +2 -2
  38. package/lib/lifecycle.js +5 -5
  39. package/lib/middleware/compress.js +4 -4
  40. package/lib/observe/health.js +1 -1
  41. package/lib/observe/index.js +1 -1
  42. package/lib/observe/logger.js +3 -3
  43. package/lib/observe/metrics.js +4 -4
  44. package/lib/observe/tracing.js +4 -4
  45. package/lib/orm/adapters/json.js +1 -1
  46. package/lib/orm/adapters/memory.js +2 -2
  47. package/lib/orm/adapters/mongo.js +2 -2
  48. package/lib/orm/adapters/mysql.js +2 -2
  49. package/lib/orm/adapters/postgres.js +2 -2
  50. package/lib/orm/adapters/sqlite.js +3 -3
  51. package/lib/orm/audit.js +1 -1
  52. package/lib/orm/index.js +7 -7
  53. package/lib/orm/migrate.js +1 -1
  54. package/lib/orm/model.js +15 -15
  55. package/lib/orm/procedures.js +1 -1
  56. package/lib/orm/profiler.js +1 -1
  57. package/lib/orm/query.js +9 -9
  58. package/lib/orm/schema.js +1 -1
  59. package/lib/orm/seed/data/person.js +1 -1
  60. package/lib/orm/seed/fake.js +10 -10
  61. package/lib/orm/seed/index.js +4 -4
  62. package/lib/orm/seed/rng.js +1 -1
  63. package/lib/orm/snapshot.js +2 -2
  64. package/lib/orm/tenancy.js +6 -6
  65. package/lib/orm/views.js +1 -1
  66. package/lib/router/index.js +9 -9
  67. package/lib/webrtc/bot.js +361 -0
  68. package/lib/webrtc/cli.js +182 -0
  69. package/lib/webrtc/cluster.js +350 -0
  70. package/lib/webrtc/e2ee.js +282 -0
  71. package/lib/webrtc/ice.js +370 -0
  72. package/lib/webrtc/index.js +132 -0
  73. package/lib/webrtc/joinToken.js +116 -0
  74. package/lib/webrtc/observe.js +229 -0
  75. package/lib/webrtc/peer.js +116 -0
  76. package/lib/webrtc/room.js +171 -0
  77. package/lib/webrtc/sdp.js +508 -0
  78. package/lib/webrtc/sfu/index.js +201 -0
  79. package/lib/webrtc/sfu/livekit.js +301 -0
  80. package/lib/webrtc/sfu/mediasoup.js +317 -0
  81. package/lib/webrtc/sfu/memory.js +204 -0
  82. package/lib/webrtc/signaling.js +546 -0
  83. package/lib/webrtc/stun.js +492 -0
  84. package/lib/webrtc/turn/codec.js +370 -0
  85. package/lib/webrtc/turn/credentials.js +141 -0
  86. package/lib/webrtc/turn/server.js +633 -0
  87. package/package.json +2 -2
  88. package/types/body.d.ts +1 -1
  89. package/types/cli.d.ts +1 -1
  90. package/types/index.d.ts +16 -4
  91. package/types/middleware.d.ts +1 -1
  92. package/types/orm.d.ts +3 -3
  93. package/types/request.d.ts +3 -3
  94. package/types/webrtc.d.ts +501 -0
@@ -5,13 +5,13 @@
5
5
  * @description Extensible fake data generator.
6
6
  *
7
7
  * Key capabilities:
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)
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 same as email()).
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 uses a
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 shared across all calls in the process lifetime
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 };
@@ -5,10 +5,10 @@
5
5
  * @description Public API for the seed subsystem.
6
6
  *
7
7
  * Re-exports:
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
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');
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  /**
17
- * mulberry32 minimal, high-quality 32-bit PRNG.
17
+ * mulberry32 - minimal, high-quality 32-bit PRNG.
18
18
  * @param {number} s - Unsigned 32-bit integer seed.
19
19
  * @returns {() => number} Float in [0, 1).
20
20
  */
@@ -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 but we need the previous snapshot's schema for that
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 ${migrationName}
271
+ * Auto-generated migration - ${migrationName}
272
272
  * Generated by: npx zh make:migration
273
273
  */
274
274
  module.exports = {
@@ -43,8 +43,8 @@ class TenantContext
43
43
  /**
44
44
  * Multi-tenancy manager.
45
45
  * Supports two strategies:
46
- * - `'row'` adds a tenant column to every query (row-level isolation)
47
- * - `'schema'` uses separate database schemas per tenant (PostgreSQL)
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() all query builder paths
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() inject tenant column
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() still applies tenant scope
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 only allow alphanumerics and underscores
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 identifier-safe only
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(', ') || '*'
@@ -298,56 +298,56 @@ class Router
298
298
  }
299
299
 
300
300
  /**
301
- * @see Router#add shortcut for GET requests.
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 shortcut for POST requests.
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 shortcut for PUT requests.
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 shortcut for DELETE requests.
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 shortcut for PATCH requests.
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 shortcut for OPTIONS requests.
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 shortcut for HEAD requests.
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 matches every HTTP method.
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 register multiple methods on the same path.
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 };