@livedesk/hub 0.1.36 → 0.1.37

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,7 @@
1
1
  import net from 'net';
2
2
  import os from 'os';
3
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';
4
+ import { createHubRelayControl } from './transport/relay-hub-control.js';
8
5
  import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
9
6
  import {
10
7
  BoundedSegmentedBuffer,
@@ -38,7 +35,6 @@ const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
38
35
  const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
39
36
  + MAX_AGENT_TCP_BINARY_PACKET_BYTES
40
37
  + (256 * 1024);
41
- const MAX_AGENT_MESSAGES_PER_SECOND = 240;
42
38
  const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
43
39
  const MAX_AGENT_TASK_CHARS = 4000;
44
40
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
@@ -47,7 +43,7 @@ const RECENT_TASK_LIMIT = 12;
47
43
  const RECENT_TASK_BATCH_LIMIT = 16;
48
44
  const RECENT_FRAME_CACHE_TTL_MS = 4000;
49
45
  const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
50
- const RECENT_LIVE_FRAME_CACHE_LIMIT = 2;
46
+ const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
51
47
  const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
52
48
  const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
53
49
  const LIVE_STREAM_PENDING_REUSE_MS = 5000;
@@ -837,53 +833,6 @@ function isPrivateNetworkAddress(value) {
837
833
  || address.startsWith('fd');
838
834
  }
839
835
 
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
-
887
836
  function readCpuTimes() {
888
837
  let idle = 0;
889
838
  let total = 0;
@@ -1213,24 +1162,21 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
1213
1162
  'process.list',
1214
1163
  'service.status',
1215
1164
  '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
1165
  'process.control',
1224
1166
  'service.control',
1225
1167
  'application.launch',
1226
1168
  'application.close',
1169
+ 'file.read',
1227
1170
  'file.write',
1228
1171
  'file.delete',
1172
+ 'file.list',
1229
1173
  'command.run',
1230
1174
  'script.run',
1231
1175
  'software.install',
1176
+ 'network.status',
1232
1177
  'system.power',
1233
- 'system.configure'
1178
+ 'system.configure',
1179
+ 'logs.collect'
1234
1180
  ]);
1235
1181
 
1236
1182
  function normalizeAgentOperation(value) {
@@ -1402,7 +1348,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
1402
1348
  return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
1403
1349
  }
1404
1350
 
1405
- function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1351
+ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1406
1352
  if (!frame) {
1407
1353
  return null;
1408
1354
  }
@@ -1424,27 +1370,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1424
1370
  serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
1425
1371
  }
1426
1372
 
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
- }
1373
+ return serialized;
1374
+ }
1375
+
1376
+ function serializeActiveLiveStream(activeLiveStream, deviceId) {
1377
+ if (!activeLiveStream) {
1378
+ return null;
1379
+ }
1380
+
1381
+ return {
1382
+ ...activeLiveStream,
1383
+ // The active descriptor is status metadata. Preserve the Hub's exact
1384
+ // internal frame owner for delivery, but never copy encoded bytes or
1385
+ // its access token into a browser status snapshot.
1386
+ latestFrame: serializeRemoteFrame(
1387
+ activeLiveStream.latestFrame,
1388
+ deviceId,
1389
+ 'live',
1390
+ { includeDataUrl: false }
1391
+ )
1392
+ };
1393
+ }
1448
1394
 
1449
1395
  function getRecentFrameCache(device, frameKind) {
1450
1396
  if (!device) {
@@ -1478,7 +1424,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
1478
1424
  : RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
1479
1425
  }
1480
1426
 
1481
- export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1427
+ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1482
1428
  if (!Array.isArray(cache)) {
1483
1429
  return;
1484
1430
  }
@@ -1499,20 +1445,6 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1499
1445
  }
1500
1446
  }
1501
1447
 
1502
- if (frameKind !== 'thumbnail' && cache.length > 1) {
1503
- // Keep the newest frame plus the newest independently decodable key
1504
- // frame. A browser can subscribe after capture has already started;
1505
- // retaining only the latest 30 fps delta makes that browser wait for a
1506
- // future GOP even though the Hub accepted a current key frame moments
1507
- // earlier. Exact stream-owner matching below prevents stale replay.
1508
- const latest = cache[0];
1509
- const keyFrame = cache.find(entry =>
1510
- entry !== latest
1511
- && (entry?.frame?.isKeyFrame === true
1512
- || safeString(entry?.frame?.chunkType, 20).toLowerCase() === 'key'));
1513
- cache.splice(0, cache.length, ...[latest, keyFrame].filter(Boolean));
1514
- }
1515
-
1516
1448
  for (let index = 0; index < cache.length; index += 1) {
1517
1449
  const entry = cache[index];
1518
1450
  const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
@@ -1524,7 +1456,7 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1524
1456
  }
1525
1457
  }
1526
1458
 
1527
- export function rememberRecentFramePayload(device, frameKind, frame) {
1459
+ function rememberRecentFramePayload(device, frameKind, frame) {
1528
1460
  if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
1529
1461
  return;
1530
1462
  }
@@ -1560,7 +1492,7 @@ export function rememberRecentFramePayload(device, frameKind, frame) {
1560
1492
  pruneRecentFrameCache(cache, kind, nowMs);
1561
1493
  }
1562
1494
 
1563
- export function isFramePayloadRequestMatch(frame, options = {}) {
1495
+ function isFramePayloadRequestMatch(frame, options = {}) {
1564
1496
  if (!frame || !Buffer.isBuffer(frame.payload)) {
1565
1497
  return false;
1566
1498
  }
@@ -1579,37 +1511,10 @@ export function isFramePayloadRequestMatch(frame, options = {}) {
1579
1511
  return false;
1580
1512
  }
1581
1513
 
1582
- if (options.requireKeyFrame === true
1583
- && frame.isKeyFrame !== true
1584
- && safeString(frame.chunkType, 20).toLowerCase() !== 'key') {
1585
- return false;
1586
- }
1587
-
1588
- for (const key of ['streamId', 'sessionId', 'commandId', 'streamPurpose']) {
1589
- const expected = safeString(options[key], 160);
1590
- if (expected && safeString(frame[key], 160) !== expected) {
1591
- return false;
1592
- }
1593
- }
1594
-
1595
- const requestedCaptureGeneration = Number(options.captureGeneration);
1596
- if (Number.isFinite(requestedCaptureGeneration)
1597
- && requestedCaptureGeneration > 0
1598
- && Number(frame.captureGeneration || 0) !== requestedCaptureGeneration) {
1599
- return false;
1600
- }
1601
-
1602
- const requestedMonitorIndex = Number(options.monitorIndex);
1603
- if (Number.isFinite(requestedMonitorIndex)
1604
- && requestedMonitorIndex >= 0
1605
- && Number(frame.monitorIndex ?? -1) !== requestedMonitorIndex) {
1606
- return false;
1607
- }
1608
-
1609
1514
  return true;
1610
1515
  }
1611
1516
 
1612
- export function findRecentFramePayload(device, frameKind, options = {}) {
1517
+ function findRecentFramePayload(device, frameKind, options = {}) {
1613
1518
  const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
1614
1519
  const latest = kind === 'thumbnail'
1615
1520
  ? device?.latestThumbnail
@@ -1620,14 +1525,7 @@ export function findRecentFramePayload(device, frameKind, options = {}) {
1620
1525
 
1621
1526
  const token = safeString(options.token, 128);
1622
1527
  const requestedSeq = Number(options.frameSeq);
1623
- const ownerQuery = options.requireKeyFrame === true
1624
- || ['streamId', 'sessionId', 'commandId', 'streamPurpose']
1625
- .some(key => safeString(options[key], 160))
1626
- || (Number.isFinite(Number(options.captureGeneration))
1627
- && Number(options.captureGeneration) > 0)
1628
- || (Number.isFinite(Number(options.monitorIndex))
1629
- && Number(options.monitorIndex) >= 0);
1630
- if (!token && !Number.isFinite(requestedSeq) && !ownerQuery) {
1528
+ if (!token && !Number.isFinite(requestedSeq)) {
1631
1529
  return null;
1632
1530
  }
1633
1531
 
@@ -1683,7 +1581,7 @@ function serializeDevice(device, options = {}) {
1683
1581
  },
1684
1582
  latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
1685
1583
  latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
1686
- activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1584
+ activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1687
1585
  liveCapturePause: device.liveCapturePause
1688
1586
  ? {
1689
1587
  token: device.liveCapturePause.token,
@@ -2301,16 +2199,6 @@ export function createRemoteHub(options = {}) {
2301
2199
  }
2302
2200
  }
2303
2201
 
2304
- function connectionPolicyError(socket, policy = {}) {
2305
- return evaluateRemoteConnectionPolicy({
2306
- remoteAddress: socket?.remoteAddress || '',
2307
- relayConnection: socket?.__liveDeskRelayControl === true,
2308
- encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
2309
- allowLegacyPlaintextForTests,
2310
- policy
2311
- });
2312
- }
2313
-
2314
2202
  function policyError(device, permission = '', command = '') {
2315
2203
  const policy = getDevicePolicy(device);
2316
2204
  const commandName = safeString(command, 80);
@@ -2359,83 +2247,22 @@ export function createRemoteHub(options = {}) {
2359
2247
  const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
2360
2248
  const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
2361
2249
  const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
2362
- const configuredEnrollmentToken = safeString(
2363
- options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
2250
+ const pairToken = safeString(
2251
+ options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
2364
2252
  256);
2365
- const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
2366
- || isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
2367
- || env.LIVEDESK_TEST_MODE === '1';
2368
- const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
2369
- && Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
2370
- && Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
2371
- ? configuredEnrollmentToken
2372
- : '';
2373
- let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
2374
- ? configuredEnrollmentToken
2375
- : canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
2376
- const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
2377
- dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
2378
- });
2379
- const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
2380
- ? options.getSecurityIdentity
2381
- : () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
2382
- udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
2383
- const identity = getSecurityIdentity() || {};
2384
- return deviceCredentialAuthority.issueRendezvousProof({
2385
- roomId,
2386
- deviceId,
2387
- role,
2388
- ttlMs,
2389
- accountId: safeString(identity.accountId, 128)
2253
+ const relayControl = options.relayControl && typeof options.relayControl === 'object'
2254
+ ? options.relayControl
2255
+ : createHubRelayControl({
2256
+ env,
2257
+ pairToken,
2258
+ logEvent,
2259
+ logWarn,
2260
+ onPeerSocket: socket => handleTcpAgentSocket(socket)
2390
2261
  });
2391
- });
2392
2262
  let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
2393
2263
  ? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
2394
2264
  : generatePairingPin();
2395
2265
  let pairingPinUpdatedAt = new Date().toISOString();
2396
- const consumeEnrollmentToken = expected => {
2397
- if (!timingSafeStringEqual(expected, pairToken)) return false;
2398
- pairToken = crypto.randomBytes(32).toString('base64url');
2399
- pairingPin = generatePairingPin();
2400
- pairingPinUpdatedAt = new Date().toISOString();
2401
- emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
2402
- pairingPinUpdatedAt,
2403
- hubId: deviceCredentialAuthority.hubId
2404
- });
2405
- return true;
2406
- };
2407
- const relayControl = options.relayControl && typeof options.relayControl === 'object'
2408
- ? options.relayControl
2409
- : allowLegacyPlaintextForTests
2410
- ? createHubRelayControl({
2411
- env,
2412
- pairToken,
2413
- logEvent,
2414
- logWarn,
2415
- onPeerSocket: socket => handleTcpAgentSocket(socket)
2416
- })
2417
- : createSecureHubRelayControl({
2418
- env,
2419
- authority: deviceCredentialAuthority,
2420
- getIdentity: () => getSecurityIdentity() || {},
2421
- getEnrollmentToken: () => pairToken,
2422
- consumeEnrollmentToken,
2423
- logEvent,
2424
- logWarn,
2425
- onPeerSocket: socket => handleTcpAgentSocket(socket)
2426
- });
2427
- const secureDirectAcceptor = createSecureDirectAcceptor({
2428
- authority: deviceCredentialAuthority,
2429
- getIdentity: () => getSecurityIdentity() || {},
2430
- getEnrollmentToken: () => pairToken,
2431
- consumeEnrollmentToken,
2432
- onSecureSocket: socket => handleTcpAgentSocket(socket),
2433
- onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
2434
- ...audit,
2435
- remoteHub: getStatus({ includeSecrets: false })
2436
- }),
2437
- logWarn
2438
- });
2439
2266
  const duplicateDeviceLogThrottleMs = clampNumber(
2440
2267
  env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
2441
2268
  5000,
@@ -2461,7 +2288,6 @@ export function createRemoteHub(options = {}) {
2461
2288
  const transportDiagnosticStartingDevices = new Set();
2462
2289
  const duplicateDeviceLogAt = new Map();
2463
2290
  let server = null;
2464
- let controlIdleSweepTimer = null;
2465
2291
  let started = false;
2466
2292
  let boundPort = requestedPort;
2467
2293
  let lastError = '';
@@ -2783,7 +2609,7 @@ export function createRemoteHub(options = {}) {
2783
2609
  host,
2784
2610
  agentHost: routeInfo.host || getAnnouncedHost(),
2785
2611
  port: boundPort || requestedPort,
2786
- protocol: 'secure-record-v1',
2612
+ protocol: 'tcp-jsonl',
2787
2613
  protocolVersion: REMOTE_PROTOCOL_VERSION,
2788
2614
  agentProtocol: REMOTE_AGENT_PROTOCOL,
2789
2615
  frameProtocol: buildRemoteFrameProtocolDescriptor(),
@@ -2803,15 +2629,6 @@ export function createRemoteHub(options = {}) {
2803
2629
  pairTokenPreview: maskToken(pairToken),
2804
2630
  pairingPin: includeSecrets ? pairingPin : '',
2805
2631
  pairingPinUpdatedAt,
2806
- security: {
2807
- direct: secureDirectAcceptor.getStatus(),
2808
- hubId: deviceCredentialAuthority.hubId,
2809
- hubPublicKeyFingerprint: crypto.createHash('sha256')
2810
- .update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
2811
- .digest('hex'),
2812
- enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
2813
- legacyPlaintextForTests: allowLegacyPlaintextForTests
2814
- },
2815
2632
  deviceCount: devices.size,
2816
2633
  connectedDeviceCount: connectedDevices,
2817
2634
  canvasDeviceListMode: 'all-devices',
@@ -4078,14 +3895,6 @@ export function createRemoteHub(options = {}) {
4078
3895
  }
4079
3896
 
4080
3897
  const writeOwner = device.inputWriteOwner;
4081
- if (device.controlOwnerConnectionId) {
4082
- endControlSession(
4083
- device,
4084
- device.controlOwnerConnectionId,
4085
- reason,
4086
- writeOwner,
4087
- writeOwner?.getBindingKey?.() || '');
4088
- }
4089
3898
  device.inputSocket = null;
4090
3899
  device.inputWriteOwner = null;
4091
3900
  device.inputSocketConnectionId = '';
@@ -4118,14 +3927,6 @@ export function createRemoteHub(options = {}) {
4118
3927
  }
4119
3928
 
4120
3929
  const writeOwner = device.inputWriteOwner;
4121
- if (device.controlOwnerConnectionId) {
4122
- endControlSession(
4123
- device,
4124
- device.controlOwnerConnectionId,
4125
- reason,
4126
- null,
4127
- '');
4128
- }
4129
3930
  device.inputSocket = null;
4130
3931
  device.inputWriteOwner = null;
4131
3932
  device.inputSocketConnectionId = '';
@@ -4559,10 +4360,6 @@ export function createRemoteHub(options = {}) {
4559
4360
  inputSocketConnectionId: '',
4560
4361
  inputOwnerConnectionId: '',
4561
4362
  inputOwnerBindingKey: '',
4562
- controlOwnerSessionId: '',
4563
- controlOwnerConnectionId: '',
4564
- controlOwnerStartedAtMs: 0,
4565
- controlOwnerLastInputAtMs: 0,
4566
4363
  frameSocket: null,
4567
4364
  audioSocket: null,
4568
4365
  fileSocket: null,
@@ -7484,12 +7281,6 @@ export function createRemoteHub(options = {}) {
7484
7281
  }
7485
7282
 
7486
7283
  if (!state.authenticated) {
7487
- const securityContext = socket.__liveDeskSecurityContext;
7488
- const secureDeviceAuthenticated = securityContext?.authenticated === true
7489
- && securityContext?.encrypted === true
7490
- && safeString(securityContext.accountId, 128)
7491
- && safeString(securityContext.hubId, 128)
7492
- && safeString(securityContext.deviceId, 128);
7493
7284
  if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7494
7285
  message = {
7495
7286
  ...message,
@@ -7502,9 +7293,8 @@ export function createRemoteHub(options = {}) {
7502
7293
  };
7503
7294
  }
7504
7295
  if (message.type === 'slot.assign') {
7505
- if (!secureDeviceAuthenticated
7506
- || safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7507
- writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
7296
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
7297
+ writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
7508
7298
  socket.end();
7509
7299
  return;
7510
7300
  }
@@ -7521,52 +7311,8 @@ export function createRemoteHub(options = {}) {
7521
7311
  return;
7522
7312
  }
7523
7313
 
7524
- if (secureDeviceAuthenticated) {
7525
- if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
7526
- writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
7527
- socket.destroy();
7528
- return;
7529
- }
7530
- const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
7531
- if (helloChannel !== securityContext.channel) {
7532
- writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
7533
- socket.destroy();
7534
- return;
7535
- }
7536
- } else {
7537
- const legacyAllowed = socket.__liveDeskRelayControl === true
7538
- || socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
7539
- if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
7540
- writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
7541
- socket.destroy();
7542
- return;
7543
- }
7544
- }
7545
-
7546
- const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
7547
- const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
7548
- ? message.capabilities
7549
- : {};
7550
- const incomingPolicy = getWelcomeDevicePolicy({
7551
- deviceId: incomingDeviceId,
7552
- capabilities: incomingCapabilities
7553
- }) || {};
7554
- const connectionDenied = connectionPolicyError(socket, incomingPolicy);
7555
- if (connectionDenied) {
7556
- const deniedTransport = socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp';
7557
- const deniedRemoteAddress = safeString(socket.remoteAddress, 256) || 'unknown';
7558
- logWarn('security', 'Remote connection rejected reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress);
7559
- writeJsonLine(socket, { type: 'error', error: connectionDenied });
7560
- emitEvent('RemoteSecurityPolicyAudit', {
7561
- result: 'rejected',
7562
- reason: connectionDenied,
7563
- deviceId: incomingDeviceId,
7564
- sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
7565
- accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
7566
- hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
7567
- transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7568
- remoteAddress: safeString(socket.remoteAddress, 256)
7569
- });
7314
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
7315
+ writeJsonLine(socket, { type: 'error', error: 'invalid-pair-token' });
7570
7316
  socket.destroy();
7571
7317
  return;
7572
7318
  }
@@ -7738,26 +7484,6 @@ export function createRemoteHub(options = {}) {
7738
7484
  }
7739
7485
  }
7740
7486
 
7741
- function enforceAgentMessageRate(socket, state, channel = 'control') {
7742
- if (state.messageRateGate.consume()) return true;
7743
- const device = state.device;
7744
- emitEvent('RemoteAbuseDefense', {
7745
- result: 'rejected',
7746
- reason: 'authenticated-message-rate-exceeded',
7747
- deviceId: device?.deviceId || '',
7748
- sessionId: device?.sessionId || '',
7749
- accountId: socket.__liveDeskSecurityContext?.accountId || '',
7750
- hubId: socket.__liveDeskSecurityContext?.hubId || '',
7751
- transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
7752
- channel,
7753
- remoteAddress: safeString(socket.remoteAddress, 256),
7754
- rate: state.messageRateGate.getStatus()
7755
- });
7756
- writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
7757
- socket.destroy();
7758
- return false;
7759
- }
7760
-
7761
7487
  function handleWebSocketAgentSocket(socket) {
7762
7488
  allSockets.add(socket);
7763
7489
  socket.setNoDelay(true);
@@ -7775,7 +7501,6 @@ export function createRemoteHub(options = {}) {
7775
7501
  binaryIngress: null,
7776
7502
  binaryQueueDrops: 0,
7777
7503
  binaryQueueEpoch: crypto.randomUUID(),
7778
- messageRateGate: createRemoteMessageRateGate(),
7779
7504
  closed: false
7780
7505
  };
7781
7506
  attachAgentBinaryIngress(socket, state);
@@ -7788,7 +7513,6 @@ export function createRemoteHub(options = {}) {
7788
7513
  }, 10000);
7789
7514
 
7790
7515
  socket.onTextMessage = text => {
7791
- if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
7792
7516
  try {
7793
7517
  handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
7794
7518
  } catch (err) {
@@ -7798,7 +7522,6 @@ export function createRemoteHub(options = {}) {
7798
7522
  };
7799
7523
 
7800
7524
  socket.onBinaryMessage = payload => {
7801
- if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
7802
7525
  try {
7803
7526
  const packet = parseRemoteHubWebSocketBinaryFrame(payload);
7804
7527
  if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
@@ -7957,7 +7680,6 @@ export function createRemoteHub(options = {}) {
7957
7680
  binaryIngress: null,
7958
7681
  binaryQueueDrops: 0,
7959
7682
  binaryQueueEpoch: crypto.randomUUID(),
7960
- messageRateGate: createRemoteMessageRateGate(),
7961
7683
  closed: false
7962
7684
  };
7963
7685
  attachAgentBinaryIngress(socket, state);
@@ -8075,15 +7797,6 @@ export function createRemoteHub(options = {}) {
8075
7797
 
8076
7798
  try {
8077
7799
  const message = parseJsonLine(lineBuffer.toString('utf8'));
8078
- if (!enforceAgentMessageRate(socket, state, state.frameOnly
8079
- ? 'frame'
8080
- : state.audioOnly
8081
- ? 'audio'
8082
- : state.inputOnly
8083
- ? 'input'
8084
- : state.fileOnly ? 'file' : 'control')) {
8085
- return;
8086
- }
8087
7800
  if (message?.type === 'frame.binary') {
8088
7801
  const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
8089
7802
  const maxBytes = frameKind === 'thumbnail'
@@ -8139,32 +7852,13 @@ export function createRemoteHub(options = {}) {
8139
7852
  }
8140
7853
 
8141
7854
  function handleSocket(socket) {
8142
- if (!allowLegacyPlaintextForTests) {
8143
- secureDirectAcceptor.accept(socket);
8144
- return;
8145
- }
8146
7855
  socket.once('data', chunk => {
8147
7856
  const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8148
- if (isSecureHandshakeStart(firstChunk)) {
8149
- secureDirectAcceptor.accept(socket, firstChunk);
8150
- return;
8151
- }
8152
7857
  if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8153
- if (!allowLegacyPlaintextForTests) {
8154
- socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
8155
- socket.destroy();
8156
- return;
8157
- }
8158
- socket.__liveDeskLegacyPlaintext = true;
8159
7858
  handleWebSocketUpgradeSocket(socket, firstChunk);
8160
7859
  return;
8161
7860
  }
8162
- if (!allowLegacyPlaintextForTests) {
8163
- writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
8164
- socket.destroy();
8165
- return;
8166
- }
8167
- socket.__liveDeskLegacyPlaintext = true;
7861
+
8168
7862
  handleTcpAgentSocket(socket, firstChunk);
8169
7863
  });
8170
7864
 
@@ -8188,9 +7882,9 @@ export function createRemoteHub(options = {}) {
8188
7882
  started = true;
8189
7883
  boundPort = candidateServer.address()?.port || port;
8190
7884
  lastError = '';
8191
- logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
7885
+ logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
8192
7886
  if (host === '0.0.0.0' || host === '::') {
8193
- logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
7887
+ logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
8194
7888
  }
8195
7889
  emitRemoteEvent('RemoteHubStarted', null);
8196
7890
  resolve();
@@ -8206,10 +7900,6 @@ export function createRemoteHub(options = {}) {
8206
7900
  try {
8207
7901
  await udpTransport?.start?.();
8208
7902
  await listenOnPort(requestedPort);
8209
- if (!controlIdleSweepTimer) {
8210
- controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
8211
- controlIdleSweepTimer.unref?.();
8212
- }
8213
7903
  try {
8214
7904
  await relayControl?.start?.();
8215
7905
  } catch {
@@ -8232,10 +7922,6 @@ export function createRemoteHub(options = {}) {
8232
7922
  }
8233
7923
 
8234
7924
  async function close() {
8235
- if (controlIdleSweepTimer) {
8236
- clearInterval(controlIdleSweepTimer);
8237
- controlIdleSweepTimer = null;
8238
- }
8239
7925
  for (const state of [...agentBinaryIngressStates]) {
8240
7926
  closeAgentBinaryIngress(state, 'hub-shutdown');
8241
7927
  }
@@ -8442,60 +8128,6 @@ export function createRemoteHub(options = {}) {
8442
8128
  return { ok: true, commandId };
8443
8129
  }
8444
8130
 
8445
- async function sendCommandAwaitResult(deviceId, command, options = {}) {
8446
- const normalizedDeviceId = safeString(deviceId, 160);
8447
- const device = devices.get(normalizedDeviceId);
8448
- const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
8449
- const commandWithId = { ...(command || {}), commandId };
8450
-
8451
- if (device?.synthetic === true && device.connected) {
8452
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8453
- return {
8454
- ...sent,
8455
- queued: sent.ok === true,
8456
- acknowledged: sent.ok === true,
8457
- acknowledgement: sent.ok === true
8458
- ? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
8459
- : { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
8460
- };
8461
- }
8462
-
8463
- if (!device?.socket || device.socket.destroyed || !device.connected) {
8464
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8465
- return {
8466
- ...sent,
8467
- queued: false,
8468
- acknowledged: false,
8469
- acknowledgement: {
8470
- ok: false,
8471
- commandId,
8472
- result: null,
8473
- error: sent.error || 'device-not-connected'
8474
- }
8475
- };
8476
- }
8477
-
8478
- const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
8479
- // Register before writing: a loopback Agent can return command.result in
8480
- // the same event-loop turn as the command write.
8481
- const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
8482
- const sent = sendCommand(normalizedDeviceId, commandWithId);
8483
- if (!sent.ok) {
8484
- failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
8485
- }
8486
- const acknowledgement = await acknowledgementPromise;
8487
- return {
8488
- ...sent,
8489
- ok: sent.ok === true && acknowledgement.ok === true,
8490
- queued: sent.ok === true,
8491
- acknowledged: acknowledgement.ok === true,
8492
- acknowledgement,
8493
- error: acknowledgement.ok === true
8494
- ? undefined
8495
- : acknowledgement.error || sent.error || 'command-result-failed'
8496
- };
8497
- }
8498
-
8499
8131
  function refreshDevicePolicies(deviceIds = undefined) {
8500
8132
  const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
8501
8133
  ? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
@@ -8507,31 +8139,6 @@ export function createRemoteHub(options = {}) {
8507
8139
  if (!device?.socket || device.socket.destroyed || !device.connected) continue;
8508
8140
  total += 1;
8509
8141
  const effectivePolicy = getDevicePolicy(device);
8510
- const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
8511
- if (connectionDenied) {
8512
- const deniedTransport = device.controlTransport || 'direct-tcp';
8513
- const deniedRemoteAddress = safeString(device.remoteAddress, 256) || 'unknown';
8514
- logWarn('security', 'Connected device rejected by refreshed policy reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress + ' device=' + device.deviceId);
8515
- emitEvent('RemoteSecurityPolicyAudit', {
8516
- result: 'rejected',
8517
- reason: connectionDenied,
8518
- deviceId: device.deviceId,
8519
- sessionId: device.sessionId,
8520
- transport: device.controlTransport,
8521
- remoteAddress: device.remoteAddress
8522
- });
8523
- disconnectDevice(device.deviceId, connectionDenied);
8524
- continue;
8525
- }
8526
- if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
8527
- releaseInputOwner(
8528
- device.deviceId,
8529
- device.controlOwnerConnectionId,
8530
- 'control-disabled-by-policy');
8531
- }
8532
- if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
8533
- stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
8534
- }
8535
8142
  if (writeJsonLine(device.socket, {
8536
8143
  type: 'policy.update',
8537
8144
  effectivePolicy,
@@ -8636,112 +8243,6 @@ export function createRemoteHub(options = {}) {
8636
8243
  };
8637
8244
  }
8638
8245
 
8639
- function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
8640
- const policy = getDevicePolicy(device);
8641
- return {
8642
- type,
8643
- payload: {
8644
- controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
8645
- hubConnectionId: safeString(ownerConnectionId, 128),
8646
- deviceId: safeString(device?.deviceId, 160),
8647
- visibleIndicator: true,
8648
- lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
8649
- idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
8650
- reason: safeString(reason, 160),
8651
- issuedAt: new Date().toISOString()
8652
- }
8653
- };
8654
- }
8655
-
8656
- function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
8657
- const message = controlSessionMessage(device, ownerConnectionId, type, reason);
8658
- if (inputWriteOwner && bindingKey) {
8659
- return inputWriteOwner.enqueue(message, bindingKey);
8660
- }
8661
- if (!device?.socket || device.socket.destroyed) {
8662
- return { ok: false, error: 'device-not-connected' };
8663
- }
8664
- const sent = writeJsonLine(device.socket, {
8665
- type: 'command',
8666
- commandId: crypto.randomUUID(),
8667
- command: type,
8668
- payload: message.payload,
8669
- issuedAt: new Date().toISOString()
8670
- });
8671
- return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
8672
- }
8673
-
8674
- function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
8675
- const owner = safeString(ownerConnectionId, 128);
8676
- if (!device || !owner) return { ok: false, error: 'control-owner-required' };
8677
- if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
8678
- return { ok: true, alreadyActive: true };
8679
- }
8680
- device.controlOwnerSessionId = crypto.randomUUID();
8681
- device.controlOwnerConnectionId = owner;
8682
- device.controlOwnerStartedAtMs = Date.now();
8683
- device.controlOwnerLastInputAtMs = Date.now();
8684
- const sent = sendControlSessionMessage(
8685
- device,
8686
- owner,
8687
- 'control.session.start',
8688
- 'browser-control-owner-active',
8689
- inputWriteOwner,
8690
- bindingKey);
8691
- if (!sent.ok) {
8692
- device.controlOwnerSessionId = '';
8693
- device.controlOwnerConnectionId = '';
8694
- device.controlOwnerStartedAtMs = 0;
8695
- device.controlOwnerLastInputAtMs = 0;
8696
- return sent;
8697
- }
8698
- emitRemoteEvent('RemoteControlSessionStarted', device, {
8699
- controlOwnerSessionId: device.controlOwnerSessionId,
8700
- hubConnectionId: owner
8701
- });
8702
- return sent;
8703
- }
8704
-
8705
- function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
8706
- const owner = safeString(ownerConnectionId, 128);
8707
- if (!device?.controlOwnerSessionId
8708
- || !owner
8709
- || device.controlOwnerConnectionId !== owner) {
8710
- return { ok: false, error: 'control-owner-not-current' };
8711
- }
8712
- const controlOwnerSessionId = device.controlOwnerSessionId;
8713
- const sent = sendControlSessionMessage(
8714
- device,
8715
- owner,
8716
- 'control.session.stop',
8717
- reason,
8718
- inputWriteOwner,
8719
- bindingKey);
8720
- device.controlOwnerSessionId = '';
8721
- device.controlOwnerConnectionId = '';
8722
- device.controlOwnerStartedAtMs = 0;
8723
- device.controlOwnerLastInputAtMs = 0;
8724
- emitRemoteEvent('RemoteControlSessionEnded', device, {
8725
- controlOwnerSessionId,
8726
- hubConnectionId: owner,
8727
- reason,
8728
- delivered: sent.ok === true
8729
- });
8730
- return sent;
8731
- }
8732
-
8733
- function sweepIdleControlSessions() {
8734
- const nowMs = Date.now();
8735
- for (const device of devices.values()) {
8736
- if (!device?.connected || !device.controlOwnerConnectionId) continue;
8737
- const policy = getDevicePolicy(device);
8738
- if (policy.disconnectIdleControlSessions !== true) continue;
8739
- const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
8740
- if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
8741
- releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
8742
- }
8743
- }
8744
-
8745
8246
  function getCurrentInputWriteOwner(device) {
8746
8247
  if (!device?.inputSocket
8747
8248
  || device.inputSocket.destroyed
@@ -8889,9 +8390,6 @@ export function createRemoteHub(options = {}) {
8889
8390
  const bindingChanged = previousInputBindingKey
8890
8391
  && previousInputBindingKey !== activeInputBindingKey;
8891
8392
  const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
8892
- const startingControlOwner = !!normalized.hubConnectionId
8893
- && (!device.controlOwnerConnectionId
8894
- || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8895
8393
  if (ownerChanged || bindingChanged || writeBindingChanged) {
8896
8394
  const resetSent = inputWriteOwner.replaceBinding(
8897
8395
  activeInputBindingKey,
@@ -8909,25 +8407,6 @@ export function createRemoteHub(options = {}) {
8909
8407
  closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
8910
8408
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8911
8409
  }
8912
- if (startingControlOwner) {
8913
- if (device.controlOwnerConnectionId) {
8914
- endControlSession(
8915
- device,
8916
- device.controlOwnerConnectionId,
8917
- 'browser-input-owner-changed',
8918
- inputWriteOwner,
8919
- activeInputBindingKey);
8920
- }
8921
- const started = beginControlSession(
8922
- device,
8923
- normalized.hubConnectionId,
8924
- inputWriteOwner,
8925
- activeInputBindingKey);
8926
- if (!started.ok) {
8927
- closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
8928
- return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8929
- }
8930
- }
8931
8410
  }
8932
8411
  const sent = inputWriteOwner.enqueue({
8933
8412
  type: 'input.control',
@@ -8942,7 +8421,6 @@ export function createRemoteHub(options = {}) {
8942
8421
  device.inputOwnerConnectionId = normalized.hubConnectionId;
8943
8422
  }
8944
8423
  device.inputOwnerBindingKey = activeInputBindingKey;
8945
- device.controlOwnerLastInputAtMs = Date.now();
8946
8424
  device.counters.commandsSent += 1;
8947
8425
  device.inputLastSeenAt = new Date().toISOString();
8948
8426
  return {
@@ -8972,25 +8450,6 @@ export function createRemoteHub(options = {}) {
8972
8450
  }
8973
8451
 
8974
8452
  const hubForwardedAtEpochMs = Date.now();
8975
- const startingFallbackOwner = !!normalized.hubConnectionId
8976
- && (!device.controlOwnerConnectionId
8977
- || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8978
- if (startingFallbackOwner) {
8979
- if (device.controlOwnerConnectionId) {
8980
- endControlSession(
8981
- device,
8982
- device.controlOwnerConnectionId,
8983
- 'browser-input-owner-changed',
8984
- null,
8985
- '');
8986
- }
8987
- const started = beginControlSession(
8988
- device,
8989
- normalized.hubConnectionId,
8990
- null,
8991
- '');
8992
- if (!started.ok) return started;
8993
- }
8994
8453
  const fallback = sendCommand(deviceId, {
8995
8454
  command: 'input.control',
8996
8455
  payload: {
@@ -9014,22 +8473,6 @@ export function createRemoteHub(options = {}) {
9014
8473
  timeoutTimer: null
9015
8474
  });
9016
8475
  }
9017
- if (fallback.ok) {
9018
- device.inputOwnerConnectionId = normalized.hubConnectionId;
9019
- device.inputOwnerBindingKey = buildRemoteInputBindingKey(
9020
- device,
9021
- controlStream,
9022
- normalized,
9023
- activeMonitorIndex);
9024
- device.controlOwnerLastInputAtMs = Date.now();
9025
- } else if (startingFallbackOwner) {
9026
- endControlSession(
9027
- device,
9028
- normalized.hubConnectionId,
9029
- 'input-control-delivery-failed',
9030
- null,
9031
- '');
9032
- }
9033
8476
  return fallback.ok
9034
8477
  ? {
9035
8478
  ...fallback,
@@ -9056,17 +8499,12 @@ export function createRemoteHub(options = {}) {
9056
8499
  return { ok: false, error: 'input-owner-not-current' };
9057
8500
  }
9058
8501
 
8502
+ device.inputOwnerConnectionId = '';
8503
+ device.inputOwnerBindingKey = '';
9059
8504
  const inputSocket = device.inputSocket;
9060
8505
  const inputWriteOwner = getCurrentInputWriteOwner(device);
9061
8506
  if (!inputSocket || !inputWriteOwner) {
9062
- const ended = endControlSession(device, owner, reason, null, '');
9063
- device.inputOwnerConnectionId = '';
9064
- device.inputOwnerBindingKey = '';
9065
- return {
9066
- ok: ended.ok !== false,
9067
- queued: false,
9068
- controlSessionEnded: ended.ok === true
9069
- };
8507
+ return { ok: true, queued: false };
9070
8508
  }
9071
8509
  const controlStream = getActiveControlStream(device);
9072
8510
  const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
@@ -9082,19 +8520,10 @@ export function createRemoteHub(options = {}) {
9082
8520
  closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
9083
8521
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
9084
8522
  }
9085
- const ended = endControlSession(
9086
- device,
9087
- owner,
9088
- reason,
9089
- inputWriteOwner,
9090
- releaseBindingKey);
9091
- device.inputOwnerConnectionId = '';
9092
- device.inputOwnerBindingKey = '';
9093
8523
  return {
9094
- ok: ended.ok !== false,
8524
+ ok: true,
9095
8525
  queued: true,
9096
- backpressured: sent.backpressured === true || ended.backpressured === true,
9097
- controlSessionEnded: ended.ok === true
8526
+ backpressured: sent.backpressured === true
9098
8527
  };
9099
8528
  }
9100
8529
 
@@ -9137,9 +8566,6 @@ export function createRemoteHub(options = {}) {
9137
8566
  }
9138
8567
  const device = devices.get(String(deviceId || ''));
9139
8568
  const requestedOperation = safeString(options.operation, 80);
9140
- if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
9141
- return { ok: false, error: 'agent-mutating-tool-disabled' };
9142
- }
9143
8569
  const operation = normalizeAgentOperation(requestedOperation);
9144
8570
  if (requestedOperation && !operation) {
9145
8571
  return { ok: false, error: 'unsupported-agent-operation' };
@@ -11388,7 +10814,6 @@ export function createRemoteHub(options = {}) {
11388
10814
  disconnectDevice,
11389
10815
  assignDeviceSlot,
11390
10816
  sendCommand,
11391
- sendCommandAwaitResult,
11392
10817
  refreshDevicePolicies,
11393
10818
  sendLegacyClientUpdate,
11394
10819
  sendInputControl,
@@ -11417,16 +10842,7 @@ export function createRemoteHub(options = {}) {
11417
10842
  seedSyntheticFleet,
11418
10843
  clearSyntheticFleet,
11419
10844
  getPairToken: () => pairToken,
11420
- getPairingPin: () => pairingPin,
11421
- getSecurityStatus: () => ({
11422
- hubId: deviceCredentialAuthority.hubId,
11423
- hubPublicKey: deviceCredentialAuthority.hubPublicKey,
11424
- hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
11425
- direct: secureDirectAcceptor.getStatus(),
11426
- devices: deviceCredentialAuthority.listDevices()
11427
- }),
11428
- revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
11429
- clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
10845
+ getPairingPin: () => pairingPin
11430
10846
  };
11431
10847
  }
11432
10848