livedesk 0.1.458 → 0.1.459

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/hub/src/server.js CHANGED
@@ -1303,7 +1303,7 @@ function isPrivateHubHostname(value) {
1303
1303
  return !!match && Number(match[2]) >= 16 && Number(match[2]) <= 31;
1304
1304
  }
1305
1305
 
1306
- function isLoopbackAddress(value) {
1306
+ function isLoopbackAddress(value) {
1307
1307
  const address = String(value || '').trim().toLowerCase();
1308
1308
  return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
1309
1309
  }
@@ -1449,6 +1449,31 @@ function normalizeDeviceIds(value) {
1449
1449
  return ids;
1450
1450
  }
1451
1451
 
1452
+ function requireLocalTransportDiagnosticRequest(req, res) {
1453
+ const forwardedFor = String(req.headers['x-forwarded-for'] || '').trim();
1454
+ const forwardedHost = String(req.headers['x-forwarded-host'] || '').trim();
1455
+ const browserOrigin = String(req.headers.origin || '').trim();
1456
+ const browserFetchSite = String(req.headers['sec-fetch-site'] || '').trim();
1457
+ if (!isLoopbackAddress(req.socket?.remoteAddress)
1458
+ || !isLoopbackHostname(requestHostname(req))
1459
+ || forwardedFor
1460
+ || forwardedHost
1461
+ || browserOrigin
1462
+ || browserFetchSite) {
1463
+ res.status(403).json({ ok: false, error: 'transport-diagnostic-loopback-only' });
1464
+ return false;
1465
+ }
1466
+ if (runtimeRole !== 'hub') {
1467
+ res.status(403).json({ ok: false, error: 'role-not-allowed' });
1468
+ return false;
1469
+ }
1470
+ return true;
1471
+ }
1472
+
1473
+ function transportDiagnosticToken(req) {
1474
+ return String(req.headers['x-livedesk-diagnostic-token'] || '').trim();
1475
+ }
1476
+
1452
1477
  function rejectRetiredAgentTaskRequest(req, res) {
1453
1478
  const body = req.body && typeof req.body === 'object' ? req.body : {};
1454
1479
  const approvalLevel = String(body.approvalLevel || '').trim().toLowerCase();
@@ -3093,7 +3118,7 @@ app.post('/api/settings/agent/summary', async (req, res) => {
3093
3118
  }
3094
3119
  });
3095
3120
 
3096
- app.get('/api/remote/status', (_req, res) => {
3121
+ app.get('/api/remote/status', (_req, res) => {
3097
3122
  noStore(res);
3098
3123
  if (runtimeRole !== 'hub') {
3099
3124
  res.status(403).json({ ok: false, error: 'role-not-allowed' });
@@ -3111,9 +3136,134 @@ app.get('/api/remote/status', (_req, res) => {
3111
3136
  agentPackage: '@livedesk/client',
3112
3137
  update: getLiveDeskUpdateStatus()
3113
3138
  });
3114
- });
3115
-
3116
- app.get('/api/runtime/status', (_req, res) => {
3139
+ });
3140
+
3141
+ app.post('/api/remote/diagnostics/transport/start', async (req, res) => {
3142
+ noStore(res);
3143
+ if (!requireLocalTransportDiagnosticRequest(req, res)) return;
3144
+ const requestController = new AbortController();
3145
+ let responseFinished = false;
3146
+ let issuedLease = null;
3147
+ let releaseIssuedLeasePromise = null;
3148
+ const detachResponseLifecycle = () => {
3149
+ res.off('finish', markResponseFinished);
3150
+ res.off('close', abortPendingStart);
3151
+ };
3152
+ const releaseIssuedLease = reason => {
3153
+ if (!issuedLease?.leaseId || !issuedLease?.token) {
3154
+ return releaseIssuedLeasePromise || Promise.resolve(null);
3155
+ }
3156
+ if (!releaseIssuedLeasePromise) {
3157
+ const lease = issuedLease;
3158
+ releaseIssuedLeasePromise = Promise.resolve(remoteHub.stopTransportDiagnostic(
3159
+ lease.leaseId,
3160
+ lease.token,
3161
+ reason
3162
+ )).catch(() => null);
3163
+ }
3164
+ return releaseIssuedLeasePromise;
3165
+ };
3166
+ const markResponseFinished = () => {
3167
+ responseFinished = true;
3168
+ issuedLease = null;
3169
+ detachResponseLifecycle();
3170
+ };
3171
+ const abortPendingStart = () => {
3172
+ if (responseFinished) return;
3173
+ requestController.abort();
3174
+ void releaseIssuedLease('start-response-closed');
3175
+ detachResponseLifecycle();
3176
+ };
3177
+ res.once('finish', markResponseFinished);
3178
+ res.once('close', abortPendingStart);
3179
+ try {
3180
+ const result = await remoteHub.startTransportDiagnostic(
3181
+ String(req.body?.deviceId || ''),
3182
+ Number(req.body?.ttlMs || 0),
3183
+ { signal: requestController.signal });
3184
+ if (result?.leaseId && result?.token) {
3185
+ issuedLease = {
3186
+ leaseId: result.leaseId,
3187
+ token: result.token
3188
+ };
3189
+ }
3190
+ if (requestController.signal.aborted) {
3191
+ await releaseIssuedLease('start-response-closed');
3192
+ return;
3193
+ }
3194
+ res.status(result.ok ? 200 : 409).json(result);
3195
+ } catch (error) {
3196
+ if (requestController.signal.aborted) return;
3197
+ res.status(500).json({
3198
+ ok: false,
3199
+ error: error instanceof Error ? error.message : String(error)
3200
+ });
3201
+ } finally {
3202
+ if (res.writableFinished) {
3203
+ markResponseFinished();
3204
+ } else if (requestController.signal.aborted) {
3205
+ detachResponseLifecycle();
3206
+ }
3207
+ }
3208
+ });
3209
+
3210
+ app.get('/api/remote/diagnostics/transport/:leaseId', (req, res) => {
3211
+ noStore(res);
3212
+ if (!requireLocalTransportDiagnosticRequest(req, res)) return;
3213
+ const token = transportDiagnosticToken(req);
3214
+ if (!token) {
3215
+ res.status(401).json({ ok: false, error: 'transport-diagnostic-token-required' });
3216
+ return;
3217
+ }
3218
+ const result = remoteHub.getTransportDiagnostic(req.params.leaseId, token);
3219
+ res.status(result.ok ? 200 : 404).json(result);
3220
+ });
3221
+
3222
+ app.post('/api/remote/diagnostics/transport/:leaseId/phase', async (req, res) => {
3223
+ noStore(res);
3224
+ if (!requireLocalTransportDiagnosticRequest(req, res)) return;
3225
+ const token = transportDiagnosticToken(req);
3226
+ if (!token) {
3227
+ res.status(401).json({ ok: false, error: 'transport-diagnostic-token-required' });
3228
+ return;
3229
+ }
3230
+ try {
3231
+ const result = await remoteHub.setTransportDiagnosticPhase(
3232
+ req.params.leaseId,
3233
+ token,
3234
+ String(req.body?.phase || ''));
3235
+ res.status(result.ok ? 200 : 409).json(result);
3236
+ } catch (error) {
3237
+ res.status(500).json({
3238
+ ok: false,
3239
+ error: error instanceof Error ? error.message : String(error)
3240
+ });
3241
+ }
3242
+ });
3243
+
3244
+ app.delete('/api/remote/diagnostics/transport/:leaseId', async (req, res) => {
3245
+ noStore(res);
3246
+ if (!requireLocalTransportDiagnosticRequest(req, res)) return;
3247
+ const token = transportDiagnosticToken(req);
3248
+ if (!token) {
3249
+ res.status(401).json({ ok: false, error: 'transport-diagnostic-token-required' });
3250
+ return;
3251
+ }
3252
+ try {
3253
+ const result = await remoteHub.stopTransportDiagnostic(
3254
+ req.params.leaseId,
3255
+ token,
3256
+ String(req.body?.reason || 'runner-complete'));
3257
+ res.status(result.ok ? 200 : 409).json(result);
3258
+ } catch (error) {
3259
+ res.status(500).json({
3260
+ ok: false,
3261
+ error: error instanceof Error ? error.message : String(error)
3262
+ });
3263
+ }
3264
+ });
3265
+
3266
+ app.get('/api/runtime/status', (_req, res) => {
3117
3267
  noStore(res);
3118
3268
  res.json({ ok: true, ...hubRuntimeStatus(runtimeManager, { role: runtimeRole, deviceId: runtimeDeviceId, deviceName: runtimeDeviceName, roleSource: runtimeRoleSource }) });
3119
3269
  });
@@ -36,6 +36,27 @@ function normalizedAddress(value) {
36
36
  return address.startsWith('::ffff:') ? address.slice(7) : address;
37
37
  }
38
38
 
39
+ function isPublicIPv4(value) {
40
+ const address = normalizedAddress(value);
41
+ const octets = address.split('.').map(Number);
42
+ if (octets.length !== 4
43
+ || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
44
+ return false;
45
+ }
46
+ const [a, b] = octets;
47
+ const c = octets[2];
48
+ return a !== 0
49
+ && a !== 10
50
+ && a !== 127
51
+ && !(a === 100 && b >= 64 && b <= 127)
52
+ && !(a === 169 && b === 254)
53
+ && !(a === 172 && b >= 16 && b <= 31)
54
+ && !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
55
+ && !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
56
+ && !(a === 203 && b === 0 && c === 113)
57
+ && !(a >= 224);
58
+ }
59
+
39
60
  export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
40
61
  const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
41
62
  const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
@@ -182,9 +203,21 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
182
203
  const session = [...sessions.values()].find(item => item.sessionId === safeText(message.roomId, 160));
183
204
  if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
184
205
  const address = safeText(message.host, 128);
185
- const port = Number(message.port);
186
- if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
187
- session.peerEndpoint = { address, port };
206
+ const port = Number(message.port);
207
+ if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
208
+ const observedAddress = normalizedAddress(message.observedHost);
209
+ const observedPort = Number(message.observedPort);
210
+ if (observedAddress
211
+ && Number.isInteger(observedPort)
212
+ && observedPort >= 1
213
+ && observedPort <= 65535) {
214
+ session.hubRendezvousEndpoint = {
215
+ address: observedAddress,
216
+ port: observedPort
217
+ };
218
+ session.hubRendezvousObservedAt = Date.now();
219
+ }
220
+ session.peerEndpoint = { address, port };
188
221
  session.lastPeerAt = Date.now();
189
222
  sendPacket(session, 'probe', { punch: true }, Buffer.alloc(0), session.peerEndpoint);
190
223
  emit('UdpPeerEndpointObserved', { deviceId: session.deviceId, endpoint: `${address}:${port}` });
@@ -290,6 +323,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
290
323
  sendControl,
291
324
  clientEndpoint: null,
292
325
  peerEndpoint: null,
326
+ hubRendezvousEndpoint: null,
327
+ hubRendezvousObservedAt: 0,
293
328
  replay: new UdpReplayWindow(),
294
329
  reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
295
330
  ready: false,
@@ -327,7 +362,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
327
362
  emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
328
363
  }
329
364
 
330
- function getDeviceStatus(deviceId) {
365
+ function getDeviceStatus(deviceId) {
331
366
  const session = sessions.get(safeText(deviceId, 160));
332
367
  if (!session) return { enabled, state: enabled ? 'disabled' : 'tcp-only', ready: false };
333
368
  return {
@@ -340,7 +375,72 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
340
375
  framesReceived: session.framesReceived,
341
376
  ...session.reassembler.getStats()
342
377
  };
343
- }
378
+ }
379
+
380
+ async function refreshDeviceDiagnosticNetwork(deviceId, { timeoutMs = 3_000 } = {}) {
381
+ const session = sessions.get(safeText(deviceId, 160));
382
+ if (!session || !started || !rendezvousHost) return false;
383
+ const requestedAt = Date.now();
384
+ await resolveRendezvousAddresses();
385
+ if (!sendRendezvousRegister(session)) return false;
386
+ const deadline = requestedAt + numberEnv(timeoutMs, 250, 5_000, 3_000);
387
+ while (Date.now() < deadline) {
388
+ if (session.hubRendezvousObservedAt >= requestedAt) return true;
389
+ await new Promise(resolve => {
390
+ const timer = setTimeout(resolve, 40);
391
+ timer.unref?.();
392
+ });
393
+ }
394
+ return false;
395
+ }
396
+
397
+ function getDeviceDiagnosticNetwork(deviceId, { salt = '' } = {}) {
398
+ const session = sessions.get(safeText(deviceId, 160));
399
+ const clientAddress = normalizedAddress(session?.clientEndpoint?.address);
400
+ const normalizedHubAddress = normalizedAddress(session?.hubRendezvousEndpoint?.address);
401
+ const hubAddressObservedAt = session?.hubRendezvousObservedAt
402
+ ? new Date(session.hubRendezvousObservedAt).toISOString()
403
+ : '';
404
+ const digest = value => value
405
+ ? crypto.createHash('sha256')
406
+ .update(`${safeText(salt, 160)}:${value}`)
407
+ .digest('hex')
408
+ .slice(0, 16)
409
+ : '';
410
+ if (!session?.ready || !clientAddress) {
411
+ return {
412
+ evidence: 'authenticated-udp-endpoint-unavailable',
413
+ differentPublicEgress: null,
414
+ clientAddressHash: '',
415
+ hubAddressHash: digest(normalizedHubAddress),
416
+ hubAddressObservedAt,
417
+ hubEndpointSource: normalizedHubAddress
418
+ ? 'trusted-rendezvous-observation'
419
+ : 'unavailable'
420
+ };
421
+ }
422
+ const clientAddressPublic = isPublicIPv4(clientAddress);
423
+ const hubAddressPublic = isPublicIPv4(normalizedHubAddress);
424
+ return {
425
+ evidence: normalizedHubAddress
426
+ ? 'authenticated-udp-endpoint-rendezvous-observed'
427
+ : 'authenticated-udp-endpoint-hub-egress-unavailable',
428
+ differentPublicEgress: clientAddressPublic && hubAddressPublic
429
+ ? clientAddress !== normalizedHubAddress
430
+ : null,
431
+ clientAddressHash: digest(clientAddress),
432
+ hubAddressHash: digest(normalizedHubAddress),
433
+ clientAddressPublic,
434
+ hubAddressPublic,
435
+ endpointAuthenticated: true,
436
+ udpReady: session.ready === true,
437
+ observedAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
438
+ hubAddressObservedAt,
439
+ hubEndpointSource: normalizedHubAddress
440
+ ? 'trusted-rendezvous-observation'
441
+ : 'unavailable'
442
+ };
443
+ }
344
444
 
345
445
  async function close() {
346
446
  stopRendezvousRefreshTimer();
@@ -354,9 +454,11 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
354
454
  start,
355
455
  close,
356
456
  registerDevice,
357
- unregisterDevice,
358
- getDeviceStatus,
359
- getStatus,
457
+ unregisterDevice,
458
+ getDeviceStatus,
459
+ refreshDeviceDiagnosticNetwork,
460
+ getDeviceDiagnosticNetwork,
461
+ getStatus,
360
462
  setFrameHandler: handler => { frameHandler = typeof handler === 'function' ? handler : () => {}; },
361
463
  setEventHandler: handler => { eventHandler = typeof handler === 'function' ? handler : () => {}; }
362
464
  };
@@ -300,7 +300,9 @@ export function createUdpRendezvousServer({
300
300
  roomId,
301
301
  role: 'client',
302
302
  host: client.address,
303
- port: client.port
303
+ port: client.port,
304
+ observedHost: hub.address,
305
+ observedPort: hub.port
304
306
  }, hub.address, hub.port);
305
307
  send({
306
308
  type: 'rendezvous.peer',
@@ -308,7 +310,9 @@ export function createUdpRendezvousServer({
308
310
  roomId,
309
311
  role: 'hub',
310
312
  host: hub.address,
311
- port: hub.port
313
+ port: hub.port,
314
+ observedHost: client.address,
315
+ observedPort: client.port
312
316
  }, client.address, client.port);
313
317
  }
314
318
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.458",
4
- "livedeskClientVersion": "0.1.212",
3
+ "version": "0.1.459",
4
+ "livedeskClientVersion": "0.1.213",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "README.md"
22
22
  ],
23
23
  "scripts": {
24
- "check": "node --check bin/livedesk.js && node --check diagnostics/livedesk-mode3-capture-smoke.mjs && node --check diagnostics/livedesk-mac-physical-gate-core.mjs && node --check diagnostics/livedesk-mac-physical-isolation.mjs && node --check scripts/sync-web-dist.js",
24
+ "check": "node --check bin/livedesk.js && node --check diagnostics/livedesk-mode3-capture-smoke.mjs && node --check diagnostics/livedesk-mac-physical-gate-core.mjs && node --check diagnostics/livedesk-mac-physical-isolation.mjs && node --check diagnostics/livedesk-transport-physical-core.mjs && node --check diagnostics/livedesk-transport-physical.mjs && node --check scripts/sync-web-dist.js",
25
25
  "build": "node scripts/sync-web-dist.js",
26
26
  "prepack": "node scripts/sync-web-dist.js",
27
27
  "pack:dry": "npm pack --dry-run"
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.416",
54
- "@livedesk/fast-osx-arm64": "0.1.416",
55
- "@livedesk/fast-osx-x64": "0.1.416",
56
- "@livedesk/fast-win-x64": "0.1.416"
53
+ "@livedesk/fast-linux-x64": "0.1.417",
54
+ "@livedesk/fast-osx-arm64": "0.1.417",
55
+ "@livedesk/fast-osx-x64": "0.1.417",
56
+ "@livedesk/fast-win-x64": "0.1.417"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"