@livedesk/hub 0.1.32 → 0.1.33

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/src/remote-hub.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import net from 'net';
2
2
  import os from 'os';
3
- import crypto from 'crypto';
4
- import { isSecureHandshakeStart } from '@livedesk/runtime-core';
5
- import { createDeviceCredentialAuthority } from './security/device-credential-authority.js';
6
- import { createHubRelayControl, createSecureHubRelayControl } from './transport/relay-hub-control.js';
7
- import { createSecureDirectAcceptor } from './transport/secure-direct-acceptor.js';
3
+ import crypto from 'crypto';
4
+ import { isSecureHandshakeStart } from '@livedesk/runtime-core';
5
+ import { createDeviceCredentialAuthority } from './security/device-credential-authority.js';
6
+ import { createHubRelayControl, createSecureHubRelayControl } from './transport/relay-hub-control.js';
7
+ import { createSecureDirectAcceptor } from './transport/secure-direct-acceptor.js';
8
8
  import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
9
9
  import {
10
10
  BoundedSegmentedBuffer,
@@ -35,10 +35,10 @@ const MAX_AGENT_TCP_BINARY_PACKET_BYTES = TCP_BINARY_FRAME_HEADER_BYTES
35
35
  const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
36
36
  + MAX_AGENT_BINARY_META_BYTES
37
37
  + MAX_AGENT_BINARY_PAYLOAD_BYTES;
38
- const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
39
- + MAX_AGENT_TCP_BINARY_PACKET_BYTES
40
- + (256 * 1024);
41
- const MAX_AGENT_MESSAGES_PER_SECOND = 240;
38
+ const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
39
+ + MAX_AGENT_TCP_BINARY_PACKET_BYTES
40
+ + (256 * 1024);
41
+ const MAX_AGENT_MESSAGES_PER_SECOND = 240;
42
42
  const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
43
43
  const MAX_AGENT_TASK_CHARS = 4000;
44
44
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
@@ -110,7 +110,7 @@ const MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS = Object.freeze([
110
110
  'handoffOldestFrameAgeMs'
111
111
  ]);
112
112
 
113
- export function hasCompleteMacNativeResourceTelemetry(frame) {
113
+ export function hasCompleteMacNativeResourceTelemetry(frame) {
114
114
  return Number(frame?.nativeCaptureTelemetryVersion) >= MAC_NATIVE_RESOURCE_TELEMETRY_VERSION
115
115
  && MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS.every(field =>
116
116
  frame?.[field] !== null
@@ -828,61 +828,61 @@ function isPrivateIPv4(value) {
828
828
  || (parts[0] === 192 && parts[1] === 168);
829
829
  }
830
830
 
831
- function isPrivateNetworkAddress(value) {
831
+ function isPrivateNetworkAddress(value) {
832
832
  const address = String(value || '').replace(/^::ffff:/i, '').trim().toLowerCase();
833
833
  return isLoopbackHost(address)
834
834
  || isPrivateIPv4(address)
835
835
  || address.startsWith('fe80:')
836
836
  || address.startsWith('fc')
837
- || address.startsWith('fd');
838
- }
839
-
840
- export function createRemoteMessageRateGate({
841
- limit = MAX_AGENT_MESSAGES_PER_SECOND,
842
- windowMs = 1_000,
843
- now = () => Date.now()
844
- } = {}) {
845
- const boundedLimit = Math.max(1, Math.min(10_000, Math.floor(Number(limit) || MAX_AGENT_MESSAGES_PER_SECOND)));
846
- const boundedWindowMs = Math.max(100, Math.min(60_000, Math.floor(Number(windowMs) || 1_000)));
847
- let windowStartedAt = Number(now());
848
- let count = 0;
849
- return Object.freeze({
850
- consume() {
851
- const current = Number(now());
852
- if (!Number.isFinite(current) || current - windowStartedAt >= boundedWindowMs || current < windowStartedAt) {
853
- windowStartedAt = current;
854
- count = 0;
855
- }
856
- count += 1;
857
- return count <= boundedLimit;
858
- },
859
- getStatus: () => ({ count, limit: boundedLimit, windowMs: boundedWindowMs, windowStartedAt })
860
- });
861
- }
862
-
863
- export function evaluateRemoteConnectionPolicy({
864
- remoteAddress = '',
865
- relayConnection = false,
866
- encrypted = false,
867
- allowLegacyPlaintextForTests = false,
868
- policy = {}
869
- } = {}) {
870
- const address = String(remoteAddress || '').replace(/^::ffff:/i, '').trim().toLowerCase();
871
- if (policy.requireEncryptedConnections === true
872
- && encrypted !== true
873
- && allowLegacyPlaintextForTests !== true) {
874
- return 'encrypted-connection-required';
875
- }
876
- if (relayConnection === true || !isPrivateNetworkAddress(address)) {
877
- return policy.allowInternetConnections === false
878
- ? 'internet-connections-blocked-by-settings'
879
- : '';
880
- }
881
- if (!isLoopbackHost(address) && policy.allowLanConnections === false) {
882
- return 'lan-connections-blocked-by-settings';
883
- }
884
- return '';
885
- }
837
+ || address.startsWith('fd');
838
+ }
839
+
840
+ export function createRemoteMessageRateGate({
841
+ limit = MAX_AGENT_MESSAGES_PER_SECOND,
842
+ windowMs = 1_000,
843
+ now = () => Date.now()
844
+ } = {}) {
845
+ const boundedLimit = Math.max(1, Math.min(10_000, Math.floor(Number(limit) || MAX_AGENT_MESSAGES_PER_SECOND)));
846
+ const boundedWindowMs = Math.max(100, Math.min(60_000, Math.floor(Number(windowMs) || 1_000)));
847
+ let windowStartedAt = Number(now());
848
+ let count = 0;
849
+ return Object.freeze({
850
+ consume() {
851
+ const current = Number(now());
852
+ if (!Number.isFinite(current) || current - windowStartedAt >= boundedWindowMs || current < windowStartedAt) {
853
+ windowStartedAt = current;
854
+ count = 0;
855
+ }
856
+ count += 1;
857
+ return count <= boundedLimit;
858
+ },
859
+ getStatus: () => ({ count, limit: boundedLimit, windowMs: boundedWindowMs, windowStartedAt })
860
+ });
861
+ }
862
+
863
+ export function evaluateRemoteConnectionPolicy({
864
+ remoteAddress = '',
865
+ relayConnection = false,
866
+ encrypted = false,
867
+ allowLegacyPlaintextForTests = false,
868
+ policy = {}
869
+ } = {}) {
870
+ const address = String(remoteAddress || '').replace(/^::ffff:/i, '').trim().toLowerCase();
871
+ if (policy.requireEncryptedConnections === true
872
+ && encrypted !== true
873
+ && allowLegacyPlaintextForTests !== true) {
874
+ return 'encrypted-connection-required';
875
+ }
876
+ if (relayConnection === true || !isPrivateNetworkAddress(address)) {
877
+ return policy.allowInternetConnections === false
878
+ ? 'internet-connections-blocked-by-settings'
879
+ : '';
880
+ }
881
+ if (!isLoopbackHost(address) && policy.allowLanConnections === false) {
882
+ return 'lan-connections-blocked-by-settings';
883
+ }
884
+ return '';
885
+ }
886
886
 
887
887
  function readCpuTimes() {
888
888
  let idle = 0;
@@ -1206,37 +1206,37 @@ function getRetiredAgentTaskError(options = {}) {
1206
1206
  : '';
1207
1207
  }
1208
1208
 
1209
- const SUPPORTED_AGENT_OPERATIONS = new Set([
1210
- 'system.health',
1211
- 'gpu.status',
1212
- 'disk.status',
1213
- 'process.list',
1214
- 'service.status',
1215
- 'diagnostics.collect',
1216
- 'file.read',
1217
- 'file.list',
1218
- 'network.status',
1219
- 'logs.collect'
1220
- ]);
1221
-
1222
- const RETIRED_AGENT_MUTATING_OPERATIONS = new Set([
1223
- 'process.control',
1224
- 'service.control',
1225
- 'application.launch',
1226
- 'application.close',
1227
- 'file.write',
1228
- 'file.delete',
1229
- 'command.run',
1230
- 'script.run',
1231
- 'software.install',
1232
- 'system.power',
1233
- 'system.configure'
1234
- ]);
1235
-
1236
- function normalizeAgentOperation(value) {
1237
- const operation = safeString(value, 80).toLowerCase();
1238
- return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
1239
- }
1209
+ const SUPPORTED_AGENT_OPERATIONS = new Set([
1210
+ 'system.health',
1211
+ 'gpu.status',
1212
+ 'disk.status',
1213
+ 'process.list',
1214
+ 'service.status',
1215
+ 'diagnostics.collect',
1216
+ 'file.read',
1217
+ 'file.list',
1218
+ 'network.status',
1219
+ 'logs.collect'
1220
+ ]);
1221
+
1222
+ const RETIRED_AGENT_MUTATING_OPERATIONS = new Set([
1223
+ 'process.control',
1224
+ 'service.control',
1225
+ 'application.launch',
1226
+ 'application.close',
1227
+ 'file.write',
1228
+ 'file.delete',
1229
+ 'command.run',
1230
+ 'script.run',
1231
+ 'software.install',
1232
+ 'system.power',
1233
+ 'system.configure'
1234
+ ]);
1235
+
1236
+ function normalizeAgentOperation(value) {
1237
+ const operation = safeString(value, 80).toLowerCase();
1238
+ return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
1239
+ }
1240
1240
 
1241
1241
  const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
1242
1242
  'monitorIndex',
@@ -1402,7 +1402,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
1402
1402
  return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
1403
1403
  }
1404
1404
 
1405
- function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1405
+ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1406
1406
  if (!frame) {
1407
1407
  return null;
1408
1408
  }
@@ -1424,27 +1424,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1424
1424
  serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
1425
1425
  }
1426
1426
 
1427
- return serialized;
1428
- }
1429
-
1430
- function serializeActiveLiveStream(activeLiveStream, deviceId) {
1431
- if (!activeLiveStream) {
1432
- return null;
1433
- }
1434
-
1435
- return {
1436
- ...activeLiveStream,
1437
- // The active descriptor is status metadata. Keep its internal frame
1438
- // owner intact for immediate delivery, but never duplicate encoded
1439
- // bytes or its bearer token into React/status snapshots.
1440
- latestFrame: serializeRemoteFrame(
1441
- activeLiveStream.latestFrame,
1442
- deviceId,
1443
- 'live',
1444
- { includeDataUrl: false }
1445
- )
1446
- };
1447
- }
1427
+ return serialized;
1428
+ }
1429
+
1430
+ function serializeActiveLiveStream(activeLiveStream, deviceId) {
1431
+ if (!activeLiveStream) {
1432
+ return null;
1433
+ }
1434
+
1435
+ return {
1436
+ ...activeLiveStream,
1437
+ // The active descriptor is status metadata. Keep its internal frame
1438
+ // owner intact for immediate delivery, but never duplicate encoded
1439
+ // bytes or its bearer token into React/status snapshots.
1440
+ latestFrame: serializeRemoteFrame(
1441
+ activeLiveStream.latestFrame,
1442
+ deviceId,
1443
+ 'live',
1444
+ { includeDataUrl: false }
1445
+ )
1446
+ };
1447
+ }
1448
1448
 
1449
1449
  function getRecentFrameCache(device, frameKind) {
1450
1450
  if (!device) {
@@ -1635,7 +1635,7 @@ function serializeDevice(device, options = {}) {
1635
1635
  },
1636
1636
  latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
1637
1637
  latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
1638
- activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1638
+ activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1639
1639
  liveCapturePause: device.liveCapturePause
1640
1640
  ? {
1641
1641
  token: device.liveCapturePause.token,
@@ -2242,7 +2242,7 @@ export function createRemoteHub(options = {}) {
2242
2242
  : null;
2243
2243
  const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
2244
2244
 
2245
- function getDevicePolicy(device) {
2245
+ function getDevicePolicy(device) {
2246
2246
  try {
2247
2247
  return getEffectiveDevicePolicy({
2248
2248
  deviceId: device?.deviceId || '',
@@ -2250,18 +2250,18 @@ export function createRemoteHub(options = {}) {
2250
2250
  }) || {};
2251
2251
  } catch {
2252
2252
  return {};
2253
- }
2254
- }
2255
-
2256
- function connectionPolicyError(socket, policy = {}) {
2257
- return evaluateRemoteConnectionPolicy({
2258
- remoteAddress: socket?.remoteAddress || '',
2259
- relayConnection: socket?.__liveDeskRelayControl === true,
2260
- encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
2261
- allowLegacyPlaintextForTests,
2262
- policy
2263
- });
2264
- }
2253
+ }
2254
+ }
2255
+
2256
+ function connectionPolicyError(socket, policy = {}) {
2257
+ return evaluateRemoteConnectionPolicy({
2258
+ remoteAddress: socket?.remoteAddress || '',
2259
+ relayConnection: socket?.__liveDeskRelayControl === true,
2260
+ encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
2261
+ allowLegacyPlaintextForTests,
2262
+ policy
2263
+ });
2264
+ }
2265
2265
 
2266
2266
  function policyError(device, permission = '', command = '') {
2267
2267
  const policy = getDevicePolicy(device);
@@ -2311,83 +2311,83 @@ export function createRemoteHub(options = {}) {
2311
2311
  const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
2312
2312
  const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
2313
2313
  const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
2314
- const configuredEnrollmentToken = safeString(
2315
- options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
2316
- 256);
2317
- const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
2318
- || isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
2319
- || env.LIVEDESK_TEST_MODE === '1';
2320
- const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
2321
- && Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
2322
- && Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
2323
- ? configuredEnrollmentToken
2324
- : '';
2325
- let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
2326
- ? configuredEnrollmentToken
2327
- : canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
2328
- const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
2329
- dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
2330
- });
2331
- const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
2332
- ? options.getSecurityIdentity
2333
- : () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
2334
- udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
2335
- const identity = getSecurityIdentity() || {};
2336
- return deviceCredentialAuthority.issueRendezvousProof({
2337
- roomId,
2338
- deviceId,
2339
- role,
2340
- ttlMs,
2341
- accountId: safeString(identity.accountId, 128)
2342
- });
2343
- });
2344
- let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
2314
+ const configuredEnrollmentToken = safeString(
2315
+ options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
2316
+ 256);
2317
+ const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
2318
+ || isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
2319
+ || env.LIVEDESK_TEST_MODE === '1';
2320
+ const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
2321
+ && Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
2322
+ && Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
2323
+ ? configuredEnrollmentToken
2324
+ : '';
2325
+ let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
2326
+ ? configuredEnrollmentToken
2327
+ : canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
2328
+ const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
2329
+ dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
2330
+ });
2331
+ const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
2332
+ ? options.getSecurityIdentity
2333
+ : () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
2334
+ udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
2335
+ const identity = getSecurityIdentity() || {};
2336
+ return deviceCredentialAuthority.issueRendezvousProof({
2337
+ roomId,
2338
+ deviceId,
2339
+ role,
2340
+ ttlMs,
2341
+ accountId: safeString(identity.accountId, 128)
2342
+ });
2343
+ });
2344
+ let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
2345
2345
  ? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
2346
2346
  : generatePairingPin();
2347
- let pairingPinUpdatedAt = new Date().toISOString();
2348
- const consumeEnrollmentToken = expected => {
2349
- if (!timingSafeStringEqual(expected, pairToken)) return false;
2350
- pairToken = crypto.randomBytes(32).toString('base64url');
2351
- pairingPin = generatePairingPin();
2352
- pairingPinUpdatedAt = new Date().toISOString();
2353
- emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
2354
- pairingPinUpdatedAt,
2355
- hubId: deviceCredentialAuthority.hubId
2356
- });
2357
- return true;
2358
- };
2359
- const relayControl = options.relayControl && typeof options.relayControl === 'object'
2360
- ? options.relayControl
2361
- : allowLegacyPlaintextForTests
2362
- ? createHubRelayControl({
2363
- env,
2364
- pairToken,
2365
- logEvent,
2366
- logWarn,
2367
- onPeerSocket: socket => handleTcpAgentSocket(socket)
2368
- })
2369
- : createSecureHubRelayControl({
2370
- env,
2371
- authority: deviceCredentialAuthority,
2372
- getIdentity: () => getSecurityIdentity() || {},
2373
- getEnrollmentToken: () => pairToken,
2374
- consumeEnrollmentToken,
2375
- logEvent,
2376
- logWarn,
2377
- onPeerSocket: socket => handleTcpAgentSocket(socket)
2378
- });
2379
- const secureDirectAcceptor = createSecureDirectAcceptor({
2380
- authority: deviceCredentialAuthority,
2381
- getIdentity: () => getSecurityIdentity() || {},
2382
- getEnrollmentToken: () => pairToken,
2383
- consumeEnrollmentToken,
2384
- onSecureSocket: socket => handleTcpAgentSocket(socket),
2385
- onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
2386
- ...audit,
2387
- remoteHub: getStatus({ includeSecrets: false })
2388
- }),
2389
- logWarn
2390
- });
2347
+ let pairingPinUpdatedAt = new Date().toISOString();
2348
+ const consumeEnrollmentToken = expected => {
2349
+ if (!timingSafeStringEqual(expected, pairToken)) return false;
2350
+ pairToken = crypto.randomBytes(32).toString('base64url');
2351
+ pairingPin = generatePairingPin();
2352
+ pairingPinUpdatedAt = new Date().toISOString();
2353
+ emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
2354
+ pairingPinUpdatedAt,
2355
+ hubId: deviceCredentialAuthority.hubId
2356
+ });
2357
+ return true;
2358
+ };
2359
+ const relayControl = options.relayControl && typeof options.relayControl === 'object'
2360
+ ? options.relayControl
2361
+ : allowLegacyPlaintextForTests
2362
+ ? createHubRelayControl({
2363
+ env,
2364
+ pairToken,
2365
+ logEvent,
2366
+ logWarn,
2367
+ onPeerSocket: socket => handleTcpAgentSocket(socket)
2368
+ })
2369
+ : createSecureHubRelayControl({
2370
+ env,
2371
+ authority: deviceCredentialAuthority,
2372
+ getIdentity: () => getSecurityIdentity() || {},
2373
+ getEnrollmentToken: () => pairToken,
2374
+ consumeEnrollmentToken,
2375
+ logEvent,
2376
+ logWarn,
2377
+ onPeerSocket: socket => handleTcpAgentSocket(socket)
2378
+ });
2379
+ const secureDirectAcceptor = createSecureDirectAcceptor({
2380
+ authority: deviceCredentialAuthority,
2381
+ getIdentity: () => getSecurityIdentity() || {},
2382
+ getEnrollmentToken: () => pairToken,
2383
+ consumeEnrollmentToken,
2384
+ onSecureSocket: socket => handleTcpAgentSocket(socket),
2385
+ onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
2386
+ ...audit,
2387
+ remoteHub: getStatus({ includeSecrets: false })
2388
+ }),
2389
+ logWarn
2390
+ });
2391
2391
  const duplicateDeviceLogThrottleMs = clampNumber(
2392
2392
  env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
2393
2393
  5000,
@@ -2412,8 +2412,8 @@ export function createRemoteHub(options = {}) {
2412
2412
  const transportDiagnostics = new Map();
2413
2413
  const transportDiagnosticStartingDevices = new Set();
2414
2414
  const duplicateDeviceLogAt = new Map();
2415
- let server = null;
2416
- let controlIdleSweepTimer = null;
2415
+ let server = null;
2416
+ let controlIdleSweepTimer = null;
2417
2417
  let started = false;
2418
2418
  let boundPort = requestedPort;
2419
2419
  let lastError = '';
@@ -2735,8 +2735,8 @@ export function createRemoteHub(options = {}) {
2735
2735
  host,
2736
2736
  agentHost: routeInfo.host || getAnnouncedHost(),
2737
2737
  port: boundPort || requestedPort,
2738
- protocol: 'secure-record-v1',
2739
- protocolVersion: REMOTE_PROTOCOL_VERSION,
2738
+ protocol: 'secure-record-v1',
2739
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
2740
2740
  agentProtocol: REMOTE_AGENT_PROTOCOL,
2741
2741
  frameProtocol: buildRemoteFrameProtocolDescriptor(),
2742
2742
  frameModes: getSupportedRemoteFrameModeProfiles(),
@@ -2752,18 +2752,18 @@ export function createRemoteHub(options = {}) {
2752
2752
  agentEndpointCandidates: routeInfo.candidates,
2753
2753
  agentEndpointCandidateDetails: routeInfo.candidateDetails,
2754
2754
  pairToken: includeSecrets ? pairToken : undefined,
2755
- pairTokenPreview: maskToken(pairToken),
2756
- pairingPin: includeSecrets ? pairingPin : '',
2757
- pairingPinUpdatedAt,
2758
- security: {
2759
- direct: secureDirectAcceptor.getStatus(),
2760
- hubId: deviceCredentialAuthority.hubId,
2761
- hubPublicKeyFingerprint: crypto.createHash('sha256')
2762
- .update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
2763
- .digest('hex'),
2764
- enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
2765
- legacyPlaintextForTests: allowLegacyPlaintextForTests
2766
- },
2755
+ pairTokenPreview: maskToken(pairToken),
2756
+ pairingPin: includeSecrets ? pairingPin : '',
2757
+ pairingPinUpdatedAt,
2758
+ security: {
2759
+ direct: secureDirectAcceptor.getStatus(),
2760
+ hubId: deviceCredentialAuthority.hubId,
2761
+ hubPublicKeyFingerprint: crypto.createHash('sha256')
2762
+ .update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
2763
+ .digest('hex'),
2764
+ enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
2765
+ legacyPlaintextForTests: allowLegacyPlaintextForTests
2766
+ },
2767
2767
  deviceCount: devices.size,
2768
2768
  connectedDeviceCount: connectedDevices,
2769
2769
  canvasDeviceListMode: 'all-devices',
@@ -4023,22 +4023,22 @@ export function createRemoteHub(options = {}) {
4023
4023
  }
4024
4024
  }
4025
4025
 
4026
- function closeInputSocket(device, reason = 'input-socket-closed') {
4026
+ function closeInputSocket(device, reason = 'input-socket-closed') {
4027
4027
  const socket = device?.inputSocket;
4028
4028
  if (!socket) {
4029
4029
  return;
4030
4030
  }
4031
-
4032
- const writeOwner = device.inputWriteOwner;
4033
- if (device.controlOwnerConnectionId) {
4034
- endControlSession(
4035
- device,
4036
- device.controlOwnerConnectionId,
4037
- reason,
4038
- writeOwner,
4039
- writeOwner?.getBindingKey?.() || '');
4040
- }
4041
- device.inputSocket = null;
4031
+
4032
+ const writeOwner = device.inputWriteOwner;
4033
+ if (device.controlOwnerConnectionId) {
4034
+ endControlSession(
4035
+ device,
4036
+ device.controlOwnerConnectionId,
4037
+ reason,
4038
+ writeOwner,
4039
+ writeOwner?.getBindingKey?.() || '');
4040
+ }
4041
+ device.inputSocket = null;
4042
4042
  device.inputWriteOwner = null;
4043
4043
  device.inputSocketConnectionId = '';
4044
4044
  device.inputOwnerConnectionId = '';
@@ -4068,17 +4068,17 @@ export function createRemoteHub(options = {}) {
4068
4068
  if (!device || device.inputSocket !== socket) {
4069
4069
  return;
4070
4070
  }
4071
-
4072
- const writeOwner = device.inputWriteOwner;
4073
- if (device.controlOwnerConnectionId) {
4074
- endControlSession(
4075
- device,
4076
- device.controlOwnerConnectionId,
4077
- reason,
4078
- null,
4079
- '');
4080
- }
4081
- device.inputSocket = null;
4071
+
4072
+ const writeOwner = device.inputWriteOwner;
4073
+ if (device.controlOwnerConnectionId) {
4074
+ endControlSession(
4075
+ device,
4076
+ device.controlOwnerConnectionId,
4077
+ reason,
4078
+ null,
4079
+ '');
4080
+ }
4081
+ device.inputSocket = null;
4082
4082
  device.inputWriteOwner = null;
4083
4083
  device.inputSocketConnectionId = '';
4084
4084
  device.inputOwnerConnectionId = '';
@@ -4510,11 +4510,11 @@ export function createRemoteHub(options = {}) {
4510
4510
  inputWriteOwner: null,
4511
4511
  inputSocketConnectionId: '',
4512
4512
  inputOwnerConnectionId: '',
4513
- inputOwnerBindingKey: '',
4514
- controlOwnerSessionId: '',
4515
- controlOwnerConnectionId: '',
4516
- controlOwnerStartedAtMs: 0,
4517
- controlOwnerLastInputAtMs: 0,
4513
+ inputOwnerBindingKey: '',
4514
+ controlOwnerSessionId: '',
4515
+ controlOwnerConnectionId: '',
4516
+ controlOwnerStartedAtMs: 0,
4517
+ controlOwnerLastInputAtMs: 0,
4518
4518
  frameSocket: null,
4519
4519
  audioSocket: null,
4520
4520
  fileSocket: null,
@@ -7435,14 +7435,14 @@ export function createRemoteHub(options = {}) {
7435
7435
  return;
7436
7436
  }
7437
7437
 
7438
- if (!state.authenticated) {
7439
- const securityContext = socket.__liveDeskSecurityContext;
7440
- const secureDeviceAuthenticated = securityContext?.authenticated === true
7441
- && securityContext?.encrypted === true
7442
- && safeString(securityContext.accountId, 128)
7443
- && safeString(securityContext.hubId, 128)
7444
- && safeString(securityContext.deviceId, 128);
7445
- if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7438
+ if (!state.authenticated) {
7439
+ const securityContext = socket.__liveDeskSecurityContext;
7440
+ const secureDeviceAuthenticated = securityContext?.authenticated === true
7441
+ && securityContext?.encrypted === true
7442
+ && safeString(securityContext.accountId, 128)
7443
+ && safeString(securityContext.hubId, 128)
7444
+ && safeString(securityContext.deviceId, 128);
7445
+ if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7446
7446
  message = {
7447
7447
  ...message,
7448
7448
  capabilities: {
@@ -7452,14 +7452,14 @@ export function createRemoteHub(options = {}) {
7452
7452
  dedicatedInputChannel: false
7453
7453
  }
7454
7454
  };
7455
- }
7456
- if (message.type === 'slot.assign') {
7457
- if (!secureDeviceAuthenticated
7458
- || safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7459
- writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
7460
- socket.end();
7461
- return;
7462
- }
7455
+ }
7456
+ if (message.type === 'slot.assign') {
7457
+ if (!secureDeviceAuthenticated
7458
+ || safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7459
+ writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
7460
+ socket.end();
7461
+ return;
7462
+ }
7463
7463
  const result = assignDeviceSlot(
7464
7464
  message.deviceId || message.DeviceId,
7465
7465
  message.slotNumber ?? message.slot ?? message.SlotNumber);
@@ -7473,54 +7473,57 @@ export function createRemoteHub(options = {}) {
7473
7473
  return;
7474
7474
  }
7475
7475
 
7476
- if (secureDeviceAuthenticated) {
7477
- if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7478
- writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
7479
- socket.destroy();
7480
- return;
7481
- }
7482
- const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
7483
- if (helloChannel !== securityContext.channel) {
7484
- writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
7485
- socket.destroy();
7486
- return;
7487
- }
7488
- } else {
7489
- const legacyAllowed = socket.__liveDeskRelayControl === true
7490
- || socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
7491
- if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
7492
- writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
7493
- socket.destroy();
7494
- return;
7495
- }
7496
- }
7497
-
7498
- const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
7499
- const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
7500
- ? message.capabilities
7501
- : {};
7502
- const incomingPolicy = getWelcomeDevicePolicy({
7503
- deviceId: incomingDeviceId,
7504
- capabilities: incomingCapabilities
7505
- }) || {};
7506
- const connectionDenied = connectionPolicyError(socket, incomingPolicy);
7507
- if (connectionDenied) {
7508
- writeJsonLine(socket, { type: 'error', error: connectionDenied });
7509
- emitEvent('RemoteSecurityPolicyAudit', {
7510
- result: 'rejected',
7511
- reason: connectionDenied,
7512
- deviceId: incomingDeviceId,
7513
- sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
7514
- accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
7515
- hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
7516
- transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7517
- remoteAddress: safeString(socket.remoteAddress, 256)
7518
- });
7519
- socket.destroy();
7520
- return;
7521
- }
7522
-
7523
- const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
7476
+ if (secureDeviceAuthenticated) {
7477
+ if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7478
+ writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
7479
+ socket.destroy();
7480
+ return;
7481
+ }
7482
+ const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
7483
+ if (helloChannel !== securityContext.channel) {
7484
+ writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
7485
+ socket.destroy();
7486
+ return;
7487
+ }
7488
+ } else {
7489
+ const legacyAllowed = socket.__liveDeskRelayControl === true
7490
+ || socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
7491
+ if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
7492
+ writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
7493
+ socket.destroy();
7494
+ return;
7495
+ }
7496
+ }
7497
+
7498
+ const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
7499
+ const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
7500
+ ? message.capabilities
7501
+ : {};
7502
+ const incomingPolicy = getWelcomeDevicePolicy({
7503
+ deviceId: incomingDeviceId,
7504
+ capabilities: incomingCapabilities
7505
+ }) || {};
7506
+ const connectionDenied = connectionPolicyError(socket, incomingPolicy);
7507
+ if (connectionDenied) {
7508
+ const deniedTransport = socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp';
7509
+ const deniedRemoteAddress = safeString(socket.remoteAddress, 256) || 'unknown';
7510
+ logWarn('security', 'Remote connection rejected reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress);
7511
+ writeJsonLine(socket, { type: 'error', error: connectionDenied });
7512
+ emitEvent('RemoteSecurityPolicyAudit', {
7513
+ result: 'rejected',
7514
+ reason: connectionDenied,
7515
+ deviceId: incomingDeviceId,
7516
+ sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
7517
+ accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
7518
+ hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
7519
+ transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7520
+ remoteAddress: safeString(socket.remoteAddress, 256)
7521
+ });
7522
+ socket.destroy();
7523
+ return;
7524
+ }
7525
+
7526
+ const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
7524
7527
  if (channel === 'input') {
7525
7528
  const device = attachInputSocket(socket, message);
7526
7529
  if (!device) {
@@ -7687,27 +7690,27 @@ export function createRemoteHub(options = {}) {
7687
7690
  }
7688
7691
  }
7689
7692
 
7690
- function enforceAgentMessageRate(socket, state, channel = 'control') {
7691
- if (state.messageRateGate.consume()) return true;
7692
- const device = state.device;
7693
- emitEvent('RemoteAbuseDefense', {
7694
- result: 'rejected',
7695
- reason: 'authenticated-message-rate-exceeded',
7696
- deviceId: device?.deviceId || '',
7697
- sessionId: device?.sessionId || '',
7698
- accountId: socket.__liveDeskSecurityContext?.accountId || '',
7699
- hubId: socket.__liveDeskSecurityContext?.hubId || '',
7700
- transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7701
- channel,
7702
- remoteAddress: safeString(socket.remoteAddress, 256),
7703
- rate: state.messageRateGate.getStatus()
7704
- });
7705
- writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
7706
- socket.destroy();
7707
- return false;
7708
- }
7709
-
7710
- function handleWebSocketAgentSocket(socket) {
7693
+ function enforceAgentMessageRate(socket, state, channel = 'control') {
7694
+ if (state.messageRateGate.consume()) return true;
7695
+ const device = state.device;
7696
+ emitEvent('RemoteAbuseDefense', {
7697
+ result: 'rejected',
7698
+ reason: 'authenticated-message-rate-exceeded',
7699
+ deviceId: device?.deviceId || '',
7700
+ sessionId: device?.sessionId || '',
7701
+ accountId: socket.__liveDeskSecurityContext?.accountId || '',
7702
+ hubId: socket.__liveDeskSecurityContext?.hubId || '',
7703
+ transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7704
+ channel,
7705
+ remoteAddress: safeString(socket.remoteAddress, 256),
7706
+ rate: state.messageRateGate.getStatus()
7707
+ });
7708
+ writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
7709
+ socket.destroy();
7710
+ return false;
7711
+ }
7712
+
7713
+ function handleWebSocketAgentSocket(socket) {
7711
7714
  allSockets.add(socket);
7712
7715
  socket.setNoDelay(true);
7713
7716
  socket.setKeepAlive(true, heartbeatMs);
@@ -7721,11 +7724,11 @@ export function createRemoteHub(options = {}) {
7721
7724
  audioOnly: false,
7722
7725
  fileOnly: false,
7723
7726
  parserBuffer: socket.buffer,
7724
- binaryIngress: null,
7725
- binaryQueueDrops: 0,
7726
- binaryQueueEpoch: crypto.randomUUID(),
7727
- messageRateGate: createRemoteMessageRateGate(),
7728
- closed: false
7727
+ binaryIngress: null,
7728
+ binaryQueueDrops: 0,
7729
+ binaryQueueEpoch: crypto.randomUUID(),
7730
+ messageRateGate: createRemoteMessageRateGate(),
7731
+ closed: false
7729
7732
  };
7730
7733
  attachAgentBinaryIngress(socket, state);
7731
7734
 
@@ -7736,9 +7739,9 @@ export function createRemoteHub(options = {}) {
7736
7739
  }
7737
7740
  }, 10000);
7738
7741
 
7739
- socket.onTextMessage = text => {
7740
- if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
7741
- try {
7742
+ socket.onTextMessage = text => {
7743
+ if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
7744
+ try {
7742
7745
  handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
7743
7746
  } catch (err) {
7744
7747
  writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
@@ -7746,9 +7749,9 @@ export function createRemoteHub(options = {}) {
7746
7749
  }
7747
7750
  };
7748
7751
 
7749
- socket.onBinaryMessage = payload => {
7750
- if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
7751
- try {
7752
+ socket.onBinaryMessage = payload => {
7753
+ if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
7754
+ try {
7752
7755
  const packet = parseRemoteHubWebSocketBinaryFrame(payload);
7753
7756
  if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
7754
7757
  writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
@@ -7903,11 +7906,11 @@ export function createRemoteHub(options = {}) {
7903
7906
  buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
7904
7907
  lineScanOffset: 0,
7905
7908
  pendingBinaryFrame: null,
7906
- binaryIngress: null,
7907
- binaryQueueDrops: 0,
7908
- binaryQueueEpoch: crypto.randomUUID(),
7909
- messageRateGate: createRemoteMessageRateGate(),
7910
- closed: false
7909
+ binaryIngress: null,
7910
+ binaryQueueDrops: 0,
7911
+ binaryQueueEpoch: crypto.randomUUID(),
7912
+ messageRateGate: createRemoteMessageRateGate(),
7913
+ closed: false
7911
7914
  };
7912
7915
  attachAgentBinaryIngress(socket, state);
7913
7916
 
@@ -8022,18 +8025,18 @@ export function createRemoteHub(options = {}) {
8022
8025
  return;
8023
8026
  }
8024
8027
 
8025
- try {
8026
- const message = parseJsonLine(lineBuffer.toString('utf8'));
8027
- if (!enforceAgentMessageRate(socket, state, state.frameOnly
8028
- ? 'frame'
8029
- : state.audioOnly
8030
- ? 'audio'
8031
- : state.inputOnly
8032
- ? 'input'
8033
- : state.fileOnly ? 'file' : 'control')) {
8034
- return;
8035
- }
8036
- if (message?.type === 'frame.binary') {
8028
+ try {
8029
+ const message = parseJsonLine(lineBuffer.toString('utf8'));
8030
+ if (!enforceAgentMessageRate(socket, state, state.frameOnly
8031
+ ? 'frame'
8032
+ : state.audioOnly
8033
+ ? 'audio'
8034
+ : state.inputOnly
8035
+ ? 'input'
8036
+ : state.fileOnly ? 'file' : 'control')) {
8037
+ return;
8038
+ }
8039
+ if (message?.type === 'frame.binary') {
8037
8040
  const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
8038
8041
  const maxBytes = frameKind === 'thumbnail'
8039
8042
  ? MAX_THUMBNAIL_BINARY_BYTES
@@ -8087,35 +8090,35 @@ export function createRemoteHub(options = {}) {
8087
8090
  }
8088
8091
  }
8089
8092
 
8090
- function handleSocket(socket) {
8091
- if (!allowLegacyPlaintextForTests) {
8092
- secureDirectAcceptor.accept(socket);
8093
- return;
8094
- }
8095
- socket.once('data', chunk => {
8096
- const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8097
- if (isSecureHandshakeStart(firstChunk)) {
8098
- secureDirectAcceptor.accept(socket, firstChunk);
8099
- return;
8100
- }
8101
- if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8102
- if (!allowLegacyPlaintextForTests) {
8103
- socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
8104
- socket.destroy();
8105
- return;
8106
- }
8107
- socket.__liveDeskLegacyPlaintext = true;
8108
- handleWebSocketUpgradeSocket(socket, firstChunk);
8109
- return;
8110
- }
8111
- if (!allowLegacyPlaintextForTests) {
8112
- writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
8113
- socket.destroy();
8114
- return;
8115
- }
8116
- socket.__liveDeskLegacyPlaintext = true;
8117
- handleTcpAgentSocket(socket, firstChunk);
8118
- });
8093
+ function handleSocket(socket) {
8094
+ if (!allowLegacyPlaintextForTests) {
8095
+ secureDirectAcceptor.accept(socket);
8096
+ return;
8097
+ }
8098
+ socket.once('data', chunk => {
8099
+ const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8100
+ if (isSecureHandshakeStart(firstChunk)) {
8101
+ secureDirectAcceptor.accept(socket, firstChunk);
8102
+ return;
8103
+ }
8104
+ if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8105
+ if (!allowLegacyPlaintextForTests) {
8106
+ socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
8107
+ socket.destroy();
8108
+ return;
8109
+ }
8110
+ socket.__liveDeskLegacyPlaintext = true;
8111
+ handleWebSocketUpgradeSocket(socket, firstChunk);
8112
+ return;
8113
+ }
8114
+ if (!allowLegacyPlaintextForTests) {
8115
+ writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
8116
+ socket.destroy();
8117
+ return;
8118
+ }
8119
+ socket.__liveDeskLegacyPlaintext = true;
8120
+ handleTcpAgentSocket(socket, firstChunk);
8121
+ });
8119
8122
 
8120
8123
  socket.once('error', () => {
8121
8124
  // The transport-specific handler owns logging after the first byte.
@@ -8137,10 +8140,10 @@ export function createRemoteHub(options = {}) {
8137
8140
  started = true;
8138
8141
  boundPort = candidateServer.address()?.port || port;
8139
8142
  lastError = '';
8140
- logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
8141
- if (host === '0.0.0.0' || host === '::') {
8142
- logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
8143
- }
8143
+ logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
8144
+ if (host === '0.0.0.0' || host === '::') {
8145
+ logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
8146
+ }
8144
8147
  emitRemoteEvent('RemoteHubStarted', null);
8145
8148
  resolve();
8146
8149
  });
@@ -8152,14 +8155,14 @@ export function createRemoteHub(options = {}) {
8152
8155
  return getStatus({ includeSecrets: false });
8153
8156
  }
8154
8157
 
8155
- try {
8156
- await udpTransport?.start?.();
8157
- await listenOnPort(requestedPort);
8158
- if (!controlIdleSweepTimer) {
8159
- controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
8160
- controlIdleSweepTimer.unref?.();
8161
- }
8162
- try {
8158
+ try {
8159
+ await udpTransport?.start?.();
8160
+ await listenOnPort(requestedPort);
8161
+ if (!controlIdleSweepTimer) {
8162
+ controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
8163
+ controlIdleSweepTimer.unref?.();
8164
+ }
8165
+ try {
8163
8166
  await relayControl?.start?.();
8164
8167
  } catch {
8165
8168
  logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
@@ -8180,12 +8183,12 @@ export function createRemoteHub(options = {}) {
8180
8183
  return getStatus({ includeSecrets: false });
8181
8184
  }
8182
8185
 
8183
- async function close() {
8184
- if (controlIdleSweepTimer) {
8185
- clearInterval(controlIdleSweepTimer);
8186
- controlIdleSweepTimer = null;
8187
- }
8188
- for (const state of [...agentBinaryIngressStates]) {
8186
+ async function close() {
8187
+ if (controlIdleSweepTimer) {
8188
+ clearInterval(controlIdleSweepTimer);
8189
+ controlIdleSweepTimer = null;
8190
+ }
8191
+ for (const state of [...agentBinaryIngressStates]) {
8189
8192
  closeAgentBinaryIngress(state, 'hub-shutdown');
8190
8193
  }
8191
8194
 
@@ -8322,7 +8325,7 @@ export function createRemoteHub(options = {}) {
8322
8325
  };
8323
8326
  }
8324
8327
 
8325
- function sendCommand(deviceId, command) {
8328
+ function sendCommand(deviceId, command) {
8326
8329
  const device = devices.get(String(deviceId || ''));
8327
8330
  const commandName = safeString(command?.command || 'ping', 80);
8328
8331
  const requiredPermission = commandName === 'input.control'
@@ -8388,64 +8391,64 @@ export function createRemoteHub(options = {}) {
8388
8391
  command: payload.command,
8389
8392
  channel: dedicatedFileSocket ? 'file' : 'control'
8390
8393
  });
8391
- return { ok: true, commandId };
8392
- }
8393
-
8394
- async function sendCommandAwaitResult(deviceId, command, options = {}) {
8395
- const normalizedDeviceId = safeString(deviceId, 160);
8396
- const device = devices.get(normalizedDeviceId);
8397
- const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
8398
- const commandWithId = { ...(command || {}), commandId };
8399
-
8400
- if (device?.synthetic === true && device.connected) {
8401
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8402
- return {
8403
- ...sent,
8404
- queued: sent.ok === true,
8405
- acknowledged: sent.ok === true,
8406
- acknowledgement: sent.ok === true
8407
- ? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
8408
- : { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
8409
- };
8410
- }
8411
-
8412
- if (!device?.socket || device.socket.destroyed || !device.connected) {
8413
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8414
- return {
8415
- ...sent,
8416
- queued: false,
8417
- acknowledged: false,
8418
- acknowledgement: {
8419
- ok: false,
8420
- commandId,
8421
- result: null,
8422
- error: sent.error || 'device-not-connected'
8423
- }
8424
- };
8425
- }
8426
-
8427
- const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
8428
- // Register before writing: a loopback Agent can return command.result in
8429
- // the same event-loop turn as the command write.
8430
- const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
8431
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8432
- if (!sent.ok) {
8433
- failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
8434
- }
8435
- const acknowledgement = await acknowledgementPromise;
8436
- return {
8437
- ...sent,
8438
- ok: sent.ok === true && acknowledgement.ok === true,
8439
- queued: sent.ok === true,
8440
- acknowledged: acknowledgement.ok === true,
8441
- acknowledgement,
8442
- error: acknowledgement.ok === true
8443
- ? undefined
8444
- : acknowledgement.error || sent.error || 'command-result-failed'
8445
- };
8446
- }
8447
-
8448
- function refreshDevicePolicies(deviceIds = undefined) {
8394
+ return { ok: true, commandId };
8395
+ }
8396
+
8397
+ async function sendCommandAwaitResult(deviceId, command, options = {}) {
8398
+ const normalizedDeviceId = safeString(deviceId, 160);
8399
+ const device = devices.get(normalizedDeviceId);
8400
+ const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
8401
+ const commandWithId = { ...(command || {}), commandId };
8402
+
8403
+ if (device?.synthetic === true && device.connected) {
8404
+ const sent = sendCommand(normalizedDeviceId, commandWithId);
8405
+ return {
8406
+ ...sent,
8407
+ queued: sent.ok === true,
8408
+ acknowledged: sent.ok === true,
8409
+ acknowledgement: sent.ok === true
8410
+ ? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
8411
+ : { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
8412
+ };
8413
+ }
8414
+
8415
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
8416
+ const sent = sendCommand(normalizedDeviceId, commandWithId);
8417
+ return {
8418
+ ...sent,
8419
+ queued: false,
8420
+ acknowledged: false,
8421
+ acknowledgement: {
8422
+ ok: false,
8423
+ commandId,
8424
+ result: null,
8425
+ error: sent.error || 'device-not-connected'
8426
+ }
8427
+ };
8428
+ }
8429
+
8430
+ const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
8431
+ // Register before writing: a loopback Agent can return command.result in
8432
+ // the same event-loop turn as the command write.
8433
+ const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
8434
+ const sent = sendCommand(normalizedDeviceId, commandWithId);
8435
+ if (!sent.ok) {
8436
+ failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
8437
+ }
8438
+ const acknowledgement = await acknowledgementPromise;
8439
+ return {
8440
+ ...sent,
8441
+ ok: sent.ok === true && acknowledgement.ok === true,
8442
+ queued: sent.ok === true,
8443
+ acknowledged: acknowledgement.ok === true,
8444
+ acknowledgement,
8445
+ error: acknowledgement.ok === true
8446
+ ? undefined
8447
+ : acknowledgement.error || sent.error || 'command-result-failed'
8448
+ };
8449
+ }
8450
+
8451
+ function refreshDevicePolicies(deviceIds = undefined) {
8449
8452
  const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
8450
8453
  ? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
8451
8454
  : null;
@@ -8454,31 +8457,34 @@ export function createRemoteHub(options = {}) {
8454
8457
  for (const device of devices.values()) {
8455
8458
  if (requestedIds && !requestedIds.has(device.deviceId)) continue;
8456
8459
  if (!device?.socket || device.socket.destroyed || !device.connected) continue;
8457
- total += 1;
8458
- const effectivePolicy = getDevicePolicy(device);
8459
- const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
8460
- if (connectionDenied) {
8461
- emitEvent('RemoteSecurityPolicyAudit', {
8462
- result: 'rejected',
8463
- reason: connectionDenied,
8464
- deviceId: device.deviceId,
8465
- sessionId: device.sessionId,
8466
- transport: device.controlTransport,
8467
- remoteAddress: device.remoteAddress
8468
- });
8469
- disconnectDevice(device.deviceId, connectionDenied);
8470
- continue;
8471
- }
8472
- if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
8473
- releaseInputOwner(
8474
- device.deviceId,
8475
- device.controlOwnerConnectionId,
8476
- 'control-disabled-by-policy');
8477
- }
8478
- if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
8479
- stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
8480
- }
8481
- if (writeJsonLine(device.socket, {
8460
+ total += 1;
8461
+ const effectivePolicy = getDevicePolicy(device);
8462
+ const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
8463
+ if (connectionDenied) {
8464
+ const deniedTransport = device.controlTransport || 'direct-tcp';
8465
+ const deniedRemoteAddress = safeString(device.remoteAddress, 256) || 'unknown';
8466
+ logWarn('security', 'Connected device rejected by refreshed policy reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress + ' device=' + device.deviceId);
8467
+ emitEvent('RemoteSecurityPolicyAudit', {
8468
+ result: 'rejected',
8469
+ reason: connectionDenied,
8470
+ deviceId: device.deviceId,
8471
+ sessionId: device.sessionId,
8472
+ transport: device.controlTransport,
8473
+ remoteAddress: device.remoteAddress
8474
+ });
8475
+ disconnectDevice(device.deviceId, connectionDenied);
8476
+ continue;
8477
+ }
8478
+ if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
8479
+ releaseInputOwner(
8480
+ device.deviceId,
8481
+ device.controlOwnerConnectionId,
8482
+ 'control-disabled-by-policy');
8483
+ }
8484
+ if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
8485
+ stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
8486
+ }
8487
+ if (writeJsonLine(device.socket, {
8482
8488
  type: 'policy.update',
8483
8489
  effectivePolicy,
8484
8490
  updatedAt: new Date().toISOString()
@@ -8557,7 +8563,7 @@ export function createRemoteHub(options = {}) {
8557
8563
  ]);
8558
8564
  }
8559
8565
 
8560
- function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
8566
+ function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
8561
8567
  return {
8562
8568
  type: 'input.control',
8563
8569
  payload: {
@@ -8579,114 +8585,114 @@ export function createRemoteHub(options = {}) {
8579
8585
  hubForwardedAtEpochMs: Date.now(),
8580
8586
  issuedAt: new Date().toISOString()
8581
8587
  }
8582
- };
8583
- }
8584
-
8585
- function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
8586
- const policy = getDevicePolicy(device);
8587
- return {
8588
- type,
8589
- payload: {
8590
- controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
8591
- hubConnectionId: safeString(ownerConnectionId, 128),
8592
- deviceId: safeString(device?.deviceId, 160),
8593
- visibleIndicator: true,
8594
- lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
8595
- idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
8596
- reason: safeString(reason, 160),
8597
- issuedAt: new Date().toISOString()
8598
- }
8599
- };
8600
- }
8601
-
8602
- function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
8603
- const message = controlSessionMessage(device, ownerConnectionId, type, reason);
8604
- if (inputWriteOwner && bindingKey) {
8605
- return inputWriteOwner.enqueue(message, bindingKey);
8606
- }
8607
- if (!device?.socket || device.socket.destroyed) {
8608
- return { ok: false, error: 'device-not-connected' };
8609
- }
8610
- const sent = writeJsonLine(device.socket, {
8611
- type: 'command',
8612
- commandId: crypto.randomUUID(),
8613
- command: type,
8614
- payload: message.payload,
8615
- issuedAt: new Date().toISOString()
8616
- });
8617
- return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
8618
- }
8619
-
8620
- function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
8621
- const owner = safeString(ownerConnectionId, 128);
8622
- if (!device || !owner) return { ok: false, error: 'control-owner-required' };
8623
- if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
8624
- return { ok: true, alreadyActive: true };
8625
- }
8626
- device.controlOwnerSessionId = crypto.randomUUID();
8627
- device.controlOwnerConnectionId = owner;
8628
- device.controlOwnerStartedAtMs = Date.now();
8629
- device.controlOwnerLastInputAtMs = Date.now();
8630
- const sent = sendControlSessionMessage(
8631
- device,
8632
- owner,
8633
- 'control.session.start',
8634
- 'browser-control-owner-active',
8635
- inputWriteOwner,
8636
- bindingKey);
8637
- if (!sent.ok) {
8638
- device.controlOwnerSessionId = '';
8639
- device.controlOwnerConnectionId = '';
8640
- device.controlOwnerStartedAtMs = 0;
8641
- device.controlOwnerLastInputAtMs = 0;
8642
- return sent;
8643
- }
8644
- emitRemoteEvent('RemoteControlSessionStarted', device, {
8645
- controlOwnerSessionId: device.controlOwnerSessionId,
8646
- hubConnectionId: owner
8647
- });
8648
- return sent;
8649
- }
8650
-
8651
- function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
8652
- const owner = safeString(ownerConnectionId, 128);
8653
- if (!device?.controlOwnerSessionId
8654
- || !owner
8655
- || device.controlOwnerConnectionId !== owner) {
8656
- return { ok: false, error: 'control-owner-not-current' };
8657
- }
8658
- const controlOwnerSessionId = device.controlOwnerSessionId;
8659
- const sent = sendControlSessionMessage(
8660
- device,
8661
- owner,
8662
- 'control.session.stop',
8663
- reason,
8664
- inputWriteOwner,
8665
- bindingKey);
8666
- device.controlOwnerSessionId = '';
8667
- device.controlOwnerConnectionId = '';
8668
- device.controlOwnerStartedAtMs = 0;
8669
- device.controlOwnerLastInputAtMs = 0;
8670
- emitRemoteEvent('RemoteControlSessionEnded', device, {
8671
- controlOwnerSessionId,
8672
- hubConnectionId: owner,
8673
- reason,
8674
- delivered: sent.ok === true
8675
- });
8676
- return sent;
8677
- }
8678
-
8679
- function sweepIdleControlSessions() {
8680
- const nowMs = Date.now();
8681
- for (const device of devices.values()) {
8682
- if (!device?.connected || !device.controlOwnerConnectionId) continue;
8683
- const policy = getDevicePolicy(device);
8684
- if (policy.disconnectIdleControlSessions !== true) continue;
8685
- const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
8686
- if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
8687
- releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
8688
- }
8689
- }
8588
+ };
8589
+ }
8590
+
8591
+ function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
8592
+ const policy = getDevicePolicy(device);
8593
+ return {
8594
+ type,
8595
+ payload: {
8596
+ controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
8597
+ hubConnectionId: safeString(ownerConnectionId, 128),
8598
+ deviceId: safeString(device?.deviceId, 160),
8599
+ visibleIndicator: true,
8600
+ lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
8601
+ idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
8602
+ reason: safeString(reason, 160),
8603
+ issuedAt: new Date().toISOString()
8604
+ }
8605
+ };
8606
+ }
8607
+
8608
+ function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
8609
+ const message = controlSessionMessage(device, ownerConnectionId, type, reason);
8610
+ if (inputWriteOwner && bindingKey) {
8611
+ return inputWriteOwner.enqueue(message, bindingKey);
8612
+ }
8613
+ if (!device?.socket || device.socket.destroyed) {
8614
+ return { ok: false, error: 'device-not-connected' };
8615
+ }
8616
+ const sent = writeJsonLine(device.socket, {
8617
+ type: 'command',
8618
+ commandId: crypto.randomUUID(),
8619
+ command: type,
8620
+ payload: message.payload,
8621
+ issuedAt: new Date().toISOString()
8622
+ });
8623
+ return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
8624
+ }
8625
+
8626
+ function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
8627
+ const owner = safeString(ownerConnectionId, 128);
8628
+ if (!device || !owner) return { ok: false, error: 'control-owner-required' };
8629
+ if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
8630
+ return { ok: true, alreadyActive: true };
8631
+ }
8632
+ device.controlOwnerSessionId = crypto.randomUUID();
8633
+ device.controlOwnerConnectionId = owner;
8634
+ device.controlOwnerStartedAtMs = Date.now();
8635
+ device.controlOwnerLastInputAtMs = Date.now();
8636
+ const sent = sendControlSessionMessage(
8637
+ device,
8638
+ owner,
8639
+ 'control.session.start',
8640
+ 'browser-control-owner-active',
8641
+ inputWriteOwner,
8642
+ bindingKey);
8643
+ if (!sent.ok) {
8644
+ device.controlOwnerSessionId = '';
8645
+ device.controlOwnerConnectionId = '';
8646
+ device.controlOwnerStartedAtMs = 0;
8647
+ device.controlOwnerLastInputAtMs = 0;
8648
+ return sent;
8649
+ }
8650
+ emitRemoteEvent('RemoteControlSessionStarted', device, {
8651
+ controlOwnerSessionId: device.controlOwnerSessionId,
8652
+ hubConnectionId: owner
8653
+ });
8654
+ return sent;
8655
+ }
8656
+
8657
+ function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
8658
+ const owner = safeString(ownerConnectionId, 128);
8659
+ if (!device?.controlOwnerSessionId
8660
+ || !owner
8661
+ || device.controlOwnerConnectionId !== owner) {
8662
+ return { ok: false, error: 'control-owner-not-current' };
8663
+ }
8664
+ const controlOwnerSessionId = device.controlOwnerSessionId;
8665
+ const sent = sendControlSessionMessage(
8666
+ device,
8667
+ owner,
8668
+ 'control.session.stop',
8669
+ reason,
8670
+ inputWriteOwner,
8671
+ bindingKey);
8672
+ device.controlOwnerSessionId = '';
8673
+ device.controlOwnerConnectionId = '';
8674
+ device.controlOwnerStartedAtMs = 0;
8675
+ device.controlOwnerLastInputAtMs = 0;
8676
+ emitRemoteEvent('RemoteControlSessionEnded', device, {
8677
+ controlOwnerSessionId,
8678
+ hubConnectionId: owner,
8679
+ reason,
8680
+ delivered: sent.ok === true
8681
+ });
8682
+ return sent;
8683
+ }
8684
+
8685
+ function sweepIdleControlSessions() {
8686
+ const nowMs = Date.now();
8687
+ for (const device of devices.values()) {
8688
+ if (!device?.connected || !device.controlOwnerConnectionId) continue;
8689
+ const policy = getDevicePolicy(device);
8690
+ if (policy.disconnectIdleControlSessions !== true) continue;
8691
+ const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
8692
+ if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
8693
+ releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
8694
+ }
8695
+ }
8690
8696
 
8691
8697
  function getCurrentInputWriteOwner(device) {
8692
8698
  if (!device?.inputSocket
@@ -8829,16 +8835,16 @@ export function createRemoteHub(options = {}) {
8829
8835
  normalized,
8830
8836
  activeMonitorIndex);
8831
8837
  const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
8832
- const ownerChanged = normalized.hubConnectionId
8838
+ const ownerChanged = normalized.hubConnectionId
8833
8839
  && previousOwnerConnectionId
8834
8840
  && normalized.hubConnectionId !== previousOwnerConnectionId;
8835
8841
  const bindingChanged = previousInputBindingKey
8836
8842
  && previousInputBindingKey !== activeInputBindingKey;
8837
- const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
8838
- const startingControlOwner = !!normalized.hubConnectionId
8839
- && (!device.controlOwnerConnectionId
8840
- || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8841
- if (ownerChanged || bindingChanged || writeBindingChanged) {
8843
+ const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
8844
+ const startingControlOwner = !!normalized.hubConnectionId
8845
+ && (!device.controlOwnerConnectionId
8846
+ || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8847
+ if (ownerChanged || bindingChanged || writeBindingChanged) {
8842
8848
  const resetSent = inputWriteOwner.replaceBinding(
8843
8849
  activeInputBindingKey,
8844
8850
  buildRemoteInputResetMessage(
@@ -8853,28 +8859,28 @@ export function createRemoteHub(options = {}) {
8853
8859
  activeMonitorIndex));
8854
8860
  if (!resetSent.ok) {
8855
8861
  closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
8856
- return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8857
- }
8858
- if (startingControlOwner) {
8859
- if (device.controlOwnerConnectionId) {
8860
- endControlSession(
8861
- device,
8862
- device.controlOwnerConnectionId,
8863
- 'browser-input-owner-changed',
8864
- inputWriteOwner,
8865
- activeInputBindingKey);
8866
- }
8867
- const started = beginControlSession(
8868
- device,
8869
- normalized.hubConnectionId,
8870
- inputWriteOwner,
8871
- activeInputBindingKey);
8872
- if (!started.ok) {
8873
- closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
8874
- return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8875
- }
8876
- }
8877
- }
8862
+ return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8863
+ }
8864
+ if (startingControlOwner) {
8865
+ if (device.controlOwnerConnectionId) {
8866
+ endControlSession(
8867
+ device,
8868
+ device.controlOwnerConnectionId,
8869
+ 'browser-input-owner-changed',
8870
+ inputWriteOwner,
8871
+ activeInputBindingKey);
8872
+ }
8873
+ const started = beginControlSession(
8874
+ device,
8875
+ normalized.hubConnectionId,
8876
+ inputWriteOwner,
8877
+ activeInputBindingKey);
8878
+ if (!started.ok) {
8879
+ closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
8880
+ return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8881
+ }
8882
+ }
8883
+ }
8878
8884
  const sent = inputWriteOwner.enqueue({
8879
8885
  type: 'input.control',
8880
8886
  payload: {
@@ -8887,8 +8893,8 @@ export function createRemoteHub(options = {}) {
8887
8893
  if (normalized.hubConnectionId) {
8888
8894
  device.inputOwnerConnectionId = normalized.hubConnectionId;
8889
8895
  }
8890
- device.inputOwnerBindingKey = activeInputBindingKey;
8891
- device.controlOwnerLastInputAtMs = Date.now();
8896
+ device.inputOwnerBindingKey = activeInputBindingKey;
8897
+ device.controlOwnerLastInputAtMs = Date.now();
8892
8898
  device.counters.commandsSent += 1;
8893
8899
  device.inputLastSeenAt = new Date().toISOString();
8894
8900
  return {
@@ -8917,27 +8923,27 @@ export function createRemoteHub(options = {}) {
8917
8923
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8918
8924
  }
8919
8925
 
8920
- const hubForwardedAtEpochMs = Date.now();
8921
- const startingFallbackOwner = !!normalized.hubConnectionId
8922
- && (!device.controlOwnerConnectionId
8923
- || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8924
- if (startingFallbackOwner) {
8925
- if (device.controlOwnerConnectionId) {
8926
- endControlSession(
8927
- device,
8928
- device.controlOwnerConnectionId,
8929
- 'browser-input-owner-changed',
8930
- null,
8931
- '');
8932
- }
8933
- const started = beginControlSession(
8934
- device,
8935
- normalized.hubConnectionId,
8936
- null,
8937
- '');
8938
- if (!started.ok) return started;
8939
- }
8940
- const fallback = sendCommand(deviceId, {
8926
+ const hubForwardedAtEpochMs = Date.now();
8927
+ const startingFallbackOwner = !!normalized.hubConnectionId
8928
+ && (!device.controlOwnerConnectionId
8929
+ || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8930
+ if (startingFallbackOwner) {
8931
+ if (device.controlOwnerConnectionId) {
8932
+ endControlSession(
8933
+ device,
8934
+ device.controlOwnerConnectionId,
8935
+ 'browser-input-owner-changed',
8936
+ null,
8937
+ '');
8938
+ }
8939
+ const started = beginControlSession(
8940
+ device,
8941
+ normalized.hubConnectionId,
8942
+ null,
8943
+ '');
8944
+ if (!started.ok) return started;
8945
+ }
8946
+ const fallback = sendCommand(deviceId, {
8941
8947
  command: 'input.control',
8942
8948
  payload: {
8943
8949
  ...normalized,
@@ -8945,7 +8951,7 @@ export function createRemoteHub(options = {}) {
8945
8951
  issuedAt: normalized.issuedAt || new Date().toISOString()
8946
8952
  }
8947
8953
  });
8948
- if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
8954
+ if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
8949
8955
  schedulePendingInputFallback(device, {
8950
8956
  commandId: fallback.commandId,
8951
8957
  inputSeq: normalized.inputSeq,
@@ -8957,26 +8963,26 @@ export function createRemoteHub(options = {}) {
8957
8963
  hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
8958
8964
  hubForwardedAtEpochMs,
8959
8965
  fallback: true,
8960
- timeoutTimer: null
8961
- });
8962
- }
8963
- if (fallback.ok) {
8964
- device.inputOwnerConnectionId = normalized.hubConnectionId;
8965
- device.inputOwnerBindingKey = buildRemoteInputBindingKey(
8966
- device,
8967
- controlStream,
8968
- normalized,
8969
- activeMonitorIndex);
8970
- device.controlOwnerLastInputAtMs = Date.now();
8971
- } else if (startingFallbackOwner) {
8972
- endControlSession(
8973
- device,
8974
- normalized.hubConnectionId,
8975
- 'input-control-delivery-failed',
8976
- null,
8977
- '');
8978
- }
8979
- return fallback.ok
8966
+ timeoutTimer: null
8967
+ });
8968
+ }
8969
+ if (fallback.ok) {
8970
+ device.inputOwnerConnectionId = normalized.hubConnectionId;
8971
+ device.inputOwnerBindingKey = buildRemoteInputBindingKey(
8972
+ device,
8973
+ controlStream,
8974
+ normalized,
8975
+ activeMonitorIndex);
8976
+ device.controlOwnerLastInputAtMs = Date.now();
8977
+ } else if (startingFallbackOwner) {
8978
+ endControlSession(
8979
+ device,
8980
+ normalized.hubConnectionId,
8981
+ 'input-control-delivery-failed',
8982
+ null,
8983
+ '');
8984
+ }
8985
+ return fallback.ok
8980
8986
  ? {
8981
8987
  ...fallback,
8982
8988
  inputSocket: false,
@@ -9002,20 +9008,20 @@ export function createRemoteHub(options = {}) {
9002
9008
  return { ok: false, error: 'input-owner-not-current' };
9003
9009
  }
9004
9010
 
9005
- const inputSocket = device.inputSocket;
9006
- const inputWriteOwner = getCurrentInputWriteOwner(device);
9007
- if (!inputSocket || !inputWriteOwner) {
9008
- const ended = endControlSession(device, owner, reason, null, '');
9009
- device.inputOwnerConnectionId = '';
9010
- device.inputOwnerBindingKey = '';
9011
- return {
9012
- ok: ended.ok !== false,
9013
- queued: false,
9014
- controlSessionEnded: ended.ok === true
9015
- };
9016
- }
9017
- const controlStream = getActiveControlStream(device);
9018
- const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
9011
+ const inputSocket = device.inputSocket;
9012
+ const inputWriteOwner = getCurrentInputWriteOwner(device);
9013
+ if (!inputSocket || !inputWriteOwner) {
9014
+ const ended = endControlSession(device, owner, reason, null, '');
9015
+ device.inputOwnerConnectionId = '';
9016
+ device.inputOwnerBindingKey = '';
9017
+ return {
9018
+ ok: ended.ok !== false,
9019
+ queued: false,
9020
+ controlSessionEnded: ended.ok === true
9021
+ };
9022
+ }
9023
+ const controlStream = getActiveControlStream(device);
9024
+ const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
9019
9025
  const sent = inputWriteOwner.replaceBinding(
9020
9026
  releaseBindingKey,
9021
9027
  buildRemoteInputResetMessage(
@@ -9024,24 +9030,24 @@ export function createRemoteHub(options = {}) {
9024
9030
  owner,
9025
9031
  reason,
9026
9032
  normalizeMonitorIndex(controlStream?.monitorIndex)));
9027
- if (!sent.ok) {
9033
+ if (!sent.ok) {
9028
9034
  closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
9029
9035
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
9030
- }
9031
- const ended = endControlSession(
9032
- device,
9033
- owner,
9034
- reason,
9035
- inputWriteOwner,
9036
- releaseBindingKey);
9037
- device.inputOwnerConnectionId = '';
9038
- device.inputOwnerBindingKey = '';
9039
- return {
9040
- ok: ended.ok !== false,
9041
- queued: true,
9042
- backpressured: sent.backpressured === true || ended.backpressured === true,
9043
- controlSessionEnded: ended.ok === true
9044
- };
9036
+ }
9037
+ const ended = endControlSession(
9038
+ device,
9039
+ owner,
9040
+ reason,
9041
+ inputWriteOwner,
9042
+ releaseBindingKey);
9043
+ device.inputOwnerConnectionId = '';
9044
+ device.inputOwnerBindingKey = '';
9045
+ return {
9046
+ ok: ended.ok !== false,
9047
+ queued: true,
9048
+ backpressured: sent.backpressured === true || ended.backpressured === true,
9049
+ controlSessionEnded: ended.ok === true
9050
+ };
9045
9051
  }
9046
9052
 
9047
9053
  function notifyAgentProgress(deviceIds, progress = {}) {
@@ -9081,12 +9087,12 @@ export function createRemoteHub(options = {}) {
9081
9087
  if (retiredRequestError) {
9082
9088
  return { ok: false, error: retiredRequestError };
9083
9089
  }
9084
- const device = devices.get(String(deviceId || ''));
9085
- const requestedOperation = safeString(options.operation, 80);
9086
- if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
9087
- return { ok: false, error: 'agent-mutating-tool-disabled' };
9088
- }
9089
- const operation = normalizeAgentOperation(requestedOperation);
9090
+ const device = devices.get(String(deviceId || ''));
9091
+ const requestedOperation = safeString(options.operation, 80);
9092
+ if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
9093
+ return { ok: false, error: 'agent-mutating-tool-disabled' };
9094
+ }
9095
+ const operation = normalizeAgentOperation(requestedOperation);
9090
9096
  if (requestedOperation && !operation) {
9091
9097
  return { ok: false, error: 'unsupported-agent-operation' };
9092
9098
  }
@@ -11332,10 +11338,10 @@ export function createRemoteHub(options = {}) {
11332
11338
  retryTaskBatch,
11333
11339
  listDeviceFrames,
11334
11340
  disconnectDevice,
11335
- assignDeviceSlot,
11336
- sendCommand,
11337
- sendCommandAwaitResult,
11338
- refreshDevicePolicies,
11341
+ assignDeviceSlot,
11342
+ sendCommand,
11343
+ sendCommandAwaitResult,
11344
+ refreshDevicePolicies,
11339
11345
  sendLegacyClientUpdate,
11340
11346
  sendInputControl,
11341
11347
  releaseInputOwner,
@@ -11362,17 +11368,17 @@ export function createRemoteHub(options = {}) {
11362
11368
  handleUdpFrame,
11363
11369
  seedSyntheticFleet,
11364
11370
  clearSyntheticFleet,
11365
- getPairToken: () => pairToken,
11366
- getPairingPin: () => pairingPin,
11367
- getSecurityStatus: () => ({
11368
- hubId: deviceCredentialAuthority.hubId,
11369
- hubPublicKey: deviceCredentialAuthority.hubPublicKey,
11370
- hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
11371
- direct: secureDirectAcceptor.getStatus(),
11372
- devices: deviceCredentialAuthority.listDevices()
11373
- }),
11374
- revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
11375
- clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
11371
+ getPairToken: () => pairToken,
11372
+ getPairingPin: () => pairingPin,
11373
+ getSecurityStatus: () => ({
11374
+ hubId: deviceCredentialAuthority.hubId,
11375
+ hubPublicKey: deviceCredentialAuthority.hubPublicKey,
11376
+ hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
11377
+ direct: secureDirectAcceptor.getStatus(),
11378
+ devices: deviceCredentialAuthority.listDevices()
11379
+ }),
11380
+ revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
11381
+ clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
11376
11382
  };
11377
11383
  }
11378
11384