@livedesk/hub 0.1.36 → 0.1.38

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;
@@ -1170,9 +1119,19 @@ function getAccountRouteEndpointReason(endpoint, context = {}) {
1170
1119
  return 'ok';
1171
1120
  }
1172
1121
 
1173
- function safeText(value, maxLength = 1000) {
1174
- return String(value ?? '').replace(/\0/g, '').trim().slice(0, maxLength);
1175
- }
1122
+ function safeText(value, maxLength = 1000) {
1123
+ return String(value ?? '').replace(/\0/g, '').trim().slice(0, maxLength);
1124
+ }
1125
+
1126
+ function normalizeRemoteKeyboardKey(value) {
1127
+ const raw = String(value ?? '').replace(/\0/g, '');
1128
+ return raw === ' ' ? ' ' : safeString(raw, 80);
1129
+ }
1130
+
1131
+ function normalizeRemoteKeyboardText(value) {
1132
+ const raw = String(value ?? '').replace(/\0/g, '');
1133
+ return raw === ' ' ? ' ' : safeText(raw, 256);
1134
+ }
1176
1135
 
1177
1136
  function safeTaskData(value) {
1178
1137
  if (value === undefined || value === null) return undefined;
@@ -1213,24 +1172,21 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
1213
1172
  'process.list',
1214
1173
  'service.status',
1215
1174
  '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
1175
  'process.control',
1224
1176
  'service.control',
1225
1177
  'application.launch',
1226
1178
  'application.close',
1179
+ 'file.read',
1227
1180
  'file.write',
1228
1181
  'file.delete',
1182
+ 'file.list',
1229
1183
  'command.run',
1230
1184
  'script.run',
1231
1185
  'software.install',
1186
+ 'network.status',
1232
1187
  'system.power',
1233
- 'system.configure'
1188
+ 'system.configure',
1189
+ 'logs.collect'
1234
1190
  ]);
1235
1191
 
1236
1192
  function normalizeAgentOperation(value) {
@@ -1256,14 +1212,14 @@ function readRawRemoteInputMonitorIndex(input) {
1256
1212
  return undefined;
1257
1213
  }
1258
1214
 
1259
- function normalizeRemoteInputEvent(value = {}) {
1215
+ function normalizeRemoteInputEvent(value = {}) {
1260
1216
  const input = value && typeof value === 'object' ? value : {};
1261
1217
  const type = safeString(input.type || input.Type, 48);
1262
1218
  const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
1263
1219
  const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
1264
1220
  const deltaX = Number(input.deltaX ?? input.DeltaX);
1265
1221
  const deltaY = Number(input.deltaY ?? input.DeltaY);
1266
- const keyCode = Number(input.keyCode ?? input.KeyCode);
1222
+ const keyCode = Number(input.keyCode ?? input.KeyCode);
1267
1223
  const location = Number(input.location ?? input.Location);
1268
1224
  const monitorIndex = parseExactLiveStreamMonitorIndex(readRawRemoteInputMonitorIndex(input));
1269
1225
  const inputSeq = Number(input.inputSeq ?? input.InputSeq);
@@ -1271,13 +1227,18 @@ function normalizeRemoteInputEvent(value = {}) {
1271
1227
  const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
1272
1228
  const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
1273
1229
  const rawPressedCodes = input.pressedCodes ?? input.PressedCodes;
1274
- const pressedCodes = Array.isArray(rawPressedCodes)
1230
+ const pressedCodes = Array.isArray(rawPressedCodes)
1275
1231
  ? [...new Set(rawPressedCodes
1276
1232
  .map(code => safeString(code, 80))
1277
1233
  .filter(Boolean))]
1278
1234
  .slice(0, 64)
1279
- : null;
1280
- return {
1235
+ : null;
1236
+ const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
1237
+ let code = safeString(input.code ?? input.Code, 80);
1238
+ if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
1239
+ code = 'Space';
1240
+ }
1241
+ return {
1281
1242
  type,
1282
1243
  // A missing monitor is not equivalent to the primary display. Control
1283
1244
  // input must prove the exact monitor owned by its capture generation.
@@ -1287,11 +1248,11 @@ function normalizeRemoteInputEvent(value = {}) {
1287
1248
  button: safeString(input.button || input.Button, 24),
1288
1249
  deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
1289
1250
  deltaY: Number.isFinite(deltaY) ? Math.max(-4096, Math.min(4096, deltaY)) : 0,
1290
- key: safeString(input.key || input.Key, 80),
1291
- code: safeString(input.code || input.Code, 80),
1251
+ key,
1252
+ code,
1292
1253
  keyCode: Number.isFinite(keyCode) ? Math.max(0, Math.min(65535, Math.trunc(keyCode))) : 0,
1293
1254
  location: Number.isFinite(location) ? Math.max(0, Math.min(255, Math.trunc(location))) : 0,
1294
- text: safeText(input.text || input.Text, 256),
1255
+ text: normalizeRemoteKeyboardText(input.text ?? input.Text),
1295
1256
  repeat: input.repeat === true || input.Repeat === true,
1296
1257
  pressedCodes,
1297
1258
  shiftKey: input.shiftKey === true || input.ShiftKey === true,
@@ -1402,7 +1363,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
1402
1363
  return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
1403
1364
  }
1404
1365
 
1405
- function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1366
+ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1406
1367
  if (!frame) {
1407
1368
  return null;
1408
1369
  }
@@ -1424,27 +1385,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
1424
1385
  serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
1425
1386
  }
1426
1387
 
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
- }
1388
+ return serialized;
1389
+ }
1390
+
1391
+ function serializeActiveLiveStream(activeLiveStream, deviceId) {
1392
+ if (!activeLiveStream) {
1393
+ return null;
1394
+ }
1395
+
1396
+ return {
1397
+ ...activeLiveStream,
1398
+ // The active descriptor is status metadata. Preserve the Hub's exact
1399
+ // internal frame owner for delivery, but never copy encoded bytes or
1400
+ // its access token into a browser status snapshot.
1401
+ latestFrame: serializeRemoteFrame(
1402
+ activeLiveStream.latestFrame,
1403
+ deviceId,
1404
+ 'live',
1405
+ { includeDataUrl: false }
1406
+ )
1407
+ };
1408
+ }
1448
1409
 
1449
1410
  function getRecentFrameCache(device, frameKind) {
1450
1411
  if (!device) {
@@ -1478,7 +1439,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
1478
1439
  : RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
1479
1440
  }
1480
1441
 
1481
- export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1442
+ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1482
1443
  if (!Array.isArray(cache)) {
1483
1444
  return;
1484
1445
  }
@@ -1499,20 +1460,6 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1499
1460
  }
1500
1461
  }
1501
1462
 
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
1463
  for (let index = 0; index < cache.length; index += 1) {
1517
1464
  const entry = cache[index];
1518
1465
  const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
@@ -1524,7 +1471,7 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1524
1471
  }
1525
1472
  }
1526
1473
 
1527
- export function rememberRecentFramePayload(device, frameKind, frame) {
1474
+ function rememberRecentFramePayload(device, frameKind, frame) {
1528
1475
  if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
1529
1476
  return;
1530
1477
  }
@@ -1560,7 +1507,7 @@ export function rememberRecentFramePayload(device, frameKind, frame) {
1560
1507
  pruneRecentFrameCache(cache, kind, nowMs);
1561
1508
  }
1562
1509
 
1563
- export function isFramePayloadRequestMatch(frame, options = {}) {
1510
+ function isFramePayloadRequestMatch(frame, options = {}) {
1564
1511
  if (!frame || !Buffer.isBuffer(frame.payload)) {
1565
1512
  return false;
1566
1513
  }
@@ -1579,37 +1526,10 @@ export function isFramePayloadRequestMatch(frame, options = {}) {
1579
1526
  return false;
1580
1527
  }
1581
1528
 
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
1529
  return true;
1610
1530
  }
1611
1531
 
1612
- export function findRecentFramePayload(device, frameKind, options = {}) {
1532
+ function findRecentFramePayload(device, frameKind, options = {}) {
1613
1533
  const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
1614
1534
  const latest = kind === 'thumbnail'
1615
1535
  ? device?.latestThumbnail
@@ -1620,14 +1540,7 @@ export function findRecentFramePayload(device, frameKind, options = {}) {
1620
1540
 
1621
1541
  const token = safeString(options.token, 128);
1622
1542
  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) {
1543
+ if (!token && !Number.isFinite(requestedSeq)) {
1631
1544
  return null;
1632
1545
  }
1633
1546
 
@@ -1683,7 +1596,7 @@ function serializeDevice(device, options = {}) {
1683
1596
  },
1684
1597
  latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
1685
1598
  latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
1686
- activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1599
+ activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
1687
1600
  liveCapturePause: device.liveCapturePause
1688
1601
  ? {
1689
1602
  token: device.liveCapturePause.token,
@@ -2301,16 +2214,6 @@ export function createRemoteHub(options = {}) {
2301
2214
  }
2302
2215
  }
2303
2216
 
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
2217
  function policyError(device, permission = '', command = '') {
2315
2218
  const policy = getDevicePolicy(device);
2316
2219
  const commandName = safeString(command, 80);
@@ -2359,83 +2262,22 @@ export function createRemoteHub(options = {}) {
2359
2262
  const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
2360
2263
  const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
2361
2264
  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,
2265
+ const pairToken = safeString(
2266
+ options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
2364
2267
  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)
2268
+ const relayControl = options.relayControl && typeof options.relayControl === 'object'
2269
+ ? options.relayControl
2270
+ : createHubRelayControl({
2271
+ env,
2272
+ pairToken,
2273
+ logEvent,
2274
+ logWarn,
2275
+ onPeerSocket: socket => handleTcpAgentSocket(socket)
2390
2276
  });
2391
- });
2392
2277
  let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
2393
2278
  ? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
2394
2279
  : generatePairingPin();
2395
2280
  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
2281
  const duplicateDeviceLogThrottleMs = clampNumber(
2440
2282
  env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
2441
2283
  5000,
@@ -2461,7 +2303,6 @@ export function createRemoteHub(options = {}) {
2461
2303
  const transportDiagnosticStartingDevices = new Set();
2462
2304
  const duplicateDeviceLogAt = new Map();
2463
2305
  let server = null;
2464
- let controlIdleSweepTimer = null;
2465
2306
  let started = false;
2466
2307
  let boundPort = requestedPort;
2467
2308
  let lastError = '';
@@ -2783,7 +2624,7 @@ export function createRemoteHub(options = {}) {
2783
2624
  host,
2784
2625
  agentHost: routeInfo.host || getAnnouncedHost(),
2785
2626
  port: boundPort || requestedPort,
2786
- protocol: 'secure-record-v1',
2627
+ protocol: 'tcp-jsonl',
2787
2628
  protocolVersion: REMOTE_PROTOCOL_VERSION,
2788
2629
  agentProtocol: REMOTE_AGENT_PROTOCOL,
2789
2630
  frameProtocol: buildRemoteFrameProtocolDescriptor(),
@@ -2803,15 +2644,6 @@ export function createRemoteHub(options = {}) {
2803
2644
  pairTokenPreview: maskToken(pairToken),
2804
2645
  pairingPin: includeSecrets ? pairingPin : '',
2805
2646
  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
2647
  deviceCount: devices.size,
2816
2648
  connectedDeviceCount: connectedDevices,
2817
2649
  canvasDeviceListMode: 'all-devices',
@@ -4078,14 +3910,6 @@ export function createRemoteHub(options = {}) {
4078
3910
  }
4079
3911
 
4080
3912
  const writeOwner = device.inputWriteOwner;
4081
- if (device.controlOwnerConnectionId) {
4082
- endControlSession(
4083
- device,
4084
- device.controlOwnerConnectionId,
4085
- reason,
4086
- writeOwner,
4087
- writeOwner?.getBindingKey?.() || '');
4088
- }
4089
3913
  device.inputSocket = null;
4090
3914
  device.inputWriteOwner = null;
4091
3915
  device.inputSocketConnectionId = '';
@@ -4118,14 +3942,6 @@ export function createRemoteHub(options = {}) {
4118
3942
  }
4119
3943
 
4120
3944
  const writeOwner = device.inputWriteOwner;
4121
- if (device.controlOwnerConnectionId) {
4122
- endControlSession(
4123
- device,
4124
- device.controlOwnerConnectionId,
4125
- reason,
4126
- null,
4127
- '');
4128
- }
4129
3945
  device.inputSocket = null;
4130
3946
  device.inputWriteOwner = null;
4131
3947
  device.inputSocketConnectionId = '';
@@ -4559,10 +4375,6 @@ export function createRemoteHub(options = {}) {
4559
4375
  inputSocketConnectionId: '',
4560
4376
  inputOwnerConnectionId: '',
4561
4377
  inputOwnerBindingKey: '',
4562
- controlOwnerSessionId: '',
4563
- controlOwnerConnectionId: '',
4564
- controlOwnerStartedAtMs: 0,
4565
- controlOwnerLastInputAtMs: 0,
4566
4378
  frameSocket: null,
4567
4379
  audioSocket: null,
4568
4380
  fileSocket: null,
@@ -7484,12 +7296,6 @@ export function createRemoteHub(options = {}) {
7484
7296
  }
7485
7297
 
7486
7298
  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
7299
  if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7494
7300
  message = {
7495
7301
  ...message,
@@ -7502,9 +7308,8 @@ export function createRemoteHub(options = {}) {
7502
7308
  };
7503
7309
  }
7504
7310
  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' });
7311
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
7312
+ writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
7508
7313
  socket.end();
7509
7314
  return;
7510
7315
  }
@@ -7521,52 +7326,8 @@ export function createRemoteHub(options = {}) {
7521
7326
  return;
7522
7327
  }
7523
7328
 
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
- });
7329
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
7330
+ writeJsonLine(socket, { type: 'error', error: 'invalid-pair-token' });
7570
7331
  socket.destroy();
7571
7332
  return;
7572
7333
  }
@@ -7738,26 +7499,6 @@ export function createRemoteHub(options = {}) {
7738
7499
  }
7739
7500
  }
7740
7501
 
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
7502
  function handleWebSocketAgentSocket(socket) {
7762
7503
  allSockets.add(socket);
7763
7504
  socket.setNoDelay(true);
@@ -7775,7 +7516,6 @@ export function createRemoteHub(options = {}) {
7775
7516
  binaryIngress: null,
7776
7517
  binaryQueueDrops: 0,
7777
7518
  binaryQueueEpoch: crypto.randomUUID(),
7778
- messageRateGate: createRemoteMessageRateGate(),
7779
7519
  closed: false
7780
7520
  };
7781
7521
  attachAgentBinaryIngress(socket, state);
@@ -7788,7 +7528,6 @@ export function createRemoteHub(options = {}) {
7788
7528
  }, 10000);
7789
7529
 
7790
7530
  socket.onTextMessage = text => {
7791
- if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
7792
7531
  try {
7793
7532
  handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
7794
7533
  } catch (err) {
@@ -7798,7 +7537,6 @@ export function createRemoteHub(options = {}) {
7798
7537
  };
7799
7538
 
7800
7539
  socket.onBinaryMessage = payload => {
7801
- if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
7802
7540
  try {
7803
7541
  const packet = parseRemoteHubWebSocketBinaryFrame(payload);
7804
7542
  if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
@@ -7957,7 +7695,6 @@ export function createRemoteHub(options = {}) {
7957
7695
  binaryIngress: null,
7958
7696
  binaryQueueDrops: 0,
7959
7697
  binaryQueueEpoch: crypto.randomUUID(),
7960
- messageRateGate: createRemoteMessageRateGate(),
7961
7698
  closed: false
7962
7699
  };
7963
7700
  attachAgentBinaryIngress(socket, state);
@@ -8075,15 +7812,6 @@ export function createRemoteHub(options = {}) {
8075
7812
 
8076
7813
  try {
8077
7814
  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
7815
  if (message?.type === 'frame.binary') {
8088
7816
  const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
8089
7817
  const maxBytes = frameKind === 'thumbnail'
@@ -8139,32 +7867,13 @@ export function createRemoteHub(options = {}) {
8139
7867
  }
8140
7868
 
8141
7869
  function handleSocket(socket) {
8142
- if (!allowLegacyPlaintextForTests) {
8143
- secureDirectAcceptor.accept(socket);
8144
- return;
8145
- }
8146
7870
  socket.once('data', chunk => {
8147
7871
  const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8148
- if (isSecureHandshakeStart(firstChunk)) {
8149
- secureDirectAcceptor.accept(socket, firstChunk);
8150
- return;
8151
- }
8152
7872
  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
7873
  handleWebSocketUpgradeSocket(socket, firstChunk);
8160
7874
  return;
8161
7875
  }
8162
- if (!allowLegacyPlaintextForTests) {
8163
- writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
8164
- socket.destroy();
8165
- return;
8166
- }
8167
- socket.__liveDeskLegacyPlaintext = true;
7876
+
8168
7877
  handleTcpAgentSocket(socket, firstChunk);
8169
7878
  });
8170
7879
 
@@ -8188,9 +7897,9 @@ export function createRemoteHub(options = {}) {
8188
7897
  started = true;
8189
7898
  boundPort = candidateServer.address()?.port || port;
8190
7899
  lastError = '';
8191
- logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
7900
+ logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
8192
7901
  if (host === '0.0.0.0' || host === '::') {
8193
- logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
7902
+ logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
8194
7903
  }
8195
7904
  emitRemoteEvent('RemoteHubStarted', null);
8196
7905
  resolve();
@@ -8206,10 +7915,6 @@ export function createRemoteHub(options = {}) {
8206
7915
  try {
8207
7916
  await udpTransport?.start?.();
8208
7917
  await listenOnPort(requestedPort);
8209
- if (!controlIdleSweepTimer) {
8210
- controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
8211
- controlIdleSweepTimer.unref?.();
8212
- }
8213
7918
  try {
8214
7919
  await relayControl?.start?.();
8215
7920
  } catch {
@@ -8232,10 +7937,6 @@ export function createRemoteHub(options = {}) {
8232
7937
  }
8233
7938
 
8234
7939
  async function close() {
8235
- if (controlIdleSweepTimer) {
8236
- clearInterval(controlIdleSweepTimer);
8237
- controlIdleSweepTimer = null;
8238
- }
8239
7940
  for (const state of [...agentBinaryIngressStates]) {
8240
7941
  closeAgentBinaryIngress(state, 'hub-shutdown');
8241
7942
  }
@@ -8442,60 +8143,6 @@ export function createRemoteHub(options = {}) {
8442
8143
  return { ok: true, commandId };
8443
8144
  }
8444
8145
 
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
8146
  function refreshDevicePolicies(deviceIds = undefined) {
8500
8147
  const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
8501
8148
  ? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
@@ -8507,31 +8154,6 @@ export function createRemoteHub(options = {}) {
8507
8154
  if (!device?.socket || device.socket.destroyed || !device.connected) continue;
8508
8155
  total += 1;
8509
8156
  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
8157
  if (writeJsonLine(device.socket, {
8536
8158
  type: 'policy.update',
8537
8159
  effectivePolicy,
@@ -8636,112 +8258,6 @@ export function createRemoteHub(options = {}) {
8636
8258
  };
8637
8259
  }
8638
8260
 
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
8261
  function getCurrentInputWriteOwner(device) {
8746
8262
  if (!device?.inputSocket
8747
8263
  || device.inputSocket.destroyed
@@ -8889,9 +8405,6 @@ export function createRemoteHub(options = {}) {
8889
8405
  const bindingChanged = previousInputBindingKey
8890
8406
  && previousInputBindingKey !== activeInputBindingKey;
8891
8407
  const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
8892
- const startingControlOwner = !!normalized.hubConnectionId
8893
- && (!device.controlOwnerConnectionId
8894
- || device.controlOwnerConnectionId !== normalized.hubConnectionId);
8895
8408
  if (ownerChanged || bindingChanged || writeBindingChanged) {
8896
8409
  const resetSent = inputWriteOwner.replaceBinding(
8897
8410
  activeInputBindingKey,
@@ -8909,25 +8422,6 @@ export function createRemoteHub(options = {}) {
8909
8422
  closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
8910
8423
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
8911
8424
  }
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
8425
  }
8932
8426
  const sent = inputWriteOwner.enqueue({
8933
8427
  type: 'input.control',
@@ -8942,7 +8436,6 @@ export function createRemoteHub(options = {}) {
8942
8436
  device.inputOwnerConnectionId = normalized.hubConnectionId;
8943
8437
  }
8944
8438
  device.inputOwnerBindingKey = activeInputBindingKey;
8945
- device.controlOwnerLastInputAtMs = Date.now();
8946
8439
  device.counters.commandsSent += 1;
8947
8440
  device.inputLastSeenAt = new Date().toISOString();
8948
8441
  return {
@@ -8972,25 +8465,6 @@ export function createRemoteHub(options = {}) {
8972
8465
  }
8973
8466
 
8974
8467
  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
8468
  const fallback = sendCommand(deviceId, {
8995
8469
  command: 'input.control',
8996
8470
  payload: {
@@ -9014,22 +8488,6 @@ export function createRemoteHub(options = {}) {
9014
8488
  timeoutTimer: null
9015
8489
  });
9016
8490
  }
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
8491
  return fallback.ok
9034
8492
  ? {
9035
8493
  ...fallback,
@@ -9056,17 +8514,12 @@ export function createRemoteHub(options = {}) {
9056
8514
  return { ok: false, error: 'input-owner-not-current' };
9057
8515
  }
9058
8516
 
8517
+ device.inputOwnerConnectionId = '';
8518
+ device.inputOwnerBindingKey = '';
9059
8519
  const inputSocket = device.inputSocket;
9060
8520
  const inputWriteOwner = getCurrentInputWriteOwner(device);
9061
8521
  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
- };
8522
+ return { ok: true, queued: false };
9070
8523
  }
9071
8524
  const controlStream = getActiveControlStream(device);
9072
8525
  const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
@@ -9082,19 +8535,10 @@ export function createRemoteHub(options = {}) {
9082
8535
  closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
9083
8536
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
9084
8537
  }
9085
- const ended = endControlSession(
9086
- device,
9087
- owner,
9088
- reason,
9089
- inputWriteOwner,
9090
- releaseBindingKey);
9091
- device.inputOwnerConnectionId = '';
9092
- device.inputOwnerBindingKey = '';
9093
8538
  return {
9094
- ok: ended.ok !== false,
8539
+ ok: true,
9095
8540
  queued: true,
9096
- backpressured: sent.backpressured === true || ended.backpressured === true,
9097
- controlSessionEnded: ended.ok === true
8541
+ backpressured: sent.backpressured === true
9098
8542
  };
9099
8543
  }
9100
8544
 
@@ -9137,9 +8581,6 @@ export function createRemoteHub(options = {}) {
9137
8581
  }
9138
8582
  const device = devices.get(String(deviceId || ''));
9139
8583
  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
8584
  const operation = normalizeAgentOperation(requestedOperation);
9144
8585
  if (requestedOperation && !operation) {
9145
8586
  return { ok: false, error: 'unsupported-agent-operation' };
@@ -11388,7 +10829,6 @@ export function createRemoteHub(options = {}) {
11388
10829
  disconnectDevice,
11389
10830
  assignDeviceSlot,
11390
10831
  sendCommand,
11391
- sendCommandAwaitResult,
11392
10832
  refreshDevicePolicies,
11393
10833
  sendLegacyClientUpdate,
11394
10834
  sendInputControl,
@@ -11417,16 +10857,7 @@ export function createRemoteHub(options = {}) {
11417
10857
  seedSyntheticFleet,
11418
10858
  clearSyntheticFleet,
11419
10859
  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()
10860
+ getPairingPin: () => pairingPin
11430
10861
  };
11431
10862
  }
11432
10863