@livedesk/hub 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/agents/agent-audit-store.js +16 -6
- package/src/agents/agent-permissions.js +9 -3
- package/src/agents/agent-tool-registry.js +19 -1
- package/src/captures/capture-store.js +50 -3
- package/src/filesystem/shared-folders.js +8 -0
- package/src/filesystem/transfer-jobs.js +36 -5
- package/src/live-desk-update.js +87 -19
- package/src/remote-hub.js +722 -173
- package/src/security/device-credential-authority.js +406 -0
- package/src/security/security-audit-store.js +260 -0
- package/src/server.js +1012 -277
- package/src/settings/settings-schema.js +19 -39
- package/src/transport/relay-hub-control.js +330 -3
- package/src/transport/secure-direct-acceptor.js +433 -0
- package/src/transport/udp-hub-transport.js +28 -5
- package/src/transport/udp-rendezvous.js +179 -13
package/src/remote-hub.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import {
|
|
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';
|
|
5
8
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
6
9
|
import {
|
|
7
10
|
BoundedSegmentedBuffer,
|
|
@@ -32,9 +35,10 @@ const MAX_AGENT_TCP_BINARY_PACKET_BYTES = TCP_BINARY_FRAME_HEADER_BYTES
|
|
|
32
35
|
const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
|
|
33
36
|
+ MAX_AGENT_BINARY_META_BYTES
|
|
34
37
|
+ MAX_AGENT_BINARY_PAYLOAD_BYTES;
|
|
35
|
-
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
36
|
-
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
37
|
-
+ (256 * 1024);
|
|
38
|
+
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
|
+
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
|
+
+ (256 * 1024);
|
|
41
|
+
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
38
42
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
39
43
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
40
44
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
@@ -106,7 +110,7 @@ const MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS = Object.freeze([
|
|
|
106
110
|
'handoffOldestFrameAgeMs'
|
|
107
111
|
]);
|
|
108
112
|
|
|
109
|
-
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
113
|
+
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
110
114
|
return Number(frame?.nativeCaptureTelemetryVersion) >= MAC_NATIVE_RESOURCE_TELEMETRY_VERSION
|
|
111
115
|
&& MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS.every(field =>
|
|
112
116
|
frame?.[field] !== null
|
|
@@ -824,14 +828,61 @@ function isPrivateIPv4(value) {
|
|
|
824
828
|
|| (parts[0] === 192 && parts[1] === 168);
|
|
825
829
|
}
|
|
826
830
|
|
|
827
|
-
function isPrivateNetworkAddress(value) {
|
|
831
|
+
function isPrivateNetworkAddress(value) {
|
|
828
832
|
const address = String(value || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
829
833
|
return isLoopbackHost(address)
|
|
830
834
|
|| isPrivateIPv4(address)
|
|
831
835
|
|| address.startsWith('fe80:')
|
|
832
836
|
|| address.startsWith('fc')
|
|
833
|
-
|| address.startsWith('fd');
|
|
834
|
-
}
|
|
837
|
+
|| address.startsWith('fd');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function createRemoteMessageRateGate({
|
|
841
|
+
limit = MAX_AGENT_MESSAGES_PER_SECOND,
|
|
842
|
+
windowMs = 1_000,
|
|
843
|
+
now = () => Date.now()
|
|
844
|
+
} = {}) {
|
|
845
|
+
const boundedLimit = Math.max(1, Math.min(10_000, Math.floor(Number(limit) || MAX_AGENT_MESSAGES_PER_SECOND)));
|
|
846
|
+
const boundedWindowMs = Math.max(100, Math.min(60_000, Math.floor(Number(windowMs) || 1_000)));
|
|
847
|
+
let windowStartedAt = Number(now());
|
|
848
|
+
let count = 0;
|
|
849
|
+
return Object.freeze({
|
|
850
|
+
consume() {
|
|
851
|
+
const current = Number(now());
|
|
852
|
+
if (!Number.isFinite(current) || current - windowStartedAt >= boundedWindowMs || current < windowStartedAt) {
|
|
853
|
+
windowStartedAt = current;
|
|
854
|
+
count = 0;
|
|
855
|
+
}
|
|
856
|
+
count += 1;
|
|
857
|
+
return count <= boundedLimit;
|
|
858
|
+
},
|
|
859
|
+
getStatus: () => ({ count, limit: boundedLimit, windowMs: boundedWindowMs, windowStartedAt })
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function evaluateRemoteConnectionPolicy({
|
|
864
|
+
remoteAddress = '',
|
|
865
|
+
relayConnection = false,
|
|
866
|
+
encrypted = false,
|
|
867
|
+
allowLegacyPlaintextForTests = false,
|
|
868
|
+
policy = {}
|
|
869
|
+
} = {}) {
|
|
870
|
+
const address = String(remoteAddress || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
871
|
+
if (policy.requireEncryptedConnections === true
|
|
872
|
+
&& encrypted !== true
|
|
873
|
+
&& allowLegacyPlaintextForTests !== true) {
|
|
874
|
+
return 'encrypted-connection-required';
|
|
875
|
+
}
|
|
876
|
+
if (relayConnection === true || !isPrivateNetworkAddress(address)) {
|
|
877
|
+
return policy.allowInternetConnections === false
|
|
878
|
+
? 'internet-connections-blocked-by-settings'
|
|
879
|
+
: '';
|
|
880
|
+
}
|
|
881
|
+
if (!isLoopbackHost(address) && policy.allowLanConnections === false) {
|
|
882
|
+
return 'lan-connections-blocked-by-settings';
|
|
883
|
+
}
|
|
884
|
+
return '';
|
|
885
|
+
}
|
|
835
886
|
|
|
836
887
|
function readCpuTimes() {
|
|
837
888
|
let idle = 0;
|
|
@@ -1155,34 +1206,37 @@ function getRetiredAgentTaskError(options = {}) {
|
|
|
1155
1206
|
: '';
|
|
1156
1207
|
}
|
|
1157
1208
|
|
|
1158
|
-
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
1159
|
-
'system.health',
|
|
1160
|
-
'gpu.status',
|
|
1161
|
-
'disk.status',
|
|
1162
|
-
'process.list',
|
|
1163
|
-
'service.status',
|
|
1164
|
-
'diagnostics.collect',
|
|
1165
|
-
'
|
|
1166
|
-
'
|
|
1167
|
-
'
|
|
1168
|
-
'
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
'
|
|
1173
|
-
'
|
|
1174
|
-
'
|
|
1175
|
-
'
|
|
1176
|
-
'
|
|
1177
|
-
'
|
|
1178
|
-
'
|
|
1179
|
-
'
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1209
|
+
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
1210
|
+
'system.health',
|
|
1211
|
+
'gpu.status',
|
|
1212
|
+
'disk.status',
|
|
1213
|
+
'process.list',
|
|
1214
|
+
'service.status',
|
|
1215
|
+
'diagnostics.collect',
|
|
1216
|
+
'file.read',
|
|
1217
|
+
'file.list',
|
|
1218
|
+
'network.status',
|
|
1219
|
+
'logs.collect'
|
|
1220
|
+
]);
|
|
1221
|
+
|
|
1222
|
+
const RETIRED_AGENT_MUTATING_OPERATIONS = new Set([
|
|
1223
|
+
'process.control',
|
|
1224
|
+
'service.control',
|
|
1225
|
+
'application.launch',
|
|
1226
|
+
'application.close',
|
|
1227
|
+
'file.write',
|
|
1228
|
+
'file.delete',
|
|
1229
|
+
'command.run',
|
|
1230
|
+
'script.run',
|
|
1231
|
+
'software.install',
|
|
1232
|
+
'system.power',
|
|
1233
|
+
'system.configure'
|
|
1234
|
+
]);
|
|
1235
|
+
|
|
1236
|
+
function normalizeAgentOperation(value) {
|
|
1237
|
+
const operation = safeString(value, 80).toLowerCase();
|
|
1238
|
+
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
1239
|
+
}
|
|
1186
1240
|
|
|
1187
1241
|
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
1188
1242
|
'monitorIndex',
|
|
@@ -1348,7 +1402,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
|
|
|
1348
1402
|
return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
|
|
1349
1403
|
}
|
|
1350
1404
|
|
|
1351
|
-
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1405
|
+
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1352
1406
|
if (!frame) {
|
|
1353
1407
|
return null;
|
|
1354
1408
|
}
|
|
@@ -1370,8 +1424,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
|
1370
1424
|
serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
|
|
1371
1425
|
}
|
|
1372
1426
|
|
|
1373
|
-
return serialized;
|
|
1374
|
-
}
|
|
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
|
+
}
|
|
1375
1448
|
|
|
1376
1449
|
function getRecentFrameCache(device, frameKind) {
|
|
1377
1450
|
if (!device) {
|
|
@@ -1562,7 +1635,7 @@ function serializeDevice(device, options = {}) {
|
|
|
1562
1635
|
},
|
|
1563
1636
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1564
1637
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1565
|
-
activeLiveStream: device.activeLiveStream
|
|
1638
|
+
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1566
1639
|
liveCapturePause: device.liveCapturePause
|
|
1567
1640
|
? {
|
|
1568
1641
|
token: device.liveCapturePause.token,
|
|
@@ -2169,7 +2242,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2169
2242
|
: null;
|
|
2170
2243
|
const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
|
|
2171
2244
|
|
|
2172
|
-
function getDevicePolicy(device) {
|
|
2245
|
+
function getDevicePolicy(device) {
|
|
2173
2246
|
try {
|
|
2174
2247
|
return getEffectiveDevicePolicy({
|
|
2175
2248
|
deviceId: device?.deviceId || '',
|
|
@@ -2177,8 +2250,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2177
2250
|
}) || {};
|
|
2178
2251
|
} catch {
|
|
2179
2252
|
return {};
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function connectionPolicyError(socket, policy = {}) {
|
|
2257
|
+
return evaluateRemoteConnectionPolicy({
|
|
2258
|
+
remoteAddress: socket?.remoteAddress || '',
|
|
2259
|
+
relayConnection: socket?.__liveDeskRelayControl === true,
|
|
2260
|
+
encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
|
|
2261
|
+
allowLegacyPlaintextForTests,
|
|
2262
|
+
policy
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2182
2265
|
|
|
2183
2266
|
function policyError(device, permission = '', command = '') {
|
|
2184
2267
|
const policy = getDevicePolicy(device);
|
|
@@ -2228,22 +2311,83 @@ export function createRemoteHub(options = {}) {
|
|
|
2228
2311
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
2229
2312
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
2230
2313
|
const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
|
|
2231
|
-
const
|
|
2232
|
-
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
2233
|
-
256);
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2314
|
+
const configuredEnrollmentToken = safeString(
|
|
2315
|
+
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
|
|
2316
|
+
256);
|
|
2317
|
+
const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
|
|
2318
|
+
|| isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
|
|
2319
|
+
|| env.LIVEDESK_TEST_MODE === '1';
|
|
2320
|
+
const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
|
|
2321
|
+
&& Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
|
|
2322
|
+
&& Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
|
|
2323
|
+
? configuredEnrollmentToken
|
|
2324
|
+
: '';
|
|
2325
|
+
let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
|
|
2326
|
+
? configuredEnrollmentToken
|
|
2327
|
+
: canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
|
|
2328
|
+
const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
|
|
2329
|
+
dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
|
|
2330
|
+
});
|
|
2331
|
+
const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
|
|
2332
|
+
? options.getSecurityIdentity
|
|
2333
|
+
: () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
|
|
2334
|
+
udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
|
|
2335
|
+
const identity = getSecurityIdentity() || {};
|
|
2336
|
+
return deviceCredentialAuthority.issueRendezvousProof({
|
|
2337
|
+
roomId,
|
|
2338
|
+
deviceId,
|
|
2339
|
+
role,
|
|
2340
|
+
ttlMs,
|
|
2341
|
+
accountId: safeString(identity.accountId, 128)
|
|
2342
|
+
});
|
|
2343
|
+
});
|
|
2344
|
+
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2244
2345
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2245
2346
|
: generatePairingPin();
|
|
2246
|
-
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2347
|
+
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2348
|
+
const consumeEnrollmentToken = expected => {
|
|
2349
|
+
if (!timingSafeStringEqual(expected, pairToken)) return false;
|
|
2350
|
+
pairToken = crypto.randomBytes(32).toString('base64url');
|
|
2351
|
+
pairingPin = generatePairingPin();
|
|
2352
|
+
pairingPinUpdatedAt = new Date().toISOString();
|
|
2353
|
+
emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
|
|
2354
|
+
pairingPinUpdatedAt,
|
|
2355
|
+
hubId: deviceCredentialAuthority.hubId
|
|
2356
|
+
});
|
|
2357
|
+
return true;
|
|
2358
|
+
};
|
|
2359
|
+
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2360
|
+
? options.relayControl
|
|
2361
|
+
: allowLegacyPlaintextForTests
|
|
2362
|
+
? createHubRelayControl({
|
|
2363
|
+
env,
|
|
2364
|
+
pairToken,
|
|
2365
|
+
logEvent,
|
|
2366
|
+
logWarn,
|
|
2367
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2368
|
+
})
|
|
2369
|
+
: createSecureHubRelayControl({
|
|
2370
|
+
env,
|
|
2371
|
+
authority: deviceCredentialAuthority,
|
|
2372
|
+
getIdentity: () => getSecurityIdentity() || {},
|
|
2373
|
+
getEnrollmentToken: () => pairToken,
|
|
2374
|
+
consumeEnrollmentToken,
|
|
2375
|
+
logEvent,
|
|
2376
|
+
logWarn,
|
|
2377
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2378
|
+
});
|
|
2379
|
+
const secureDirectAcceptor = createSecureDirectAcceptor({
|
|
2380
|
+
authority: deviceCredentialAuthority,
|
|
2381
|
+
getIdentity: () => getSecurityIdentity() || {},
|
|
2382
|
+
getEnrollmentToken: () => pairToken,
|
|
2383
|
+
consumeEnrollmentToken,
|
|
2384
|
+
onSecureSocket: socket => handleTcpAgentSocket(socket),
|
|
2385
|
+
onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
|
|
2386
|
+
...audit,
|
|
2387
|
+
remoteHub: getStatus({ includeSecrets: false })
|
|
2388
|
+
}),
|
|
2389
|
+
logWarn
|
|
2390
|
+
});
|
|
2247
2391
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2248
2392
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2249
2393
|
5000,
|
|
@@ -2268,7 +2412,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2268
2412
|
const transportDiagnostics = new Map();
|
|
2269
2413
|
const transportDiagnosticStartingDevices = new Set();
|
|
2270
2414
|
const duplicateDeviceLogAt = new Map();
|
|
2271
|
-
let server = null;
|
|
2415
|
+
let server = null;
|
|
2416
|
+
let controlIdleSweepTimer = null;
|
|
2272
2417
|
let started = false;
|
|
2273
2418
|
let boundPort = requestedPort;
|
|
2274
2419
|
let lastError = '';
|
|
@@ -2590,8 +2735,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2590
2735
|
host,
|
|
2591
2736
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2592
2737
|
port: boundPort || requestedPort,
|
|
2593
|
-
protocol: '
|
|
2594
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2738
|
+
protocol: 'secure-record-v1',
|
|
2739
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2595
2740
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2596
2741
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2597
2742
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2607,9 +2752,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2607
2752
|
agentEndpointCandidates: routeInfo.candidates,
|
|
2608
2753
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
2609
2754
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
2610
|
-
pairTokenPreview: maskToken(pairToken),
|
|
2611
|
-
pairingPin: includeSecrets ? pairingPin : '',
|
|
2612
|
-
pairingPinUpdatedAt,
|
|
2755
|
+
pairTokenPreview: maskToken(pairToken),
|
|
2756
|
+
pairingPin: includeSecrets ? pairingPin : '',
|
|
2757
|
+
pairingPinUpdatedAt,
|
|
2758
|
+
security: {
|
|
2759
|
+
direct: secureDirectAcceptor.getStatus(),
|
|
2760
|
+
hubId: deviceCredentialAuthority.hubId,
|
|
2761
|
+
hubPublicKeyFingerprint: crypto.createHash('sha256')
|
|
2762
|
+
.update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
|
|
2763
|
+
.digest('hex'),
|
|
2764
|
+
enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
|
|
2765
|
+
legacyPlaintextForTests: allowLegacyPlaintextForTests
|
|
2766
|
+
},
|
|
2613
2767
|
deviceCount: devices.size,
|
|
2614
2768
|
connectedDeviceCount: connectedDevices,
|
|
2615
2769
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -3869,14 +4023,22 @@ export function createRemoteHub(options = {}) {
|
|
|
3869
4023
|
}
|
|
3870
4024
|
}
|
|
3871
4025
|
|
|
3872
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4026
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
3873
4027
|
const socket = device?.inputSocket;
|
|
3874
4028
|
if (!socket) {
|
|
3875
4029
|
return;
|
|
3876
4030
|
}
|
|
3877
|
-
|
|
3878
|
-
const writeOwner = device.inputWriteOwner;
|
|
3879
|
-
device.
|
|
4031
|
+
|
|
4032
|
+
const writeOwner = device.inputWriteOwner;
|
|
4033
|
+
if (device.controlOwnerConnectionId) {
|
|
4034
|
+
endControlSession(
|
|
4035
|
+
device,
|
|
4036
|
+
device.controlOwnerConnectionId,
|
|
4037
|
+
reason,
|
|
4038
|
+
writeOwner,
|
|
4039
|
+
writeOwner?.getBindingKey?.() || '');
|
|
4040
|
+
}
|
|
4041
|
+
device.inputSocket = null;
|
|
3880
4042
|
device.inputWriteOwner = null;
|
|
3881
4043
|
device.inputSocketConnectionId = '';
|
|
3882
4044
|
device.inputOwnerConnectionId = '';
|
|
@@ -3906,9 +4068,17 @@ export function createRemoteHub(options = {}) {
|
|
|
3906
4068
|
if (!device || device.inputSocket !== socket) {
|
|
3907
4069
|
return;
|
|
3908
4070
|
}
|
|
3909
|
-
|
|
3910
|
-
const writeOwner = device.inputWriteOwner;
|
|
3911
|
-
device.
|
|
4071
|
+
|
|
4072
|
+
const writeOwner = device.inputWriteOwner;
|
|
4073
|
+
if (device.controlOwnerConnectionId) {
|
|
4074
|
+
endControlSession(
|
|
4075
|
+
device,
|
|
4076
|
+
device.controlOwnerConnectionId,
|
|
4077
|
+
reason,
|
|
4078
|
+
null,
|
|
4079
|
+
'');
|
|
4080
|
+
}
|
|
4081
|
+
device.inputSocket = null;
|
|
3912
4082
|
device.inputWriteOwner = null;
|
|
3913
4083
|
device.inputSocketConnectionId = '';
|
|
3914
4084
|
device.inputOwnerConnectionId = '';
|
|
@@ -4340,7 +4510,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4340
4510
|
inputWriteOwner: null,
|
|
4341
4511
|
inputSocketConnectionId: '',
|
|
4342
4512
|
inputOwnerConnectionId: '',
|
|
4343
|
-
inputOwnerBindingKey: '',
|
|
4513
|
+
inputOwnerBindingKey: '',
|
|
4514
|
+
controlOwnerSessionId: '',
|
|
4515
|
+
controlOwnerConnectionId: '',
|
|
4516
|
+
controlOwnerStartedAtMs: 0,
|
|
4517
|
+
controlOwnerLastInputAtMs: 0,
|
|
4344
4518
|
frameSocket: null,
|
|
4345
4519
|
audioSocket: null,
|
|
4346
4520
|
fileSocket: null,
|
|
@@ -7261,8 +7435,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7261
7435
|
return;
|
|
7262
7436
|
}
|
|
7263
7437
|
|
|
7264
|
-
if (!state.authenticated) {
|
|
7265
|
-
|
|
7438
|
+
if (!state.authenticated) {
|
|
7439
|
+
const securityContext = socket.__liveDeskSecurityContext;
|
|
7440
|
+
const secureDeviceAuthenticated = securityContext?.authenticated === true
|
|
7441
|
+
&& securityContext?.encrypted === true
|
|
7442
|
+
&& safeString(securityContext.accountId, 128)
|
|
7443
|
+
&& safeString(securityContext.hubId, 128)
|
|
7444
|
+
&& safeString(securityContext.deviceId, 128);
|
|
7445
|
+
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
7266
7446
|
message = {
|
|
7267
7447
|
...message,
|
|
7268
7448
|
capabilities: {
|
|
@@ -7272,13 +7452,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7272
7452
|
dedicatedInputChannel: false
|
|
7273
7453
|
}
|
|
7274
7454
|
};
|
|
7275
|
-
}
|
|
7276
|
-
if (message.type === 'slot.assign') {
|
|
7277
|
-
if (!
|
|
7278
|
-
|
|
7279
|
-
socket.
|
|
7280
|
-
|
|
7281
|
-
|
|
7455
|
+
}
|
|
7456
|
+
if (message.type === 'slot.assign') {
|
|
7457
|
+
if (!secureDeviceAuthenticated
|
|
7458
|
+
|| safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7459
|
+
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
|
|
7460
|
+
socket.end();
|
|
7461
|
+
return;
|
|
7462
|
+
}
|
|
7282
7463
|
const result = assignDeviceSlot(
|
|
7283
7464
|
message.deviceId || message.DeviceId,
|
|
7284
7465
|
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
@@ -7292,13 +7473,54 @@ export function createRemoteHub(options = {}) {
|
|
|
7292
7473
|
return;
|
|
7293
7474
|
}
|
|
7294
7475
|
|
|
7295
|
-
if (
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7476
|
+
if (secureDeviceAuthenticated) {
|
|
7477
|
+
if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7478
|
+
writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
|
|
7479
|
+
socket.destroy();
|
|
7480
|
+
return;
|
|
7481
|
+
}
|
|
7482
|
+
const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
|
|
7483
|
+
if (helloChannel !== securityContext.channel) {
|
|
7484
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
|
|
7485
|
+
socket.destroy();
|
|
7486
|
+
return;
|
|
7487
|
+
}
|
|
7488
|
+
} else {
|
|
7489
|
+
const legacyAllowed = socket.__liveDeskRelayControl === true
|
|
7490
|
+
|| socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
|
|
7491
|
+
if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7492
|
+
writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
|
|
7493
|
+
socket.destroy();
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
|
|
7498
|
+
const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
|
|
7499
|
+
const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
|
|
7500
|
+
? message.capabilities
|
|
7501
|
+
: {};
|
|
7502
|
+
const incomingPolicy = getWelcomeDevicePolicy({
|
|
7503
|
+
deviceId: incomingDeviceId,
|
|
7504
|
+
capabilities: incomingCapabilities
|
|
7505
|
+
}) || {};
|
|
7506
|
+
const connectionDenied = connectionPolicyError(socket, incomingPolicy);
|
|
7507
|
+
if (connectionDenied) {
|
|
7508
|
+
writeJsonLine(socket, { type: 'error', error: connectionDenied });
|
|
7509
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
7510
|
+
result: 'rejected',
|
|
7511
|
+
reason: connectionDenied,
|
|
7512
|
+
deviceId: incomingDeviceId,
|
|
7513
|
+
sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
|
|
7514
|
+
accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
|
|
7515
|
+
hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
|
|
7516
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7517
|
+
remoteAddress: safeString(socket.remoteAddress, 256)
|
|
7518
|
+
});
|
|
7519
|
+
socket.destroy();
|
|
7520
|
+
return;
|
|
7521
|
+
}
|
|
7522
|
+
|
|
7523
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
7302
7524
|
if (channel === 'input') {
|
|
7303
7525
|
const device = attachInputSocket(socket, message);
|
|
7304
7526
|
if (!device) {
|
|
@@ -7465,7 +7687,27 @@ export function createRemoteHub(options = {}) {
|
|
|
7465
7687
|
}
|
|
7466
7688
|
}
|
|
7467
7689
|
|
|
7468
|
-
function
|
|
7690
|
+
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7691
|
+
if (state.messageRateGate.consume()) return true;
|
|
7692
|
+
const device = state.device;
|
|
7693
|
+
emitEvent('RemoteAbuseDefense', {
|
|
7694
|
+
result: 'rejected',
|
|
7695
|
+
reason: 'authenticated-message-rate-exceeded',
|
|
7696
|
+
deviceId: device?.deviceId || '',
|
|
7697
|
+
sessionId: device?.sessionId || '',
|
|
7698
|
+
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7699
|
+
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7700
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7701
|
+
channel,
|
|
7702
|
+
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7703
|
+
rate: state.messageRateGate.getStatus()
|
|
7704
|
+
});
|
|
7705
|
+
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7706
|
+
socket.destroy();
|
|
7707
|
+
return false;
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
function handleWebSocketAgentSocket(socket) {
|
|
7469
7711
|
allSockets.add(socket);
|
|
7470
7712
|
socket.setNoDelay(true);
|
|
7471
7713
|
socket.setKeepAlive(true, heartbeatMs);
|
|
@@ -7479,10 +7721,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7479
7721
|
audioOnly: false,
|
|
7480
7722
|
fileOnly: false,
|
|
7481
7723
|
parserBuffer: socket.buffer,
|
|
7482
|
-
binaryIngress: null,
|
|
7483
|
-
binaryQueueDrops: 0,
|
|
7484
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7485
|
-
|
|
7724
|
+
binaryIngress: null,
|
|
7725
|
+
binaryQueueDrops: 0,
|
|
7726
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7727
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7728
|
+
closed: false
|
|
7486
7729
|
};
|
|
7487
7730
|
attachAgentBinaryIngress(socket, state);
|
|
7488
7731
|
|
|
@@ -7493,8 +7736,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7493
7736
|
}
|
|
7494
7737
|
}, 10000);
|
|
7495
7738
|
|
|
7496
|
-
socket.onTextMessage = text => {
|
|
7497
|
-
|
|
7739
|
+
socket.onTextMessage = text => {
|
|
7740
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7741
|
+
try {
|
|
7498
7742
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7499
7743
|
} catch (err) {
|
|
7500
7744
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
@@ -7502,8 +7746,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7502
7746
|
}
|
|
7503
7747
|
};
|
|
7504
7748
|
|
|
7505
|
-
socket.onBinaryMessage = payload => {
|
|
7506
|
-
|
|
7749
|
+
socket.onBinaryMessage = payload => {
|
|
7750
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7751
|
+
try {
|
|
7507
7752
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7508
7753
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
7509
7754
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
@@ -7658,10 +7903,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7658
7903
|
buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
|
|
7659
7904
|
lineScanOffset: 0,
|
|
7660
7905
|
pendingBinaryFrame: null,
|
|
7661
|
-
binaryIngress: null,
|
|
7662
|
-
binaryQueueDrops: 0,
|
|
7663
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7664
|
-
|
|
7906
|
+
binaryIngress: null,
|
|
7907
|
+
binaryQueueDrops: 0,
|
|
7908
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7909
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7910
|
+
closed: false
|
|
7665
7911
|
};
|
|
7666
7912
|
attachAgentBinaryIngress(socket, state);
|
|
7667
7913
|
|
|
@@ -7776,9 +8022,18 @@ export function createRemoteHub(options = {}) {
|
|
|
7776
8022
|
return;
|
|
7777
8023
|
}
|
|
7778
8024
|
|
|
7779
|
-
try {
|
|
7780
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
7781
|
-
if (
|
|
8025
|
+
try {
|
|
8026
|
+
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8027
|
+
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8028
|
+
? 'frame'
|
|
8029
|
+
: state.audioOnly
|
|
8030
|
+
? 'audio'
|
|
8031
|
+
: state.inputOnly
|
|
8032
|
+
? 'input'
|
|
8033
|
+
: state.fileOnly ? 'file' : 'control')) {
|
|
8034
|
+
return;
|
|
8035
|
+
}
|
|
8036
|
+
if (message?.type === 'frame.binary') {
|
|
7782
8037
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
7783
8038
|
const maxBytes = frameKind === 'thumbnail'
|
|
7784
8039
|
? MAX_THUMBNAIL_BINARY_BYTES
|
|
@@ -7832,16 +8087,35 @@ export function createRemoteHub(options = {}) {
|
|
|
7832
8087
|
}
|
|
7833
8088
|
}
|
|
7834
8089
|
|
|
7835
|
-
function handleSocket(socket) {
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
8090
|
+
function handleSocket(socket) {
|
|
8091
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8092
|
+
secureDirectAcceptor.accept(socket);
|
|
8093
|
+
return;
|
|
8094
|
+
}
|
|
8095
|
+
socket.once('data', chunk => {
|
|
8096
|
+
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8097
|
+
if (isSecureHandshakeStart(firstChunk)) {
|
|
8098
|
+
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8099
|
+
return;
|
|
8100
|
+
}
|
|
8101
|
+
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8102
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8103
|
+
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8104
|
+
socket.destroy();
|
|
8105
|
+
return;
|
|
8106
|
+
}
|
|
8107
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8108
|
+
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8109
|
+
return;
|
|
8110
|
+
}
|
|
8111
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8112
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8113
|
+
socket.destroy();
|
|
8114
|
+
return;
|
|
8115
|
+
}
|
|
8116
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8117
|
+
handleTcpAgentSocket(socket, firstChunk);
|
|
8118
|
+
});
|
|
7845
8119
|
|
|
7846
8120
|
socket.once('error', () => {
|
|
7847
8121
|
// The transport-specific handler owns logging after the first byte.
|
|
@@ -7863,10 +8137,10 @@ export function createRemoteHub(options = {}) {
|
|
|
7863
8137
|
started = true;
|
|
7864
8138
|
boundPort = candidateServer.address()?.port || port;
|
|
7865
8139
|
lastError = '';
|
|
7866
|
-
logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
7867
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
7868
|
-
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable
|
|
7869
|
-
}
|
|
8140
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8141
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
8142
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8143
|
+
}
|
|
7870
8144
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
7871
8145
|
resolve();
|
|
7872
8146
|
});
|
|
@@ -7878,10 +8152,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7878
8152
|
return getStatus({ includeSecrets: false });
|
|
7879
8153
|
}
|
|
7880
8154
|
|
|
7881
|
-
try {
|
|
7882
|
-
await udpTransport?.start?.();
|
|
7883
|
-
await listenOnPort(requestedPort);
|
|
7884
|
-
|
|
8155
|
+
try {
|
|
8156
|
+
await udpTransport?.start?.();
|
|
8157
|
+
await listenOnPort(requestedPort);
|
|
8158
|
+
if (!controlIdleSweepTimer) {
|
|
8159
|
+
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8160
|
+
controlIdleSweepTimer.unref?.();
|
|
8161
|
+
}
|
|
8162
|
+
try {
|
|
7885
8163
|
await relayControl?.start?.();
|
|
7886
8164
|
} catch {
|
|
7887
8165
|
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
@@ -7902,8 +8180,12 @@ export function createRemoteHub(options = {}) {
|
|
|
7902
8180
|
return getStatus({ includeSecrets: false });
|
|
7903
8181
|
}
|
|
7904
8182
|
|
|
7905
|
-
async function close() {
|
|
7906
|
-
|
|
8183
|
+
async function close() {
|
|
8184
|
+
if (controlIdleSweepTimer) {
|
|
8185
|
+
clearInterval(controlIdleSweepTimer);
|
|
8186
|
+
controlIdleSweepTimer = null;
|
|
8187
|
+
}
|
|
8188
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
7907
8189
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
7908
8190
|
}
|
|
7909
8191
|
|
|
@@ -8040,7 +8322,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8040
8322
|
};
|
|
8041
8323
|
}
|
|
8042
8324
|
|
|
8043
|
-
function sendCommand(deviceId, command) {
|
|
8325
|
+
function sendCommand(deviceId, command) {
|
|
8044
8326
|
const device = devices.get(String(deviceId || ''));
|
|
8045
8327
|
const commandName = safeString(command?.command || 'ping', 80);
|
|
8046
8328
|
const requiredPermission = commandName === 'input.control'
|
|
@@ -8106,10 +8388,64 @@ export function createRemoteHub(options = {}) {
|
|
|
8106
8388
|
command: payload.command,
|
|
8107
8389
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8108
8390
|
});
|
|
8109
|
-
return { ok: true, commandId };
|
|
8110
|
-
}
|
|
8111
|
-
|
|
8112
|
-
function
|
|
8391
|
+
return { ok: true, commandId };
|
|
8392
|
+
}
|
|
8393
|
+
|
|
8394
|
+
async function sendCommandAwaitResult(deviceId, command, options = {}) {
|
|
8395
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
8396
|
+
const device = devices.get(normalizedDeviceId);
|
|
8397
|
+
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8398
|
+
const commandWithId = { ...(command || {}), commandId };
|
|
8399
|
+
|
|
8400
|
+
if (device?.synthetic === true && device.connected) {
|
|
8401
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8402
|
+
return {
|
|
8403
|
+
...sent,
|
|
8404
|
+
queued: sent.ok === true,
|
|
8405
|
+
acknowledged: sent.ok === true,
|
|
8406
|
+
acknowledgement: sent.ok === true
|
|
8407
|
+
? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
|
|
8408
|
+
: { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
|
|
8412
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8413
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8414
|
+
return {
|
|
8415
|
+
...sent,
|
|
8416
|
+
queued: false,
|
|
8417
|
+
acknowledged: false,
|
|
8418
|
+
acknowledgement: {
|
|
8419
|
+
ok: false,
|
|
8420
|
+
commandId,
|
|
8421
|
+
result: null,
|
|
8422
|
+
error: sent.error || 'device-not-connected'
|
|
8423
|
+
}
|
|
8424
|
+
};
|
|
8425
|
+
}
|
|
8426
|
+
|
|
8427
|
+
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
|
|
8428
|
+
// Register before writing: a loopback Agent can return command.result in
|
|
8429
|
+
// the same event-loop turn as the command write.
|
|
8430
|
+
const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
|
|
8431
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8432
|
+
if (!sent.ok) {
|
|
8433
|
+
failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
|
|
8434
|
+
}
|
|
8435
|
+
const acknowledgement = await acknowledgementPromise;
|
|
8436
|
+
return {
|
|
8437
|
+
...sent,
|
|
8438
|
+
ok: sent.ok === true && acknowledgement.ok === true,
|
|
8439
|
+
queued: sent.ok === true,
|
|
8440
|
+
acknowledged: acknowledgement.ok === true,
|
|
8441
|
+
acknowledgement,
|
|
8442
|
+
error: acknowledgement.ok === true
|
|
8443
|
+
? undefined
|
|
8444
|
+
: acknowledgement.error || sent.error || 'command-result-failed'
|
|
8445
|
+
};
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8113
8449
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8114
8450
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8115
8451
|
: null;
|
|
@@ -8118,9 +8454,31 @@ export function createRemoteHub(options = {}) {
|
|
|
8118
8454
|
for (const device of devices.values()) {
|
|
8119
8455
|
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8120
8456
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8121
|
-
total += 1;
|
|
8122
|
-
const effectivePolicy = getDevicePolicy(device);
|
|
8123
|
-
|
|
8457
|
+
total += 1;
|
|
8458
|
+
const effectivePolicy = getDevicePolicy(device);
|
|
8459
|
+
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8460
|
+
if (connectionDenied) {
|
|
8461
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
8462
|
+
result: 'rejected',
|
|
8463
|
+
reason: connectionDenied,
|
|
8464
|
+
deviceId: device.deviceId,
|
|
8465
|
+
sessionId: device.sessionId,
|
|
8466
|
+
transport: device.controlTransport,
|
|
8467
|
+
remoteAddress: device.remoteAddress
|
|
8468
|
+
});
|
|
8469
|
+
disconnectDevice(device.deviceId, connectionDenied);
|
|
8470
|
+
continue;
|
|
8471
|
+
}
|
|
8472
|
+
if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
|
|
8473
|
+
releaseInputOwner(
|
|
8474
|
+
device.deviceId,
|
|
8475
|
+
device.controlOwnerConnectionId,
|
|
8476
|
+
'control-disabled-by-policy');
|
|
8477
|
+
}
|
|
8478
|
+
if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
|
|
8479
|
+
stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
|
|
8480
|
+
}
|
|
8481
|
+
if (writeJsonLine(device.socket, {
|
|
8124
8482
|
type: 'policy.update',
|
|
8125
8483
|
effectivePolicy,
|
|
8126
8484
|
updatedAt: new Date().toISOString()
|
|
@@ -8199,7 +8557,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8199
8557
|
]);
|
|
8200
8558
|
}
|
|
8201
8559
|
|
|
8202
|
-
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8560
|
+
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8203
8561
|
return {
|
|
8204
8562
|
type: 'input.control',
|
|
8205
8563
|
payload: {
|
|
@@ -8221,8 +8579,114 @@ export function createRemoteHub(options = {}) {
|
|
|
8221
8579
|
hubForwardedAtEpochMs: Date.now(),
|
|
8222
8580
|
issuedAt: new Date().toISOString()
|
|
8223
8581
|
}
|
|
8224
|
-
};
|
|
8225
|
-
}
|
|
8582
|
+
};
|
|
8583
|
+
}
|
|
8584
|
+
|
|
8585
|
+
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8586
|
+
const policy = getDevicePolicy(device);
|
|
8587
|
+
return {
|
|
8588
|
+
type,
|
|
8589
|
+
payload: {
|
|
8590
|
+
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8591
|
+
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8592
|
+
deviceId: safeString(device?.deviceId, 160),
|
|
8593
|
+
visibleIndicator: true,
|
|
8594
|
+
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8595
|
+
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8596
|
+
reason: safeString(reason, 160),
|
|
8597
|
+
issuedAt: new Date().toISOString()
|
|
8598
|
+
}
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8603
|
+
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8604
|
+
if (inputWriteOwner && bindingKey) {
|
|
8605
|
+
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8606
|
+
}
|
|
8607
|
+
if (!device?.socket || device.socket.destroyed) {
|
|
8608
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
+
}
|
|
8610
|
+
const sent = writeJsonLine(device.socket, {
|
|
8611
|
+
type: 'command',
|
|
8612
|
+
commandId: crypto.randomUUID(),
|
|
8613
|
+
command: type,
|
|
8614
|
+
payload: message.payload,
|
|
8615
|
+
issuedAt: new Date().toISOString()
|
|
8616
|
+
});
|
|
8617
|
+
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8618
|
+
}
|
|
8619
|
+
|
|
8620
|
+
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8621
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8622
|
+
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8623
|
+
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8624
|
+
return { ok: true, alreadyActive: true };
|
|
8625
|
+
}
|
|
8626
|
+
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8627
|
+
device.controlOwnerConnectionId = owner;
|
|
8628
|
+
device.controlOwnerStartedAtMs = Date.now();
|
|
8629
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8630
|
+
const sent = sendControlSessionMessage(
|
|
8631
|
+
device,
|
|
8632
|
+
owner,
|
|
8633
|
+
'control.session.start',
|
|
8634
|
+
'browser-control-owner-active',
|
|
8635
|
+
inputWriteOwner,
|
|
8636
|
+
bindingKey);
|
|
8637
|
+
if (!sent.ok) {
|
|
8638
|
+
device.controlOwnerSessionId = '';
|
|
8639
|
+
device.controlOwnerConnectionId = '';
|
|
8640
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8641
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8642
|
+
return sent;
|
|
8643
|
+
}
|
|
8644
|
+
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8645
|
+
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8646
|
+
hubConnectionId: owner
|
|
8647
|
+
});
|
|
8648
|
+
return sent;
|
|
8649
|
+
}
|
|
8650
|
+
|
|
8651
|
+
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8652
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8653
|
+
if (!device?.controlOwnerSessionId
|
|
8654
|
+
|| !owner
|
|
8655
|
+
|| device.controlOwnerConnectionId !== owner) {
|
|
8656
|
+
return { ok: false, error: 'control-owner-not-current' };
|
|
8657
|
+
}
|
|
8658
|
+
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8659
|
+
const sent = sendControlSessionMessage(
|
|
8660
|
+
device,
|
|
8661
|
+
owner,
|
|
8662
|
+
'control.session.stop',
|
|
8663
|
+
reason,
|
|
8664
|
+
inputWriteOwner,
|
|
8665
|
+
bindingKey);
|
|
8666
|
+
device.controlOwnerSessionId = '';
|
|
8667
|
+
device.controlOwnerConnectionId = '';
|
|
8668
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8669
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8670
|
+
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8671
|
+
controlOwnerSessionId,
|
|
8672
|
+
hubConnectionId: owner,
|
|
8673
|
+
reason,
|
|
8674
|
+
delivered: sent.ok === true
|
|
8675
|
+
});
|
|
8676
|
+
return sent;
|
|
8677
|
+
}
|
|
8678
|
+
|
|
8679
|
+
function sweepIdleControlSessions() {
|
|
8680
|
+
const nowMs = Date.now();
|
|
8681
|
+
for (const device of devices.values()) {
|
|
8682
|
+
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8683
|
+
const policy = getDevicePolicy(device);
|
|
8684
|
+
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8685
|
+
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8686
|
+
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8687
|
+
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8226
8690
|
|
|
8227
8691
|
function getCurrentInputWriteOwner(device) {
|
|
8228
8692
|
if (!device?.inputSocket
|
|
@@ -8365,13 +8829,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8365
8829
|
normalized,
|
|
8366
8830
|
activeMonitorIndex);
|
|
8367
8831
|
const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
|
|
8368
|
-
const ownerChanged = normalized.hubConnectionId
|
|
8832
|
+
const ownerChanged = normalized.hubConnectionId
|
|
8369
8833
|
&& previousOwnerConnectionId
|
|
8370
8834
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8371
8835
|
const bindingChanged = previousInputBindingKey
|
|
8372
8836
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8373
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8374
|
-
|
|
8837
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8838
|
+
const startingControlOwner = !!normalized.hubConnectionId
|
|
8839
|
+
&& (!device.controlOwnerConnectionId
|
|
8840
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8841
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8375
8842
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8376
8843
|
activeInputBindingKey,
|
|
8377
8844
|
buildRemoteInputResetMessage(
|
|
@@ -8386,9 +8853,28 @@ export function createRemoteHub(options = {}) {
|
|
|
8386
8853
|
activeMonitorIndex));
|
|
8387
8854
|
if (!resetSent.ok) {
|
|
8388
8855
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
8389
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8390
|
-
}
|
|
8391
|
-
|
|
8856
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8857
|
+
}
|
|
8858
|
+
if (startingControlOwner) {
|
|
8859
|
+
if (device.controlOwnerConnectionId) {
|
|
8860
|
+
endControlSession(
|
|
8861
|
+
device,
|
|
8862
|
+
device.controlOwnerConnectionId,
|
|
8863
|
+
'browser-input-owner-changed',
|
|
8864
|
+
inputWriteOwner,
|
|
8865
|
+
activeInputBindingKey);
|
|
8866
|
+
}
|
|
8867
|
+
const started = beginControlSession(
|
|
8868
|
+
device,
|
|
8869
|
+
normalized.hubConnectionId,
|
|
8870
|
+
inputWriteOwner,
|
|
8871
|
+
activeInputBindingKey);
|
|
8872
|
+
if (!started.ok) {
|
|
8873
|
+
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8874
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8392
8878
|
const sent = inputWriteOwner.enqueue({
|
|
8393
8879
|
type: 'input.control',
|
|
8394
8880
|
payload: {
|
|
@@ -8401,7 +8887,8 @@ export function createRemoteHub(options = {}) {
|
|
|
8401
8887
|
if (normalized.hubConnectionId) {
|
|
8402
8888
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8403
8889
|
}
|
|
8404
|
-
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8890
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8891
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8405
8892
|
device.counters.commandsSent += 1;
|
|
8406
8893
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8407
8894
|
return {
|
|
@@ -8430,8 +8917,27 @@ export function createRemoteHub(options = {}) {
|
|
|
8430
8917
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8431
8918
|
}
|
|
8432
8919
|
|
|
8433
|
-
const hubForwardedAtEpochMs = Date.now();
|
|
8434
|
-
const
|
|
8920
|
+
const hubForwardedAtEpochMs = Date.now();
|
|
8921
|
+
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8922
|
+
&& (!device.controlOwnerConnectionId
|
|
8923
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8924
|
+
if (startingFallbackOwner) {
|
|
8925
|
+
if (device.controlOwnerConnectionId) {
|
|
8926
|
+
endControlSession(
|
|
8927
|
+
device,
|
|
8928
|
+
device.controlOwnerConnectionId,
|
|
8929
|
+
'browser-input-owner-changed',
|
|
8930
|
+
null,
|
|
8931
|
+
'');
|
|
8932
|
+
}
|
|
8933
|
+
const started = beginControlSession(
|
|
8934
|
+
device,
|
|
8935
|
+
normalized.hubConnectionId,
|
|
8936
|
+
null,
|
|
8937
|
+
'');
|
|
8938
|
+
if (!started.ok) return started;
|
|
8939
|
+
}
|
|
8940
|
+
const fallback = sendCommand(deviceId, {
|
|
8435
8941
|
command: 'input.control',
|
|
8436
8942
|
payload: {
|
|
8437
8943
|
...normalized,
|
|
@@ -8439,7 +8945,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8439
8945
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8440
8946
|
}
|
|
8441
8947
|
});
|
|
8442
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8948
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8443
8949
|
schedulePendingInputFallback(device, {
|
|
8444
8950
|
commandId: fallback.commandId,
|
|
8445
8951
|
inputSeq: normalized.inputSeq,
|
|
@@ -8451,10 +8957,26 @@ export function createRemoteHub(options = {}) {
|
|
|
8451
8957
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8452
8958
|
hubForwardedAtEpochMs,
|
|
8453
8959
|
fallback: true,
|
|
8454
|
-
timeoutTimer: null
|
|
8455
|
-
});
|
|
8456
|
-
}
|
|
8457
|
-
|
|
8960
|
+
timeoutTimer: null
|
|
8961
|
+
});
|
|
8962
|
+
}
|
|
8963
|
+
if (fallback.ok) {
|
|
8964
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8965
|
+
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
8966
|
+
device,
|
|
8967
|
+
controlStream,
|
|
8968
|
+
normalized,
|
|
8969
|
+
activeMonitorIndex);
|
|
8970
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8971
|
+
} else if (startingFallbackOwner) {
|
|
8972
|
+
endControlSession(
|
|
8973
|
+
device,
|
|
8974
|
+
normalized.hubConnectionId,
|
|
8975
|
+
'input-control-delivery-failed',
|
|
8976
|
+
null,
|
|
8977
|
+
'');
|
|
8978
|
+
}
|
|
8979
|
+
return fallback.ok
|
|
8458
8980
|
? {
|
|
8459
8981
|
...fallback,
|
|
8460
8982
|
inputSocket: false,
|
|
@@ -8480,15 +9002,20 @@ export function createRemoteHub(options = {}) {
|
|
|
8480
9002
|
return { ok: false, error: 'input-owner-not-current' };
|
|
8481
9003
|
}
|
|
8482
9004
|
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
9005
|
+
const inputSocket = device.inputSocket;
|
|
9006
|
+
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9007
|
+
if (!inputSocket || !inputWriteOwner) {
|
|
9008
|
+
const ended = endControlSession(device, owner, reason, null, '');
|
|
9009
|
+
device.inputOwnerConnectionId = '';
|
|
9010
|
+
device.inputOwnerBindingKey = '';
|
|
9011
|
+
return {
|
|
9012
|
+
ok: ended.ok !== false,
|
|
9013
|
+
queued: false,
|
|
9014
|
+
controlSessionEnded: ended.ok === true
|
|
9015
|
+
};
|
|
9016
|
+
}
|
|
9017
|
+
const controlStream = getActiveControlStream(device);
|
|
9018
|
+
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
8492
9019
|
const sent = inputWriteOwner.replaceBinding(
|
|
8493
9020
|
releaseBindingKey,
|
|
8494
9021
|
buildRemoteInputResetMessage(
|
|
@@ -8497,15 +9024,24 @@ export function createRemoteHub(options = {}) {
|
|
|
8497
9024
|
owner,
|
|
8498
9025
|
reason,
|
|
8499
9026
|
normalizeMonitorIndex(controlStream?.monitorIndex)));
|
|
8500
|
-
if (!sent.ok) {
|
|
9027
|
+
if (!sent.ok) {
|
|
8501
9028
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
8502
9029
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8503
|
-
}
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
9030
|
+
}
|
|
9031
|
+
const ended = endControlSession(
|
|
9032
|
+
device,
|
|
9033
|
+
owner,
|
|
9034
|
+
reason,
|
|
9035
|
+
inputWriteOwner,
|
|
9036
|
+
releaseBindingKey);
|
|
9037
|
+
device.inputOwnerConnectionId = '';
|
|
9038
|
+
device.inputOwnerBindingKey = '';
|
|
9039
|
+
return {
|
|
9040
|
+
ok: ended.ok !== false,
|
|
9041
|
+
queued: true,
|
|
9042
|
+
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
9043
|
+
controlSessionEnded: ended.ok === true
|
|
9044
|
+
};
|
|
8509
9045
|
}
|
|
8510
9046
|
|
|
8511
9047
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -8545,9 +9081,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8545
9081
|
if (retiredRequestError) {
|
|
8546
9082
|
return { ok: false, error: retiredRequestError };
|
|
8547
9083
|
}
|
|
8548
|
-
const device = devices.get(String(deviceId || ''));
|
|
8549
|
-
const requestedOperation = safeString(options.operation, 80);
|
|
8550
|
-
|
|
9084
|
+
const device = devices.get(String(deviceId || ''));
|
|
9085
|
+
const requestedOperation = safeString(options.operation, 80);
|
|
9086
|
+
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9087
|
+
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9088
|
+
}
|
|
9089
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
8551
9090
|
if (requestedOperation && !operation) {
|
|
8552
9091
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
8553
9092
|
}
|
|
@@ -10793,9 +11332,10 @@ export function createRemoteHub(options = {}) {
|
|
|
10793
11332
|
retryTaskBatch,
|
|
10794
11333
|
listDeviceFrames,
|
|
10795
11334
|
disconnectDevice,
|
|
10796
|
-
assignDeviceSlot,
|
|
10797
|
-
sendCommand,
|
|
10798
|
-
|
|
11335
|
+
assignDeviceSlot,
|
|
11336
|
+
sendCommand,
|
|
11337
|
+
sendCommandAwaitResult,
|
|
11338
|
+
refreshDevicePolicies,
|
|
10799
11339
|
sendLegacyClientUpdate,
|
|
10800
11340
|
sendInputControl,
|
|
10801
11341
|
releaseInputOwner,
|
|
@@ -10822,8 +11362,17 @@ export function createRemoteHub(options = {}) {
|
|
|
10822
11362
|
handleUdpFrame,
|
|
10823
11363
|
seedSyntheticFleet,
|
|
10824
11364
|
clearSyntheticFleet,
|
|
10825
|
-
getPairToken: () => pairToken,
|
|
10826
|
-
getPairingPin: () => pairingPin
|
|
11365
|
+
getPairToken: () => pairToken,
|
|
11366
|
+
getPairingPin: () => pairingPin,
|
|
11367
|
+
getSecurityStatus: () => ({
|
|
11368
|
+
hubId: deviceCredentialAuthority.hubId,
|
|
11369
|
+
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11370
|
+
hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
|
|
11371
|
+
direct: secureDirectAcceptor.getStatus(),
|
|
11372
|
+
devices: deviceCredentialAuthority.listDevices()
|
|
11373
|
+
}),
|
|
11374
|
+
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11375
|
+
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
10827
11376
|
};
|
|
10828
11377
|
}
|
|
10829
11378
|
|