@livedesk/hub 0.1.27 → 0.1.28
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/package.json +2 -2
- package/src/agents/agent-manager.js +27 -11
- package/src/agents/codex-agent-runtime.js +325 -41
- package/src/frame-packet-contract.mjs +427 -0
- package/src/live-capture-transition-retry.mjs +297 -0
- package/src/mode4-atlas-pool.js +17 -3
- package/src/mode4-atlas-worker.js +134 -21
- package/src/mode4-atlas.js +180 -16
- package/src/remote-audio-subscription-contract.mjs +109 -0
- package/src/remote-audio-subscription-contract.test.mjs +72 -0
- package/src/remote-hub.js +6405 -4236
- package/src/server.js +3234 -2206
- package/src/transport/agent-binary-ingress.js +810 -0
- package/src/transport/relay-hub-control.js +372 -56
- package/src/transport/udp-hub-transport.js +186 -45
- package/src/transport/udp-protocol.js +302 -70
|
@@ -3,15 +3,19 @@ import { lookup } from 'node:dns/promises';
|
|
|
3
3
|
import dgram from 'node:dgram';
|
|
4
4
|
import {
|
|
5
5
|
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
+
UDP_FRAME_CHUNK_PROTOCOL,
|
|
6
7
|
UDP_P2P_PROTOCOL,
|
|
7
8
|
UdpFrameReassembler,
|
|
8
9
|
UdpReplayWindow,
|
|
9
|
-
decodeRendezvousMessage,
|
|
10
|
-
decodeUdpPacket,
|
|
10
|
+
decodeRendezvousMessage,
|
|
11
|
+
decodeUdpPacket,
|
|
11
12
|
encodeRendezvousMessage,
|
|
12
13
|
encodeUdpPacket,
|
|
13
|
-
createUdpSessionKey
|
|
14
|
-
|
|
14
|
+
createUdpSessionKey,
|
|
15
|
+
isUdpBinaryDatagram,
|
|
16
|
+
peekUdpSessionRouteKey,
|
|
17
|
+
udpSessionRouteKey
|
|
18
|
+
} from './udp-protocol.js';
|
|
15
19
|
|
|
16
20
|
function isEnabled(value, fallback = false) {
|
|
17
21
|
if (value === undefined || value === null || value === '') return fallback;
|
|
@@ -69,9 +73,21 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
69
73
|
const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
|
|
70
74
|
const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
|
|
71
75
|
const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
|
|
72
|
-
const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
|
|
73
|
-
const socket = dgram.createSocket('udp4');
|
|
74
|
-
const sessions = new Map();
|
|
76
|
+
const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
|
|
77
|
+
const socket = dgram.createSocket('udp4');
|
|
78
|
+
const sessions = new Map();
|
|
79
|
+
const sessionsBySessionId = new Map();
|
|
80
|
+
const sessionsByRouteKey = new Map();
|
|
81
|
+
const resource = {
|
|
82
|
+
datagramsReceived: 0,
|
|
83
|
+
binaryDatagramsReceived: 0,
|
|
84
|
+
rendezvousJsonParseAttempts: 0,
|
|
85
|
+
binaryJsonParseAttempts: 0,
|
|
86
|
+
sessionLookupCount: 0,
|
|
87
|
+
linearSessionScanCount: 0,
|
|
88
|
+
authenticatedDatagrams: 0,
|
|
89
|
+
rejectedDatagrams: 0
|
|
90
|
+
};
|
|
75
91
|
let frameHandler = () => {};
|
|
76
92
|
let eventHandler = () => {};
|
|
77
93
|
let started = false;
|
|
@@ -80,6 +96,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
80
96
|
let rendezvousAddresses = new Set();
|
|
81
97
|
let rendezvousRefreshTimer = null;
|
|
82
98
|
let rendezvousRefreshPromise = null;
|
|
99
|
+
let frameExpiryTimer = null;
|
|
83
100
|
|
|
84
101
|
function emit(type, event = {}) {
|
|
85
102
|
eventHandler(type, event);
|
|
@@ -172,17 +189,31 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
172
189
|
rendezvousRefreshTimer = null;
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
function startFrameExpiryTimer() {
|
|
193
|
+
if (frameExpiryTimer) return;
|
|
194
|
+
frameExpiryTimer = setInterval(() => {
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
for (const session of sessions.values()) {
|
|
197
|
+
session.reassembler.expire(now);
|
|
198
|
+
}
|
|
199
|
+
}, Math.max(20, Math.min(250, frameTimeoutMs)));
|
|
200
|
+
frameExpiryTimer.unref?.();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function stopFrameExpiryTimer() {
|
|
204
|
+
if (frameExpiryTimer) clearInterval(frameExpiryTimer);
|
|
205
|
+
frameExpiryTimer = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
175
208
|
function isTrustedRendezvousSource(rinfo) {
|
|
176
209
|
return Number(rinfo?.port) === rendezvousPort
|
|
177
210
|
&& rendezvousAddresses.has(normalizedAddress(rinfo?.address));
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
function sessionForPacket(packet) {
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const sessionId = packet.subarray(27, 27 + sessionLength).toString('utf8');
|
|
185
|
-
return [...sessions.values()].find(item => item.sessionId === sessionId) || null;
|
|
214
|
+
resource.sessionLookupCount += 1;
|
|
215
|
+
const routeKey = peekUdpSessionRouteKey(packet);
|
|
216
|
+
return routeKey === null ? null : sessionsByRouteKey.get(routeKey) || null;
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
function bindAuthenticatedClientEndpoint(session, rinfo) {
|
|
@@ -200,7 +231,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
200
231
|
}
|
|
201
232
|
|
|
202
233
|
function onRendezvousPeer(message, rinfo) {
|
|
203
|
-
const session =
|
|
234
|
+
const session = sessionsBySessionId.get(safeText(message.roomId, 160));
|
|
204
235
|
if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
|
|
205
236
|
const address = safeText(message.host, 128);
|
|
206
237
|
const port = Number(message.port);
|
|
@@ -223,16 +254,43 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
223
254
|
emit('UdpPeerEndpointObserved', { deviceId: session.deviceId, endpoint: `${address}:${port}` });
|
|
224
255
|
}
|
|
225
256
|
|
|
226
|
-
socket.on('message', (payload, rinfo) => {
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
257
|
+
socket.on('message', (payload, rinfo) => {
|
|
258
|
+
resource.datagramsReceived += 1;
|
|
259
|
+
if (!isUdpBinaryDatagram(payload)) {
|
|
260
|
+
// Rendezvous messages are the only JSON datagrams. Binary packets
|
|
261
|
+
// never pay a speculative JSON parse/exception before routing.
|
|
262
|
+
if (payload?.[0] !== 0x7b) {
|
|
263
|
+
resource.rejectedDatagrams += 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
resource.rendezvousJsonParseAttempts += 1;
|
|
267
|
+
const rendezvous = decodeRendezvousMessage(payload);
|
|
268
|
+
if (rendezvous?.type === 'rendezvous.peer') {
|
|
269
|
+
onRendezvousPeer(rendezvous, rinfo);
|
|
270
|
+
} else {
|
|
271
|
+
resource.rejectedDatagrams += 1;
|
|
272
|
+
}
|
|
230
273
|
return;
|
|
231
274
|
}
|
|
275
|
+
resource.binaryDatagramsReceived += 1;
|
|
232
276
|
const session = sessionForPacket(payload);
|
|
233
|
-
if (!session)
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
if (!session) {
|
|
278
|
+
resource.rejectedDatagrams += 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
session.resource.binaryDatagramsReceived += 1;
|
|
282
|
+
session.resource.decryptAttempts += 1;
|
|
283
|
+
const packet = decodeUdpPacket(payload, session.key, {
|
|
284
|
+
id: session.sessionId,
|
|
285
|
+
bytes: session.sessionIdBytes
|
|
286
|
+
});
|
|
287
|
+
if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) {
|
|
288
|
+
session.resource.rejectedDatagrams += 1;
|
|
289
|
+
resource.rejectedDatagrams += 1;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
resource.authenticatedDatagrams += 1;
|
|
293
|
+
session.resource.authenticatedDatagrams += 1;
|
|
236
294
|
bindAuthenticatedClientEndpoint(session, rinfo);
|
|
237
295
|
session.lastPacketAt = Date.now();
|
|
238
296
|
if (packet.type === 'probe') {
|
|
@@ -288,6 +346,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
288
346
|
protocol: 'livedesk.frame-transport-proof.v1',
|
|
289
347
|
streamId: safeText(complete.header?.streamId, 128),
|
|
290
348
|
commandId: safeText(complete.header?.commandId, 128),
|
|
349
|
+
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
291
350
|
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
292
351
|
monitorIndex: Number(complete.header?.monitorIndex ?? -1),
|
|
293
352
|
error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
|
|
@@ -300,6 +359,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
300
359
|
frameSeq: Number(complete.header?.frameSeq || 0) || 0,
|
|
301
360
|
streamId: safeText(complete.header?.streamId, 128),
|
|
302
361
|
commandId: safeText(complete.header?.commandId, 128),
|
|
362
|
+
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
303
363
|
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
304
364
|
monitorIndex: Number(complete.header?.monitorIndex ?? -1)
|
|
305
365
|
});
|
|
@@ -327,19 +387,35 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
327
387
|
logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
|
|
328
388
|
void refreshRendezvousRegistrations('startup');
|
|
329
389
|
startRendezvousRefreshTimer();
|
|
390
|
+
startFrameExpiryTimer();
|
|
330
391
|
return getStatus();
|
|
331
392
|
}
|
|
332
393
|
|
|
333
|
-
function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
|
|
334
|
-
if (!enabled || capabilities?.udpP2p !== true || !started) return null;
|
|
335
|
-
const id = safeText(deviceId, 160);
|
|
336
|
-
if (!id) return null;
|
|
337
|
-
sessions.get(id)
|
|
338
|
-
|
|
339
|
-
|
|
394
|
+
function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
|
|
395
|
+
if (!enabled || capabilities?.udpP2p !== true || !started) return null;
|
|
396
|
+
const id = safeText(deviceId, 160);
|
|
397
|
+
if (!id) return null;
|
|
398
|
+
const previous = sessions.get(id);
|
|
399
|
+
previous?.sendControl?.({ type: 'udp.session.close', reason: 'replaced' });
|
|
400
|
+
if (previous) {
|
|
401
|
+
sessions.delete(id);
|
|
402
|
+
sessionsBySessionId.delete(previous.sessionId);
|
|
403
|
+
sessionsByRouteKey.delete(previous.routeKey);
|
|
404
|
+
previous.reassembler.clear();
|
|
405
|
+
}
|
|
406
|
+
const key = createUdpSessionKey();
|
|
407
|
+
let udpSessionId;
|
|
408
|
+
let routeKey;
|
|
409
|
+
do {
|
|
410
|
+
udpSessionId = crypto.randomUUID();
|
|
411
|
+
routeKey = udpSessionRouteKey(udpSessionId);
|
|
412
|
+
} while (sessionsByRouteKey.has(routeKey));
|
|
413
|
+
const session = {
|
|
340
414
|
deviceId: id,
|
|
341
415
|
tcpSessionId: safeText(tcpSessionId, 160),
|
|
342
|
-
sessionId:
|
|
416
|
+
sessionId: udpSessionId,
|
|
417
|
+
sessionIdBytes: Buffer.from(udpSessionId, 'utf8'),
|
|
418
|
+
routeKey,
|
|
343
419
|
key,
|
|
344
420
|
keyBase64: key.toString('base64'),
|
|
345
421
|
rendezvousToken: crypto.randomBytes(16).toString('hex'),
|
|
@@ -349,14 +425,22 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
349
425
|
hubRendezvousEndpoint: null,
|
|
350
426
|
hubRendezvousObservedAt: 0,
|
|
351
427
|
replay: new UdpReplayWindow(),
|
|
352
|
-
reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
|
|
353
|
-
|
|
428
|
+
reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
|
|
429
|
+
resource: {
|
|
430
|
+
binaryDatagramsReceived: 0,
|
|
431
|
+
decryptAttempts: 0,
|
|
432
|
+
authenticatedDatagrams: 0,
|
|
433
|
+
rejectedDatagrams: 0
|
|
434
|
+
},
|
|
435
|
+
ready: false,
|
|
354
436
|
createdAt: Date.now(),
|
|
355
437
|
lastPacketAt: 0,
|
|
356
438
|
framesReceived: 0
|
|
357
|
-
};
|
|
358
|
-
sessions.set(id, session);
|
|
359
|
-
|
|
439
|
+
};
|
|
440
|
+
sessions.set(id, session);
|
|
441
|
+
sessionsBySessionId.set(session.sessionId, session);
|
|
442
|
+
sessionsByRouteKey.set(session.routeKey, session);
|
|
443
|
+
sendControl?.({
|
|
360
444
|
type: 'udp.session.offer',
|
|
361
445
|
protocol: UDP_P2P_PROTOCOL,
|
|
362
446
|
sessionId: session.sessionId,
|
|
@@ -365,8 +449,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
365
449
|
port: boundPort,
|
|
366
450
|
expiresInMs: punchTimeoutMs + 15_000,
|
|
367
451
|
punchTimeoutMs,
|
|
368
|
-
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
369
|
-
|
|
452
|
+
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
453
|
+
frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
|
|
454
|
+
frameTimeoutMs,
|
|
370
455
|
maxPendingFrames,
|
|
371
456
|
rendezvous: rendezvousHost
|
|
372
457
|
? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.rendezvousToken }
|
|
@@ -377,12 +462,15 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
377
462
|
return { sessionId: session.sessionId, ready: false };
|
|
378
463
|
}
|
|
379
464
|
|
|
380
|
-
function unregisterDevice(deviceId, tcpSessionId = '') {
|
|
465
|
+
function unregisterDevice(deviceId, tcpSessionId = '') {
|
|
381
466
|
const id = safeText(deviceId, 160);
|
|
382
467
|
const session = sessions.get(id);
|
|
383
|
-
if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
|
|
384
|
-
sessions.delete(id);
|
|
385
|
-
|
|
468
|
+
if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
|
|
469
|
+
sessions.delete(id);
|
|
470
|
+
sessionsBySessionId.delete(session.sessionId);
|
|
471
|
+
sessionsByRouteKey.delete(session.routeKey);
|
|
472
|
+
session.reassembler.clear();
|
|
473
|
+
emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
|
|
386
474
|
}
|
|
387
475
|
|
|
388
476
|
function getDeviceStatus(deviceId) {
|
|
@@ -396,6 +484,12 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
396
484
|
sessionId: session.sessionId,
|
|
397
485
|
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
398
486
|
framesReceived: session.framesReceived,
|
|
487
|
+
resource: {
|
|
488
|
+
...session.resource,
|
|
489
|
+
binaryJsonParseAttempts: 0,
|
|
490
|
+
sessionLookup: 'map-o1',
|
|
491
|
+
linearSessionScanCount: 0
|
|
492
|
+
},
|
|
399
493
|
...session.reassembler.getStats()
|
|
400
494
|
};
|
|
401
495
|
}
|
|
@@ -467,7 +561,11 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
467
561
|
|
|
468
562
|
async function close() {
|
|
469
563
|
stopRendezvousRefreshTimer();
|
|
564
|
+
stopFrameExpiryTimer();
|
|
565
|
+
for (const session of sessions.values()) session.reassembler.clear();
|
|
470
566
|
sessions.clear();
|
|
567
|
+
sessionsBySessionId.clear();
|
|
568
|
+
sessionsByRouteKey.clear();
|
|
471
569
|
if (!started) return;
|
|
472
570
|
started = false;
|
|
473
571
|
await new Promise(resolve => socket.close(() => resolve()));
|
|
@@ -486,8 +584,45 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
486
584
|
setEventHandler: handler => { eventHandler = typeof handler === 'function' ? handler : () => {}; }
|
|
487
585
|
};
|
|
488
586
|
|
|
489
|
-
function getStatus() {
|
|
490
|
-
|
|
587
|
+
function getStatus() {
|
|
588
|
+
let readySessionCount = 0;
|
|
589
|
+
const reassembly = {
|
|
590
|
+
pendingFrames: 0,
|
|
591
|
+
pendingBytes: 0,
|
|
592
|
+
pendingChunkBuffers: 0,
|
|
593
|
+
pendingBytesHighWatermark: 0,
|
|
594
|
+
pendingChunkBuffersHighWatermark: 0,
|
|
595
|
+
droppedFrames: 0,
|
|
596
|
+
completedFrames: 0,
|
|
597
|
+
completedPayloadCopyCount: 0,
|
|
598
|
+
completedPayloadCopyBytes: 0,
|
|
599
|
+
fullWireDuplicateCopyCount: 0,
|
|
600
|
+
binaryChunkHeaderCount: 0,
|
|
601
|
+
legacyChunkHeaderCount: 0
|
|
602
|
+
};
|
|
603
|
+
for (const session of sessions.values()) {
|
|
604
|
+
if (session.ready) readySessionCount += 1;
|
|
605
|
+
const stats = session.reassembler.getStats();
|
|
606
|
+
reassembly.pendingFrames += stats.pendingFrames;
|
|
607
|
+
reassembly.pendingBytes += stats.pendingBytes;
|
|
608
|
+
reassembly.pendingChunkBuffers += stats.pendingChunkBuffers;
|
|
609
|
+
reassembly.pendingBytesHighWatermark = Math.max(
|
|
610
|
+
reassembly.pendingBytesHighWatermark,
|
|
611
|
+
stats.pendingBytesHighWatermark
|
|
612
|
+
);
|
|
613
|
+
reassembly.pendingChunkBuffersHighWatermark = Math.max(
|
|
614
|
+
reassembly.pendingChunkBuffersHighWatermark,
|
|
615
|
+
stats.pendingChunkBuffersHighWatermark
|
|
616
|
+
);
|
|
617
|
+
reassembly.droppedFrames += stats.droppedFrames;
|
|
618
|
+
reassembly.completedFrames += stats.completedFrames;
|
|
619
|
+
reassembly.completedPayloadCopyCount += stats.completedPayloadCopyCount;
|
|
620
|
+
reassembly.completedPayloadCopyBytes += stats.completedPayloadCopyBytes;
|
|
621
|
+
reassembly.fullWireDuplicateCopyCount += stats.fullWireDuplicateCopyCount;
|
|
622
|
+
reassembly.binaryChunkHeaderCount += stats.binaryChunkHeaderCount;
|
|
623
|
+
reassembly.legacyChunkHeaderCount += stats.legacyChunkHeaderCount;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
491
626
|
enabled,
|
|
492
627
|
preferred: preferP2p,
|
|
493
628
|
started,
|
|
@@ -498,9 +633,15 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
498
633
|
rendezvousPort,
|
|
499
634
|
rendezvousRefreshMs,
|
|
500
635
|
rendezvousResolvedAddressCount: rendezvousAddresses.size,
|
|
501
|
-
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
502
|
-
sessionCount: sessions.size,
|
|
503
|
-
readySessionCount
|
|
504
|
-
|
|
636
|
+
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
637
|
+
sessionCount: sessions.size,
|
|
638
|
+
readySessionCount,
|
|
639
|
+
resource: {
|
|
640
|
+
...resource,
|
|
641
|
+
indexedSessionCount: sessionsBySessionId.size,
|
|
642
|
+
indexedRouteCount: sessionsByRouteKey.size,
|
|
643
|
+
...reassembly
|
|
644
|
+
}
|
|
645
|
+
};
|
|
505
646
|
}
|
|
506
647
|
}
|