@livedesk/hub 0.1.49 → 0.1.51
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/README.md +1 -1
- package/package.json +4 -4
- package/src/agents/agent-manager.js +2 -2
- package/src/agents/agent-permissions.js +2 -2
- package/src/agents/agent-tool-registry.js +36 -36
- package/src/agents/codex-agent-runtime.js +16 -16
- package/src/agents/codex-mcp-server.js +5 -5
- package/src/filesystem/roots.js +20 -20
- package/src/filesystem/shared-folders.js +2 -2
- package/src/live-desk-update.js +9 -9
- package/src/remote-clipboard-contract.mjs +482 -482
- package/src/remote-hub.js +1063 -1035
- package/src/server.js +242 -196
- package/src/settings/effective-device-policy.js +31 -31
- package/src/settings/settings-schema.js +2 -2
package/src/remote-hub.js
CHANGED
|
@@ -3,18 +3,18 @@ import os from 'os';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
5
5
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
6
|
-
import {
|
|
7
|
-
BoundedSegmentedBuffer,
|
|
8
|
-
createBoundedAgentBinaryIngressLane
|
|
9
|
-
} from './transport/agent-binary-ingress.js';
|
|
10
|
-
import {
|
|
11
|
-
REMOTE_CLIPBOARD_FILE_ACTIONS,
|
|
12
|
-
REMOTE_CLIPBOARD_LIMITS,
|
|
13
|
-
REMOTE_CLIPBOARD_PROTOCOL,
|
|
14
|
-
normalizeRemoteClipboardCommandResult,
|
|
15
|
-
normalizeRemoteClipboardManifest,
|
|
16
|
-
remoteClipboardContentNeedsFileTransfer
|
|
17
|
-
} from './remote-clipboard-contract.mjs';
|
|
6
|
+
import {
|
|
7
|
+
BoundedSegmentedBuffer,
|
|
8
|
+
createBoundedAgentBinaryIngressLane
|
|
9
|
+
} from './transport/agent-binary-ingress.js';
|
|
10
|
+
import {
|
|
11
|
+
REMOTE_CLIPBOARD_FILE_ACTIONS,
|
|
12
|
+
REMOTE_CLIPBOARD_LIMITS,
|
|
13
|
+
REMOTE_CLIPBOARD_PROTOCOL,
|
|
14
|
+
normalizeRemoteClipboardCommandResult,
|
|
15
|
+
normalizeRemoteClipboardManifest,
|
|
16
|
+
remoteClipboardContentNeedsFileTransfer
|
|
17
|
+
} from './remote-clipboard-contract.mjs';
|
|
18
18
|
|
|
19
19
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
20
20
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
@@ -236,13 +236,13 @@ const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
|
236
236
|
]);
|
|
237
237
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
238
238
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
239
|
-
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
240
|
-
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
241
|
-
const DEFAULT_DEVICE_CONNECTION_STALE_MS = 20000;
|
|
242
|
-
const DEFAULT_DEVICE_CONNECTION_SWEEP_MS = 5000;
|
|
243
|
-
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
244
|
-
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
245
|
-
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 16;
|
|
239
|
+
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
240
|
+
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
241
|
+
const DEFAULT_DEVICE_CONNECTION_STALE_MS = 20000;
|
|
242
|
+
const DEFAULT_DEVICE_CONNECTION_SWEEP_MS = 5000;
|
|
243
|
+
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
244
|
+
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
245
|
+
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 16;
|
|
246
246
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_BYTES = 12 * 1024 * 1024;
|
|
247
247
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_AGE_MS = 120;
|
|
248
248
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
@@ -1089,7 +1089,7 @@ async function fetchTextWithTimeout(url, timeoutMs) {
|
|
|
1089
1089
|
signal: controller.signal,
|
|
1090
1090
|
headers: {
|
|
1091
1091
|
accept: 'application/json,text/plain;q=0.9,*/*;q=0.1',
|
|
1092
|
-
'user-agent': '
|
|
1092
|
+
'user-agent': 'VuvoDesk-Hub/0.1'
|
|
1093
1093
|
}
|
|
1094
1094
|
});
|
|
1095
1095
|
if (!response.ok) {
|
|
@@ -1244,11 +1244,11 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1244
1244
|
'application.close',
|
|
1245
1245
|
'file.read',
|
|
1246
1246
|
'file.write',
|
|
1247
|
-
'file.delete',
|
|
1248
|
-
'file.list',
|
|
1249
|
-
'file.search',
|
|
1250
|
-
'directory.create',
|
|
1251
|
-
'command.run',
|
|
1247
|
+
'file.delete',
|
|
1248
|
+
'file.list',
|
|
1249
|
+
'file.search',
|
|
1250
|
+
'directory.create',
|
|
1251
|
+
'command.run',
|
|
1252
1252
|
'script.run',
|
|
1253
1253
|
'software.install',
|
|
1254
1254
|
'network.status',
|
|
@@ -1280,7 +1280,7 @@ function readRawRemoteInputMonitorIndex(input) {
|
|
|
1280
1280
|
return undefined;
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
|
-
function normalizeRemoteInputEvent(value = {}) {
|
|
1283
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
1284
1284
|
const input = value && typeof value === 'object' ? value : {};
|
|
1285
1285
|
const type = safeString(input.type || input.Type, 48);
|
|
1286
1286
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
@@ -1301,14 +1301,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1301
1301
|
.filter(Boolean))]
|
|
1302
1302
|
.slice(0, 64)
|
|
1303
1303
|
: null;
|
|
1304
|
-
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1305
|
-
const clipboardOperationId = safeString(
|
|
1306
|
-
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1307
|
-
128);
|
|
1308
|
-
const clipboardDirection = safeString(
|
|
1309
|
-
input.clipboardDirection || input.ClipboardDirection,
|
|
1310
|
-
16).toLowerCase();
|
|
1311
|
-
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1304
|
+
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1305
|
+
const clipboardOperationId = safeString(
|
|
1306
|
+
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1307
|
+
128);
|
|
1308
|
+
const clipboardDirection = safeString(
|
|
1309
|
+
input.clipboardDirection || input.ClipboardDirection,
|
|
1310
|
+
16).toLowerCase();
|
|
1311
|
+
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1312
1312
|
let code = safeString(input.code ?? input.Code, 80);
|
|
1313
1313
|
if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
|
|
1314
1314
|
code = 'Space';
|
|
@@ -1333,13 +1333,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1333
1333
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
1334
1334
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
1335
1335
|
altKey: input.altKey === true || input.AltKey === true,
|
|
1336
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1337
|
-
clipboardOperationId,
|
|
1338
|
-
operationId: clipboardOperationId,
|
|
1339
|
-
clipboardDirection,
|
|
1340
|
-
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1341
|
-
? clipboardManifest
|
|
1342
|
-
: null,
|
|
1336
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1337
|
+
clipboardOperationId,
|
|
1338
|
+
operationId: clipboardOperationId,
|
|
1339
|
+
clipboardDirection,
|
|
1340
|
+
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1341
|
+
? clipboardManifest
|
|
1342
|
+
: null,
|
|
1343
1343
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
1344
1344
|
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
1345
1345
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
@@ -2331,29 +2331,29 @@ export function createRemoteHub(options = {}) {
|
|
|
2331
2331
|
|
|
2332
2332
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
2333
2333
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
2334
|
-
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2335
|
-
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2336
|
-
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2337
|
-
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2338
|
-
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2339
|
-
heartbeatMs * 4);
|
|
2340
|
-
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2341
|
-
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2342
|
-
: clampNumber(
|
|
2343
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2344
|
-
defaultDeviceConnectionStaleMs,
|
|
2345
|
-
10 * 60 * 1000,
|
|
2346
|
-
defaultDeviceConnectionStaleMs);
|
|
2347
|
-
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2348
|
-
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2349
|
-
: clampNumber(
|
|
2350
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2351
|
-
1000,
|
|
2352
|
-
60000,
|
|
2353
|
-
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2354
|
-
const taskTimeoutMs = clampNumber(
|
|
2355
|
-
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2356
|
-
50,
|
|
2334
|
+
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2335
|
+
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2336
|
+
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2337
|
+
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2338
|
+
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2339
|
+
heartbeatMs * 4);
|
|
2340
|
+
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2341
|
+
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2342
|
+
: clampNumber(
|
|
2343
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2344
|
+
defaultDeviceConnectionStaleMs,
|
|
2345
|
+
10 * 60 * 1000,
|
|
2346
|
+
defaultDeviceConnectionStaleMs);
|
|
2347
|
+
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2348
|
+
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2349
|
+
: clampNumber(
|
|
2350
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2351
|
+
1000,
|
|
2352
|
+
60000,
|
|
2353
|
+
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2354
|
+
const taskTimeoutMs = clampNumber(
|
|
2355
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2356
|
+
50,
|
|
2357
2357
|
30 * 60 * 1000,
|
|
2358
2358
|
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
2359
2359
|
const inputAckTimeoutMs = clampNumber(
|
|
@@ -2416,18 +2416,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2416
2416
|
const audioSockets = new Map();
|
|
2417
2417
|
const fileSockets = new Map();
|
|
2418
2418
|
const allSockets = new Set();
|
|
2419
|
-
const agentBinaryIngressStates = new Set();
|
|
2420
|
-
const pendingCommandResultWaiters = new Map();
|
|
2421
|
-
const clipboardOperations = new Map();
|
|
2419
|
+
const agentBinaryIngressStates = new Set();
|
|
2420
|
+
const pendingCommandResultWaiters = new Map();
|
|
2421
|
+
const clipboardOperations = new Map();
|
|
2422
2422
|
const transportDiagnostics = new Map();
|
|
2423
2423
|
const transportDiagnosticStartingDevices = new Set();
|
|
2424
|
-
const duplicateDeviceLogAt = new Map();
|
|
2425
|
-
let server = null;
|
|
2426
|
-
let started = false;
|
|
2427
|
-
let deviceConnectionSweepTimer = null;
|
|
2428
|
-
let boundPort = requestedPort;
|
|
2429
|
-
let lastError = '';
|
|
2430
|
-
let hostTarget = null;
|
|
2424
|
+
const duplicateDeviceLogAt = new Map();
|
|
2425
|
+
let server = null;
|
|
2426
|
+
let started = false;
|
|
2427
|
+
let deviceConnectionSweepTimer = null;
|
|
2428
|
+
let boundPort = requestedPort;
|
|
2429
|
+
let lastError = '';
|
|
2430
|
+
let hostTarget = null;
|
|
2431
2431
|
let publicIPv4Cache = {
|
|
2432
2432
|
address: safeString(env.LIVEDESK_REMOTE_PUBLIC_IPV4 || env.LIVEDESK_PUBLIC_IPV4 || env.REMOTE_HUB_PUBLIC_IPV4, 64),
|
|
2433
2433
|
updatedAt: 0,
|
|
@@ -2748,14 +2748,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2748
2748
|
protocol: 'tcp-jsonl',
|
|
2749
2749
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2750
2750
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2751
|
-
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2752
|
-
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2753
|
-
heartbeatMs,
|
|
2754
|
-
deviceConnectionStaleMs,
|
|
2755
|
-
deviceConnectionSweepMs,
|
|
2756
|
-
taskTimeoutMs,
|
|
2757
|
-
managerPackage,
|
|
2758
|
-
managerVersion,
|
|
2751
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2752
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2753
|
+
heartbeatMs,
|
|
2754
|
+
deviceConnectionStaleMs,
|
|
2755
|
+
deviceConnectionSweepMs,
|
|
2756
|
+
taskTimeoutMs,
|
|
2757
|
+
managerPackage,
|
|
2758
|
+
managerVersion,
|
|
2759
2759
|
hostInstanceId,
|
|
2760
2760
|
agentPackage: '@livedesk/client',
|
|
2761
2761
|
agentEndpoint: routeInfo.endpoint,
|
|
@@ -3018,7 +3018,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3018
3018
|
return true;
|
|
3019
3019
|
}
|
|
3020
3020
|
|
|
3021
|
-
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3021
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3022
3022
|
if (!device?.deviceId) {
|
|
3023
3023
|
return;
|
|
3024
3024
|
}
|
|
@@ -3035,31 +3035,31 @@ export function createRemoteHub(options = {}) {
|
|
|
3035
3035
|
result: null,
|
|
3036
3036
|
error: safeString(error, 500) || 'command-result-unavailable'
|
|
3037
3037
|
});
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3042
|
-
if (!device?.deviceId) {
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3046
|
-
if (waiter.deviceId !== device.deviceId
|
|
3047
|
-
|| waiter.sessionId !== device.sessionId
|
|
3048
|
-
|| waiter.channel !== 'file') {
|
|
3049
|
-
continue;
|
|
3050
|
-
}
|
|
3051
|
-
pendingCommandResultWaiters.delete(waiterKey);
|
|
3052
|
-
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3053
|
-
waiter.resolve({
|
|
3054
|
-
ok: false,
|
|
3055
|
-
commandId: waiter.commandId,
|
|
3056
|
-
result: null,
|
|
3057
|
-
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3042
|
+
if (!device?.deviceId) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3046
|
+
if (waiter.deviceId !== device.deviceId
|
|
3047
|
+
|| waiter.sessionId !== device.sessionId
|
|
3048
|
+
|| waiter.channel !== 'file') {
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
pendingCommandResultWaiters.delete(waiterKey);
|
|
3052
|
+
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3053
|
+
waiter.resolve({
|
|
3054
|
+
ok: false,
|
|
3055
|
+
commandId: waiter.commandId,
|
|
3056
|
+
result: null,
|
|
3057
|
+
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
3063
3063
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3064
3064
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3065
3065
|
device?.deviceId,
|
|
@@ -3080,7 +3080,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3080
3080
|
return true;
|
|
3081
3081
|
}
|
|
3082
3082
|
|
|
3083
|
-
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3083
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3084
3084
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3085
3085
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3086
3086
|
device?.deviceId,
|
|
@@ -3106,13 +3106,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3106
3106
|
error: 'command-result-wait-replaced'
|
|
3107
3107
|
});
|
|
3108
3108
|
}
|
|
3109
|
-
const waiterRecord = {
|
|
3110
|
-
deviceId: device.deviceId,
|
|
3111
|
-
sessionId: device.sessionId,
|
|
3112
|
-
commandId: normalizedCommandId,
|
|
3113
|
-
channel: safeString(metadata.channel, 24),
|
|
3114
|
-
operationId: safeString(metadata.operationId, 128),
|
|
3115
|
-
timer: null,
|
|
3109
|
+
const waiterRecord = {
|
|
3110
|
+
deviceId: device.deviceId,
|
|
3111
|
+
sessionId: device.sessionId,
|
|
3112
|
+
commandId: normalizedCommandId,
|
|
3113
|
+
channel: safeString(metadata.channel, 24),
|
|
3114
|
+
operationId: safeString(metadata.operationId, 128),
|
|
3115
|
+
timer: null,
|
|
3116
3116
|
deadlineImmediate: null,
|
|
3117
3117
|
resolve
|
|
3118
3118
|
};
|
|
@@ -3145,246 +3145,246 @@ export function createRemoteHub(options = {}) {
|
|
|
3145
3145
|
});
|
|
3146
3146
|
}, timeoutMs);
|
|
3147
3147
|
waiterRecord.timer.unref?.();
|
|
3148
|
-
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3153
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3154
|
-
const normalizedSessionId = safeString(sessionId, 160);
|
|
3155
|
-
const normalizedOperationId = safeString(operationId, 128);
|
|
3156
|
-
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3157
|
-
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3158
|
-
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3159
|
-
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3160
|
-
: '';
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3164
|
-
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3165
|
-
return false;
|
|
3166
|
-
}
|
|
3167
|
-
const cleanupCommand = operation.direction === 'paste'
|
|
3168
|
-
? 'clipboard.paste.cancel'
|
|
3169
|
-
: 'clipboard.copy.end';
|
|
3170
|
-
const commandId = crypto.randomUUID();
|
|
3171
|
-
try {
|
|
3172
|
-
writeJsonLine(device.fileSocket, {
|
|
3173
|
-
type: 'command',
|
|
3174
|
-
commandId,
|
|
3175
|
-
command: cleanupCommand,
|
|
3176
|
-
payload: {
|
|
3177
|
-
operationId: operation.operationId,
|
|
3178
|
-
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3179
|
-
},
|
|
3180
|
-
issuedAt: new Date().toISOString()
|
|
3181
|
-
});
|
|
3182
|
-
device.counters.commandsSent += 1;
|
|
3183
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3184
|
-
commandId,
|
|
3185
|
-
command: cleanupCommand,
|
|
3186
|
-
channel: 'file',
|
|
3187
|
-
operationId: operation.operationId,
|
|
3188
|
-
cleanup: true
|
|
3189
|
-
});
|
|
3190
|
-
return true;
|
|
3191
|
-
} catch {
|
|
3192
|
-
// The exact side channel is already closing. Native session
|
|
3193
|
-
// teardown remains the authoritative cleanup.
|
|
3194
|
-
return false;
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3199
|
-
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3200
|
-
let cleared = 0;
|
|
3201
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3202
|
-
if (operation.deviceId !== device?.deviceId
|
|
3203
|
-
|| operation.sessionId !== device?.sessionId
|
|
3204
|
-
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3205
|
-
continue;
|
|
3206
|
-
}
|
|
3207
|
-
queueClipboardAgentCleanup(device, operation, reason);
|
|
3208
|
-
clipboardOperations.delete(key);
|
|
3209
|
-
cleared += 1;
|
|
3210
|
-
}
|
|
3211
|
-
if (cleared > 0) {
|
|
3212
|
-
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3213
|
-
reason,
|
|
3214
|
-
ownerConnectionId: requestedOwner,
|
|
3215
|
-
count: cleared
|
|
3216
|
-
});
|
|
3217
|
-
}
|
|
3218
|
-
return cleared;
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3222
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3223
|
-
const currentDevice = devices.get(operation.deviceId);
|
|
3224
|
-
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3225
|
-
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3226
|
-
const staleOwner = !currentDevice
|
|
3227
|
-
|| currentDevice.sessionId !== operation.sessionId
|
|
3228
|
-
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3229
|
-
if (expired || staleOwner) {
|
|
3230
|
-
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3231
|
-
queueClipboardAgentCleanup(
|
|
3232
|
-
currentDevice,
|
|
3233
|
-
operation,
|
|
3234
|
-
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3235
|
-
}
|
|
3236
|
-
clipboardOperations.delete(key);
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
|
|
3241
|
-
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3242
|
-
const capabilities = device?.capabilities || {};
|
|
3243
|
-
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3244
|
-
|| capabilities.clipboardText !== true) {
|
|
3245
|
-
return 'device-clipboard-text-unavailable';
|
|
3246
|
-
}
|
|
3247
|
-
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3248
|
-
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3249
|
-
}
|
|
3250
|
-
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3251
|
-
return 'device-clipboard-copy-unavailable';
|
|
3252
|
-
}
|
|
3253
|
-
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3254
|
-
return 'device-clipboard-paste-unavailable';
|
|
3255
|
-
}
|
|
3256
|
-
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3257
|
-
return 'device-clipboard-image-unavailable';
|
|
3258
|
-
}
|
|
3259
|
-
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3260
|
-
return 'device-clipboard-files-unavailable';
|
|
3261
|
-
}
|
|
3262
|
-
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3263
|
-
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3264
|
-
return 'device-clipboard-limits-unavailable';
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3268
|
-
return 'device-clipboard-chunk-limit-unavailable';
|
|
3269
|
-
}
|
|
3270
|
-
if (contentKind === 'image'
|
|
3271
|
-
&& (!Number.isSafeInteger(Number(
|
|
3272
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3273
|
-
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3274
|
-
return 'device-clipboard-limits-unavailable';
|
|
3275
|
-
}
|
|
3276
|
-
if (contentKind === 'files'
|
|
3277
|
-
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3278
|
-
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3279
|
-
return 'device-clipboard-limits-unavailable';
|
|
3280
|
-
}
|
|
3281
|
-
return '';
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
function clipboardManifestCapabilityError(device, manifest) {
|
|
3285
|
-
if (!manifest) {
|
|
3286
|
-
return '';
|
|
3287
|
-
}
|
|
3288
|
-
const capabilities = device?.capabilities || {};
|
|
3289
|
-
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3290
|
-
return 'device-clipboard-total-limit-exceeded';
|
|
3291
|
-
}
|
|
3292
|
-
if (manifest.contentKind === 'text'
|
|
3293
|
-
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3294
|
-
return 'device-clipboard-text-limit-exceeded';
|
|
3295
|
-
}
|
|
3296
|
-
if (manifest.contentKind === 'image'
|
|
3297
|
-
&& manifest.totalBytes > Number(
|
|
3298
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3299
|
-
return 'device-clipboard-image-limit-exceeded';
|
|
3300
|
-
}
|
|
3301
|
-
if (manifest.contentKind === 'files'
|
|
3302
|
-
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3303
|
-
return 'device-clipboard-file-count-limit-exceeded';
|
|
3304
|
-
}
|
|
3305
|
-
return '';
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3309
|
-
const device = devices.get(String(deviceId || ''));
|
|
3310
|
-
if (!device) {
|
|
3311
|
-
return { ok: false, error: 'device-not-found' };
|
|
3312
|
-
}
|
|
3313
|
-
if (device.synthetic === true) {
|
|
3314
|
-
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3315
|
-
}
|
|
3316
|
-
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3317
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3318
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3319
|
-
}
|
|
3320
|
-
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3321
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
const policy = getDevicePolicy(device);
|
|
3325
|
-
if (policy.accessMode === 'block-remote-access') {
|
|
3326
|
-
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3327
|
-
}
|
|
3328
|
-
if (policy.allowControl !== true) {
|
|
3329
|
-
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3330
|
-
}
|
|
3331
|
-
if (policy.allowClipboardText !== true) {
|
|
3332
|
-
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3333
|
-
}
|
|
3334
|
-
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3335
|
-
&& policy.allowFileTransfer !== true) {
|
|
3336
|
-
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
const direction = request.clipboardDirection
|
|
3340
|
-
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3341
|
-
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3342
|
-
if (capabilityError) {
|
|
3343
|
-
return { ok: false, error: capabilityError };
|
|
3344
|
-
}
|
|
3345
|
-
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3346
|
-
device,
|
|
3347
|
-
request.payload?.manifest || request.manifest);
|
|
3348
|
-
if (manifestCapabilityError) {
|
|
3349
|
-
return { ok: false, error: manifestCapabilityError };
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
const controlStream = getActiveControlStream(device);
|
|
3353
|
-
if (!controlStream
|
|
3354
|
-
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3355
|
-
|| !safeString(controlStream.commandId, 128)
|
|
3356
|
-
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3357
|
-
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3358
|
-
}
|
|
3359
|
-
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3360
|
-
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3361
|
-
}
|
|
3362
|
-
const binding = request.binding || request;
|
|
3363
|
-
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3364
|
-
!== safeString(device.sessionId, 160)
|
|
3365
|
-
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3366
|
-
!== safeString(controlStream.commandId, 128)) {
|
|
3367
|
-
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3368
|
-
}
|
|
3369
|
-
if (Number(binding.captureGeneration || 0)
|
|
3370
|
-
!== Number(controlStream.captureGeneration || 0)) {
|
|
3371
|
-
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3372
|
-
}
|
|
3373
|
-
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3374
|
-
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3375
|
-
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3376
|
-
}
|
|
3377
|
-
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3378
|
-
if (!hubConnectionId
|
|
3379
|
-
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3380
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
3381
|
-
}
|
|
3382
|
-
if (options.requireFileSocket === true
|
|
3383
|
-
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3384
|
-
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3385
|
-
}
|
|
3386
|
-
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3387
|
-
}
|
|
3148
|
+
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3153
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3154
|
+
const normalizedSessionId = safeString(sessionId, 160);
|
|
3155
|
+
const normalizedOperationId = safeString(operationId, 128);
|
|
3156
|
+
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3157
|
+
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3158
|
+
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3159
|
+
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3160
|
+
: '';
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3164
|
+
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
const cleanupCommand = operation.direction === 'paste'
|
|
3168
|
+
? 'clipboard.paste.cancel'
|
|
3169
|
+
: 'clipboard.copy.end';
|
|
3170
|
+
const commandId = crypto.randomUUID();
|
|
3171
|
+
try {
|
|
3172
|
+
writeJsonLine(device.fileSocket, {
|
|
3173
|
+
type: 'command',
|
|
3174
|
+
commandId,
|
|
3175
|
+
command: cleanupCommand,
|
|
3176
|
+
payload: {
|
|
3177
|
+
operationId: operation.operationId,
|
|
3178
|
+
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3179
|
+
},
|
|
3180
|
+
issuedAt: new Date().toISOString()
|
|
3181
|
+
});
|
|
3182
|
+
device.counters.commandsSent += 1;
|
|
3183
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3184
|
+
commandId,
|
|
3185
|
+
command: cleanupCommand,
|
|
3186
|
+
channel: 'file',
|
|
3187
|
+
operationId: operation.operationId,
|
|
3188
|
+
cleanup: true
|
|
3189
|
+
});
|
|
3190
|
+
return true;
|
|
3191
|
+
} catch {
|
|
3192
|
+
// The exact side channel is already closing. Native session
|
|
3193
|
+
// teardown remains the authoritative cleanup.
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3199
|
+
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3200
|
+
let cleared = 0;
|
|
3201
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3202
|
+
if (operation.deviceId !== device?.deviceId
|
|
3203
|
+
|| operation.sessionId !== device?.sessionId
|
|
3204
|
+
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
queueClipboardAgentCleanup(device, operation, reason);
|
|
3208
|
+
clipboardOperations.delete(key);
|
|
3209
|
+
cleared += 1;
|
|
3210
|
+
}
|
|
3211
|
+
if (cleared > 0) {
|
|
3212
|
+
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3213
|
+
reason,
|
|
3214
|
+
ownerConnectionId: requestedOwner,
|
|
3215
|
+
count: cleared
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return cleared;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3222
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3223
|
+
const currentDevice = devices.get(operation.deviceId);
|
|
3224
|
+
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3225
|
+
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3226
|
+
const staleOwner = !currentDevice
|
|
3227
|
+
|| currentDevice.sessionId !== operation.sessionId
|
|
3228
|
+
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3229
|
+
if (expired || staleOwner) {
|
|
3230
|
+
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3231
|
+
queueClipboardAgentCleanup(
|
|
3232
|
+
currentDevice,
|
|
3233
|
+
operation,
|
|
3234
|
+
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3235
|
+
}
|
|
3236
|
+
clipboardOperations.delete(key);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3242
|
+
const capabilities = device?.capabilities || {};
|
|
3243
|
+
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3244
|
+
|| capabilities.clipboardText !== true) {
|
|
3245
|
+
return 'device-clipboard-text-unavailable';
|
|
3246
|
+
}
|
|
3247
|
+
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3248
|
+
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3249
|
+
}
|
|
3250
|
+
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3251
|
+
return 'device-clipboard-copy-unavailable';
|
|
3252
|
+
}
|
|
3253
|
+
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3254
|
+
return 'device-clipboard-paste-unavailable';
|
|
3255
|
+
}
|
|
3256
|
+
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3257
|
+
return 'device-clipboard-image-unavailable';
|
|
3258
|
+
}
|
|
3259
|
+
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3260
|
+
return 'device-clipboard-files-unavailable';
|
|
3261
|
+
}
|
|
3262
|
+
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3263
|
+
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3264
|
+
return 'device-clipboard-limits-unavailable';
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3268
|
+
return 'device-clipboard-chunk-limit-unavailable';
|
|
3269
|
+
}
|
|
3270
|
+
if (contentKind === 'image'
|
|
3271
|
+
&& (!Number.isSafeInteger(Number(
|
|
3272
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3273
|
+
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3274
|
+
return 'device-clipboard-limits-unavailable';
|
|
3275
|
+
}
|
|
3276
|
+
if (contentKind === 'files'
|
|
3277
|
+
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3278
|
+
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3279
|
+
return 'device-clipboard-limits-unavailable';
|
|
3280
|
+
}
|
|
3281
|
+
return '';
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function clipboardManifestCapabilityError(device, manifest) {
|
|
3285
|
+
if (!manifest) {
|
|
3286
|
+
return '';
|
|
3287
|
+
}
|
|
3288
|
+
const capabilities = device?.capabilities || {};
|
|
3289
|
+
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3290
|
+
return 'device-clipboard-total-limit-exceeded';
|
|
3291
|
+
}
|
|
3292
|
+
if (manifest.contentKind === 'text'
|
|
3293
|
+
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3294
|
+
return 'device-clipboard-text-limit-exceeded';
|
|
3295
|
+
}
|
|
3296
|
+
if (manifest.contentKind === 'image'
|
|
3297
|
+
&& manifest.totalBytes > Number(
|
|
3298
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3299
|
+
return 'device-clipboard-image-limit-exceeded';
|
|
3300
|
+
}
|
|
3301
|
+
if (manifest.contentKind === 'files'
|
|
3302
|
+
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3303
|
+
return 'device-clipboard-file-count-limit-exceeded';
|
|
3304
|
+
}
|
|
3305
|
+
return '';
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3309
|
+
const device = devices.get(String(deviceId || ''));
|
|
3310
|
+
if (!device) {
|
|
3311
|
+
return { ok: false, error: 'device-not-found' };
|
|
3312
|
+
}
|
|
3313
|
+
if (device.synthetic === true) {
|
|
3314
|
+
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3315
|
+
}
|
|
3316
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3317
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3318
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3319
|
+
}
|
|
3320
|
+
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3321
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
const policy = getDevicePolicy(device);
|
|
3325
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
3326
|
+
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3327
|
+
}
|
|
3328
|
+
if (policy.allowControl !== true) {
|
|
3329
|
+
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3330
|
+
}
|
|
3331
|
+
if (policy.allowClipboardText !== true) {
|
|
3332
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3333
|
+
}
|
|
3334
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3335
|
+
&& policy.allowFileTransfer !== true) {
|
|
3336
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const direction = request.clipboardDirection
|
|
3340
|
+
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3341
|
+
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3342
|
+
if (capabilityError) {
|
|
3343
|
+
return { ok: false, error: capabilityError };
|
|
3344
|
+
}
|
|
3345
|
+
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3346
|
+
device,
|
|
3347
|
+
request.payload?.manifest || request.manifest);
|
|
3348
|
+
if (manifestCapabilityError) {
|
|
3349
|
+
return { ok: false, error: manifestCapabilityError };
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
const controlStream = getActiveControlStream(device);
|
|
3353
|
+
if (!controlStream
|
|
3354
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3355
|
+
|| !safeString(controlStream.commandId, 128)
|
|
3356
|
+
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3357
|
+
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3358
|
+
}
|
|
3359
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3360
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3361
|
+
}
|
|
3362
|
+
const binding = request.binding || request;
|
|
3363
|
+
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3364
|
+
!== safeString(device.sessionId, 160)
|
|
3365
|
+
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3366
|
+
!== safeString(controlStream.commandId, 128)) {
|
|
3367
|
+
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3368
|
+
}
|
|
3369
|
+
if (Number(binding.captureGeneration || 0)
|
|
3370
|
+
!== Number(controlStream.captureGeneration || 0)) {
|
|
3371
|
+
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3372
|
+
}
|
|
3373
|
+
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3374
|
+
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3375
|
+
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3376
|
+
}
|
|
3377
|
+
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3378
|
+
if (!hubConnectionId
|
|
3379
|
+
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3380
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
3381
|
+
}
|
|
3382
|
+
if (options.requireFileSocket === true
|
|
3383
|
+
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3384
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3385
|
+
}
|
|
3386
|
+
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3387
|
+
}
|
|
3388
3388
|
|
|
3389
3389
|
function inspectAnnexBH264(payload) {
|
|
3390
3390
|
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
@@ -4266,15 +4266,15 @@ export function createRemoteHub(options = {}) {
|
|
|
4266
4266
|
}
|
|
4267
4267
|
}
|
|
4268
4268
|
|
|
4269
|
-
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4270
|
-
const socket = device?.fileSocket;
|
|
4271
|
-
if (!socket) {
|
|
4272
|
-
return;
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4276
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4277
|
-
device.fileSocket = null;
|
|
4269
|
+
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4270
|
+
const socket = device?.fileSocket;
|
|
4271
|
+
if (!socket) {
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4276
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4277
|
+
device.fileSocket = null;
|
|
4278
4278
|
fileSockets.delete(socket);
|
|
4279
4279
|
try {
|
|
4280
4280
|
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
@@ -4288,22 +4288,24 @@ export function createRemoteHub(options = {}) {
|
|
|
4288
4288
|
}
|
|
4289
4289
|
}
|
|
4290
4290
|
|
|
4291
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4291
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4292
4292
|
const socket = device?.inputSocket;
|
|
4293
4293
|
if (!socket) {
|
|
4294
4294
|
return;
|
|
4295
4295
|
}
|
|
4296
4296
|
|
|
4297
|
-
const writeOwner = device.inputWriteOwner;
|
|
4298
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4297
|
+
const writeOwner = device.inputWriteOwner;
|
|
4298
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4299
4299
|
device.inputSocket = null;
|
|
4300
4300
|
device.inputWriteOwner = null;
|
|
4301
4301
|
device.inputSocketConnectionId = '';
|
|
4302
4302
|
device.inputOwnerConnectionId = '';
|
|
4303
|
-
device.inputOwnerBindingKey = '';
|
|
4304
|
-
inputSockets.delete(socket);
|
|
4305
|
-
writeOwner?.close?.(reason);
|
|
4306
|
-
|
|
4303
|
+
device.inputOwnerBindingKey = '';
|
|
4304
|
+
inputSockets.delete(socket);
|
|
4305
|
+
writeOwner?.close?.(reason);
|
|
4306
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
4307
|
+
emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
|
|
4308
|
+
try {
|
|
4307
4309
|
writeJsonLine(socket, { type: 'disconnect', reason });
|
|
4308
4310
|
} catch {
|
|
4309
4311
|
// Best-effort notice before closing the side channel.
|
|
@@ -4315,7 +4317,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4315
4317
|
}
|
|
4316
4318
|
}
|
|
4317
4319
|
|
|
4318
|
-
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4320
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4319
4321
|
const deviceId = inputSockets.get(socket);
|
|
4320
4322
|
inputSockets.delete(socket);
|
|
4321
4323
|
if (!deviceId) {
|
|
@@ -4327,8 +4329,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4327
4329
|
return;
|
|
4328
4330
|
}
|
|
4329
4331
|
|
|
4330
|
-
const writeOwner = device.inputWriteOwner;
|
|
4331
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4332
|
+
const writeOwner = device.inputWriteOwner;
|
|
4333
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4332
4334
|
device.inputSocket = null;
|
|
4333
4335
|
device.inputWriteOwner = null;
|
|
4334
4336
|
device.inputSocketConnectionId = '';
|
|
@@ -4373,7 +4375,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4373
4375
|
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
4374
4376
|
}
|
|
4375
4377
|
|
|
4376
|
-
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4378
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4377
4379
|
const deviceId = fileSockets.get(socket);
|
|
4378
4380
|
fileSockets.delete(socket);
|
|
4379
4381
|
if (!deviceId) {
|
|
@@ -4381,107 +4383,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4381
4383
|
}
|
|
4382
4384
|
|
|
4383
4385
|
const device = devices.get(deviceId);
|
|
4384
|
-
if (!device || device.fileSocket !== socket) {
|
|
4385
|
-
return;
|
|
4386
|
-
}
|
|
4387
|
-
|
|
4388
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4389
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4390
|
-
device.fileSocket = null;
|
|
4386
|
+
if (!device || device.fileSocket !== socket) {
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4391
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4392
|
+
device.fileSocket = null;
|
|
4391
4393
|
device.fileLastSeenAt = new Date().toISOString();
|
|
4392
|
-
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4393
|
-
}
|
|
4394
|
-
|
|
4395
|
-
function getDeviceConnectionHeartbeatMs(device) {
|
|
4396
|
-
return Date.parse(device?.lastStatusAt || '')
|
|
4397
|
-
|| Date.parse(device?.connectedAt || '')
|
|
4398
|
-
|| 0;
|
|
4399
|
-
}
|
|
4400
|
-
|
|
4401
|
-
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4394
|
+
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
function getDeviceConnectionHeartbeatMs(device) {
|
|
4398
|
+
return Date.parse(device?.lastStatusAt || '')
|
|
4399
|
+
|| Date.parse(device?.connectedAt || '')
|
|
4400
|
+
|| 0;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4402
4404
|
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4403
|
-
return false;
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4407
|
-
if (!lastHeartbeatMs) {
|
|
4408
|
-
return true;
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
|
-
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4415
|
-
const deviceId = safeString(device?.deviceId, 160);
|
|
4416
|
-
const sessionId = safeString(device?.sessionId, 160);
|
|
4417
|
-
const socket = device?.socket;
|
|
4418
|
-
if (!deviceId
|
|
4419
|
-
|| !sessionId
|
|
4420
|
-
|| !socket
|
|
4421
|
-
|| socket.destroyed
|
|
4422
|
-
|| devices.get(deviceId) !== device
|
|
4423
|
-
|| device.socket !== socket
|
|
4424
|
-
|| device.sessionId !== sessionId) {
|
|
4425
|
-
return false;
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
closeInputSocket(device, reason);
|
|
4429
|
-
closeFrameSocket(device, reason);
|
|
4430
|
-
closeAudioSocket(device, reason);
|
|
4431
|
-
closeFileSocket(device, reason);
|
|
4432
|
-
try {
|
|
4433
|
-
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4434
|
-
} catch {
|
|
4435
|
-
// The stale connection is retired below even if the final notice fails.
|
|
4436
|
-
}
|
|
4437
|
-
detachSocket(socket, reason);
|
|
4438
|
-
try {
|
|
4439
|
-
socket.destroy?.();
|
|
4440
|
-
} catch {
|
|
4441
|
-
// detachSocket already made the exact session terminal.
|
|
4442
|
-
}
|
|
4443
|
-
return true;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4447
|
-
pruneClipboardOperations(nowMs);
|
|
4448
|
-
let retired = 0;
|
|
4449
|
-
for (const device of devices.values()) {
|
|
4450
|
-
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4451
|
-
continue;
|
|
4452
|
-
}
|
|
4453
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4454
|
-
continue;
|
|
4455
|
-
}
|
|
4456
|
-
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4457
|
-
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4458
|
-
retired += 1;
|
|
4459
|
-
logWarn(
|
|
4460
|
-
'remote',
|
|
4461
|
-
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4462
|
-
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4463
|
-
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
return retired;
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
function scheduleDeviceConnectionSweep() {
|
|
4470
|
-
if (!started || deviceConnectionSweepTimer) {
|
|
4471
|
-
return;
|
|
4472
|
-
}
|
|
4473
|
-
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4474
|
-
deviceConnectionSweepTimer = null;
|
|
4475
|
-
if (!started) {
|
|
4476
|
-
return;
|
|
4477
|
-
}
|
|
4478
|
-
sweepStaleDeviceConnections();
|
|
4479
|
-
scheduleDeviceConnectionSweep();
|
|
4480
|
-
}, deviceConnectionSweepMs);
|
|
4481
|
-
deviceConnectionSweepTimer.unref?.();
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
4405
|
+
return false;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4409
|
+
if (!lastHeartbeatMs) {
|
|
4410
|
+
return true;
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4417
|
+
const deviceId = safeString(device?.deviceId, 160);
|
|
4418
|
+
const sessionId = safeString(device?.sessionId, 160);
|
|
4419
|
+
const socket = device?.socket;
|
|
4420
|
+
if (!deviceId
|
|
4421
|
+
|| !sessionId
|
|
4422
|
+
|| !socket
|
|
4423
|
+
|| socket.destroyed
|
|
4424
|
+
|| devices.get(deviceId) !== device
|
|
4425
|
+
|| device.socket !== socket
|
|
4426
|
+
|| device.sessionId !== sessionId) {
|
|
4427
|
+
return false;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
closeInputSocket(device, reason);
|
|
4431
|
+
closeFrameSocket(device, reason);
|
|
4432
|
+
closeAudioSocket(device, reason);
|
|
4433
|
+
closeFileSocket(device, reason);
|
|
4434
|
+
try {
|
|
4435
|
+
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4436
|
+
} catch {
|
|
4437
|
+
// The stale connection is retired below even if the final notice fails.
|
|
4438
|
+
}
|
|
4439
|
+
detachSocket(socket, reason);
|
|
4440
|
+
try {
|
|
4441
|
+
socket.destroy?.();
|
|
4442
|
+
} catch {
|
|
4443
|
+
// detachSocket already made the exact session terminal.
|
|
4444
|
+
}
|
|
4445
|
+
return true;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4449
|
+
pruneClipboardOperations(nowMs);
|
|
4450
|
+
let retired = 0;
|
|
4451
|
+
for (const device of devices.values()) {
|
|
4452
|
+
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4455
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4459
|
+
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4460
|
+
retired += 1;
|
|
4461
|
+
logWarn(
|
|
4462
|
+
'remote',
|
|
4463
|
+
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4464
|
+
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4465
|
+
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
return retired;
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function scheduleDeviceConnectionSweep() {
|
|
4472
|
+
if (!started || deviceConnectionSweepTimer) {
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4476
|
+
deviceConnectionSweepTimer = null;
|
|
4477
|
+
if (!started) {
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
sweepStaleDeviceConnections();
|
|
4481
|
+
scheduleDeviceConnectionSweep();
|
|
4482
|
+
}, deviceConnectionSweepMs);
|
|
4483
|
+
deviceConnectionSweepTimer.unref?.();
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
4485
4487
|
const retryAfterMs = Math.max(heartbeatMs, duplicateDeviceRetryAfterMs);
|
|
4486
4488
|
writeJsonLine(socket, {
|
|
4487
4489
|
type: 'disconnect',
|
|
@@ -4933,20 +4935,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4933
4935
|
return device;
|
|
4934
4936
|
}
|
|
4935
4937
|
|
|
4936
|
-
function attachInputSocket(socket, hello) {
|
|
4937
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4938
|
-
const device = devices.get(deviceId);
|
|
4938
|
+
function attachInputSocket(socket, hello) {
|
|
4939
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4940
|
+
const device = devices.get(deviceId);
|
|
4939
4941
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4940
4942
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4941
4943
|
socket.destroy();
|
|
4942
|
-
return null;
|
|
4943
|
-
}
|
|
4944
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4945
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4946
|
-
return null;
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4947
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4948
|
+
return null;
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
4950
4952
|
|
|
4951
4953
|
const now = new Date().toISOString();
|
|
4952
4954
|
const inputSocketConnectionId = crypto.randomUUID();
|
|
@@ -5060,20 +5062,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5060
5062
|
return device;
|
|
5061
5063
|
}
|
|
5062
5064
|
|
|
5063
|
-
function attachFileSocket(socket, hello) {
|
|
5064
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5065
|
-
const device = devices.get(deviceId);
|
|
5065
|
+
function attachFileSocket(socket, hello) {
|
|
5066
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5067
|
+
const device = devices.get(deviceId);
|
|
5066
5068
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5067
5069
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5068
5070
|
socket.destroy();
|
|
5069
|
-
return null;
|
|
5070
|
-
}
|
|
5071
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5072
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5073
|
-
return null;
|
|
5074
|
-
}
|
|
5075
|
-
|
|
5076
|
-
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5074
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5075
|
+
return null;
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
5077
5079
|
const now = new Date().toISOString();
|
|
5078
5080
|
device.fileSocket = socket;
|
|
5079
5081
|
device.fileConnectedAt = now;
|
|
@@ -8376,9 +8378,9 @@ export function createRemoteHub(options = {}) {
|
|
|
8376
8378
|
started = true;
|
|
8377
8379
|
boundPort = candidateServer.address()?.port || port;
|
|
8378
8380
|
lastError = '';
|
|
8379
|
-
logEvent('remote', `
|
|
8381
|
+
logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
8380
8382
|
if (host === '0.0.0.0' || host === '::') {
|
|
8381
|
-
logWarn('remote', '
|
|
8383
|
+
logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
|
|
8382
8384
|
}
|
|
8383
8385
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8384
8386
|
resolve();
|
|
@@ -8396,14 +8398,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8396
8398
|
await listenOnPort(requestedPort);
|
|
8397
8399
|
try {
|
|
8398
8400
|
await relayControl?.start?.();
|
|
8399
|
-
} catch {
|
|
8400
|
-
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8401
|
-
}
|
|
8402
|
-
scheduleDeviceConnectionSweep();
|
|
8403
|
-
} catch (error) {
|
|
8404
|
-
if (error?.code === 'EADDRINUSE') {
|
|
8405
|
-
const conflict = new Error(
|
|
8406
|
-
`
|
|
8401
|
+
} catch {
|
|
8402
|
+
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8403
|
+
}
|
|
8404
|
+
scheduleDeviceConnectionSweep();
|
|
8405
|
+
} catch (error) {
|
|
8406
|
+
if (error?.code === 'EADDRINUSE') {
|
|
8407
|
+
const conflict = new Error(
|
|
8408
|
+
`VuvoDesk Hub requires client endpoint TCP ${requestedPort}, but that port is already in use. Close the owning process or start Hub with an explicit --remote-port value.`);
|
|
8407
8409
|
conflict.code = 'EADDRINUSE';
|
|
8408
8410
|
conflict.port = requestedPort;
|
|
8409
8411
|
conflict.cause = error;
|
|
@@ -8414,16 +8416,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8414
8416
|
}
|
|
8415
8417
|
|
|
8416
8418
|
return getStatus({ includeSecrets: false });
|
|
8417
|
-
}
|
|
8418
|
-
|
|
8419
|
-
async function close() {
|
|
8420
|
-
if (deviceConnectionSweepTimer) {
|
|
8421
|
-
clearTimeout(deviceConnectionSweepTimer);
|
|
8422
|
-
deviceConnectionSweepTimer = null;
|
|
8423
|
-
}
|
|
8424
|
-
for (const state of [...agentBinaryIngressStates]) {
|
|
8425
|
-
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8426
|
-
}
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
async function close() {
|
|
8422
|
+
if (deviceConnectionSweepTimer) {
|
|
8423
|
+
clearTimeout(deviceConnectionSweepTimer);
|
|
8424
|
+
deviceConnectionSweepTimer = null;
|
|
8425
|
+
}
|
|
8426
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
8427
|
+
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8428
|
+
}
|
|
8427
8429
|
|
|
8428
8430
|
for (const device of devices.values()) {
|
|
8429
8431
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
@@ -8558,12 +8560,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8558
8560
|
};
|
|
8559
8561
|
}
|
|
8560
8562
|
|
|
8561
|
-
function sendCommand(deviceId, command) {
|
|
8562
|
-
const device = devices.get(String(deviceId || ''));
|
|
8563
|
-
const commandName = safeString(command?.command || 'ping', 80);
|
|
8564
|
-
if (REMOTE_CLIPBOARD_FILE_ACTIONS.has(commandName)) {
|
|
8565
|
-
return { ok: false, error: 'clipboard-command-requires-awaited-file-channel' };
|
|
8566
|
-
}
|
|
8563
|
+
function sendCommand(deviceId, command) {
|
|
8564
|
+
const device = devices.get(String(deviceId || ''));
|
|
8565
|
+
const commandName = safeString(command?.command || 'ping', 80);
|
|
8566
|
+
if (REMOTE_CLIPBOARD_FILE_ACTIONS.has(commandName)) {
|
|
8567
|
+
return { ok: false, error: 'clipboard-command-requires-awaited-file-channel' };
|
|
8568
|
+
}
|
|
8567
8569
|
const requiredPermission = commandName === 'input.control'
|
|
8568
8570
|
? 'allowControl'
|
|
8569
8571
|
: commandName === 'system.power'
|
|
@@ -8599,16 +8601,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8599
8601
|
result: { ok: true, synthetic: true },
|
|
8600
8602
|
error: ''
|
|
8601
8603
|
});
|
|
8602
|
-
return { ok: true, commandId, synthetic: true };
|
|
8603
|
-
}
|
|
8604
|
-
|
|
8605
|
-
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
8606
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8607
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8608
|
-
}
|
|
8609
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8610
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8611
|
-
}
|
|
8604
|
+
return { ok: true, commandId, synthetic: true };
|
|
8605
|
+
}
|
|
8606
|
+
|
|
8607
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
8608
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8609
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8610
|
+
}
|
|
8611
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8612
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8613
|
+
}
|
|
8612
8614
|
|
|
8613
8615
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8614
8616
|
const payload = {
|
|
@@ -8621,300 +8623,300 @@ export function createRemoteHub(options = {}) {
|
|
|
8621
8623
|
|
|
8622
8624
|
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
8623
8625
|
&& device.fileSocket
|
|
8624
|
-
&& !device.fileSocket.destroyed
|
|
8625
|
-
? device.fileSocket
|
|
8626
|
-
: null;
|
|
8627
|
-
try {
|
|
8628
|
-
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8629
|
-
} catch {
|
|
8630
|
-
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8631
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8632
|
-
}
|
|
8633
|
-
device.counters.commandsSent += 1;
|
|
8634
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8635
|
-
commandId,
|
|
8626
|
+
&& !device.fileSocket.destroyed
|
|
8627
|
+
? device.fileSocket
|
|
8628
|
+
: null;
|
|
8629
|
+
try {
|
|
8630
|
+
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8631
|
+
} catch {
|
|
8632
|
+
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8633
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8634
|
+
}
|
|
8635
|
+
device.counters.commandsSent += 1;
|
|
8636
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8637
|
+
commandId,
|
|
8636
8638
|
command: payload.command,
|
|
8637
8639
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8638
8640
|
});
|
|
8639
|
-
return { ok: true, commandId };
|
|
8640
|
-
}
|
|
8641
|
-
|
|
8642
|
-
async function sendClipboardCommand(deviceId, request = {}) {
|
|
8643
|
-
pruneClipboardOperations();
|
|
8644
|
-
const action = safeString(request.action, 80).toLowerCase();
|
|
8645
|
-
if (!REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
8646
|
-
return { ok: false, error: 'clipboard-file-action-invalid' };
|
|
8647
|
-
}
|
|
8648
|
-
const operationId = safeString(request.operationId, 128);
|
|
8649
|
-
if (!operationId) {
|
|
8650
|
-
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
8651
|
-
}
|
|
8652
|
-
|
|
8653
|
-
const direction = action.startsWith('clipboard.paste.')
|
|
8654
|
-
? 'paste'
|
|
8655
|
-
: action.startsWith('clipboard.copy.')
|
|
8656
|
-
? 'copy'
|
|
8657
|
-
: '';
|
|
8658
|
-
const operationKey = buildClipboardOperationKey(deviceId, request.binding?.controlSessionId, operationId);
|
|
8659
|
-
let operation = clipboardOperations.get(operationKey);
|
|
8660
|
-
const beginsOperation = action === 'clipboard.copy.begin' || action === 'clipboard.paste.begin';
|
|
8661
|
-
const endsOperation = action === 'clipboard.copy.end' || action === 'clipboard.paste.cancel';
|
|
8662
|
-
const requestedManifest = request.payload?.manifest || null;
|
|
8663
|
-
|
|
8664
|
-
if (beginsOperation) {
|
|
8665
|
-
if (operation) {
|
|
8666
|
-
return { ok: false, error: 'clipboard-operation-already-exists' };
|
|
8667
|
-
}
|
|
8668
|
-
const activeForDevice = [...clipboardOperations.values()].filter(candidate =>
|
|
8669
|
-
candidate.deviceId === String(deviceId || '')
|
|
8670
|
-
&& candidate.sessionId === request.binding?.controlSessionId);
|
|
8671
|
-
if (activeForDevice.some(candidate => candidate.direction === direction)) {
|
|
8672
|
-
return { ok: false, error: 'clipboard-operation-direction-busy' };
|
|
8673
|
-
}
|
|
8674
|
-
if (activeForDevice.length >= REMOTE_CLIPBOARD_LIMITS.maxOperationsPerDevice) {
|
|
8675
|
-
return { ok: false, error: 'clipboard-operation-capacity-reached' };
|
|
8676
|
-
}
|
|
8677
|
-
operation = {
|
|
8678
|
-
deviceId: String(deviceId || ''),
|
|
8679
|
-
sessionId: safeString(request.binding?.controlSessionId, 160),
|
|
8680
|
-
operationId,
|
|
8681
|
-
direction,
|
|
8682
|
-
hubConnectionId: safeString(request.hubConnectionId, 128),
|
|
8683
|
-
controlCommandId: safeString(request.binding?.controlCommandId, 128),
|
|
8684
|
-
captureGeneration: Number(request.binding?.captureGeneration || 0),
|
|
8685
|
-
monitorIndex: Number(request.binding?.monitorIndex),
|
|
8686
|
-
contentKind: requestedManifest?.contentKind || 'text',
|
|
8687
|
-
manifest: requestedManifest,
|
|
8688
|
-
offsets: new Map(),
|
|
8689
|
-
status: action === 'clipboard.paste.begin' ? 'preparing' : 'copying',
|
|
8690
|
-
inFlight: false,
|
|
8691
|
-
touchedAt: Date.now()
|
|
8692
|
-
};
|
|
8693
|
-
clipboardOperations.set(operationKey, operation);
|
|
8694
|
-
} else {
|
|
8695
|
-
if (!operation) {
|
|
8696
|
-
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8697
|
-
}
|
|
8698
|
-
const requestedDirection = direction || operation.direction;
|
|
8699
|
-
if (operation.direction !== requestedDirection
|
|
8700
|
-
|| operation.hubConnectionId !== safeString(request.hubConnectionId, 128)
|
|
8701
|
-
|| operation.controlCommandId !== safeString(request.binding?.controlCommandId, 128)
|
|
8702
|
-
|| operation.captureGeneration !== Number(request.binding?.captureGeneration || 0)
|
|
8703
|
-
|| operation.monitorIndex !== Number(request.binding?.monitorIndex)) {
|
|
8704
|
-
return { ok: false, error: 'clipboard-operation-owner-stale' };
|
|
8705
|
-
}
|
|
8706
|
-
}
|
|
8707
|
-
|
|
8708
|
-
if (operation.inFlight) {
|
|
8709
|
-
return { ok: false, error: 'clipboard-operation-busy' };
|
|
8710
|
-
}
|
|
8711
|
-
|
|
8712
|
-
const contentKind = operation.contentKind || requestedManifest?.contentKind || 'text';
|
|
8713
|
-
const bindingResult = validateCurrentClipboardBinding(
|
|
8714
|
-
deviceId,
|
|
8715
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8716
|
-
contentKind,
|
|
8717
|
-
{ requireFileSocket: true });
|
|
8718
|
-
if (!bindingResult.ok) {
|
|
8719
|
-
if (beginsOperation || bindingResult.error?.startsWith('STALE_') || bindingResult.error === 'input-owner-not-current') {
|
|
8720
|
-
clipboardOperations.delete(operationKey);
|
|
8721
|
-
}
|
|
8722
|
-
return bindingResult;
|
|
8723
|
-
}
|
|
8724
|
-
|
|
8725
|
-
const { device } = bindingResult;
|
|
8726
|
-
const fileSocket = device.fileSocket;
|
|
8727
|
-
const itemIndex = request.payload?.itemIndex;
|
|
8728
|
-
const expectedOffset = Number.isInteger(itemIndex)
|
|
8729
|
-
? Number(operation.offsets.get(itemIndex) || 0)
|
|
8730
|
-
: 0;
|
|
8731
|
-
let outgoingChunkBytes = 0;
|
|
8732
|
-
if (action === 'clipboard.copy.chunk' || action === 'clipboard.paste.chunk') {
|
|
8733
|
-
const item = operation.manifest?.items?.[itemIndex];
|
|
8734
|
-
if (!item) {
|
|
8735
|
-
return { ok: false, error: 'clipboard-item-not-found' };
|
|
8736
|
-
}
|
|
8737
|
-
if (request.payload.offset !== expectedOffset) {
|
|
8738
|
-
return { ok: false, error: 'clipboard-chunk-offset-stale', expectedOffset };
|
|
8739
|
-
}
|
|
8740
|
-
if (action === 'clipboard.copy.chunk'
|
|
8741
|
-
&& request.payload.maxBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8742
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8743
|
-
}
|
|
8744
|
-
if (action === 'clipboard.paste.chunk') {
|
|
8745
|
-
outgoingChunkBytes = Buffer.from(request.payload.dataBase64, 'base64').length;
|
|
8746
|
-
if (outgoingChunkBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8747
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8748
|
-
}
|
|
8749
|
-
const nextOffset = expectedOffset + outgoingChunkBytes;
|
|
8750
|
-
if (outgoingChunkBytes === 0
|
|
8751
|
-
&& !(item.size === 0 && expectedOffset === 0 && request.payload.final === true)) {
|
|
8752
|
-
return { ok: false, error: 'clipboard-empty-chunk-invalid' };
|
|
8753
|
-
}
|
|
8754
|
-
if (nextOffset > item.size
|
|
8755
|
-
|| request.payload.final !== (nextOffset === item.size)) {
|
|
8756
|
-
return { ok: false, error: 'clipboard-chunk-boundary-invalid' };
|
|
8757
|
-
}
|
|
8758
|
-
}
|
|
8759
|
-
}
|
|
8760
|
-
|
|
8761
|
-
operation.inFlight = true;
|
|
8762
|
-
operation.touchedAt = Date.now();
|
|
8763
|
-
let commandCompleted = false;
|
|
8764
|
-
const commandId = crypto.randomUUID();
|
|
8765
|
-
const resultPromise = waitForCommandResult(
|
|
8766
|
-
device,
|
|
8767
|
-
commandId,
|
|
8768
|
-
REMOTE_CLIPBOARD_LIMITS.commandTimeoutMs,
|
|
8769
|
-
{ channel: 'file', operationId });
|
|
8770
|
-
const commandPayload = {
|
|
8771
|
-
type: 'command',
|
|
8772
|
-
commandId,
|
|
8773
|
-
command: action,
|
|
8774
|
-
payload: {
|
|
8775
|
-
operationId,
|
|
8776
|
-
controlSessionId: operation.sessionId,
|
|
8777
|
-
controlCommandId: operation.controlCommandId,
|
|
8778
|
-
captureGeneration: operation.captureGeneration,
|
|
8779
|
-
monitorIndex: operation.monitorIndex,
|
|
8780
|
-
hubConnectionId: operation.hubConnectionId,
|
|
8781
|
-
...(request.payload || {})
|
|
8782
|
-
},
|
|
8783
|
-
issuedAt: new Date().toISOString()
|
|
8784
|
-
};
|
|
8785
|
-
|
|
8786
|
-
try {
|
|
8787
|
-
try {
|
|
8788
|
-
// Clipboard payloads are never allowed onto the concurrent
|
|
8789
|
-
// main command socket. The dedicated file side channel owns
|
|
8790
|
-
// the one bounded command and its exact-session waiter.
|
|
8791
|
-
writeJsonLine(fileSocket, commandPayload);
|
|
8792
|
-
} catch {
|
|
8793
|
-
failPendingCommandResultWaiter(device, commandId, 'clipboard-file-channel-write-failed');
|
|
8794
|
-
if (device.fileSocket === fileSocket) {
|
|
8795
|
-
closeFileSocket(device, 'clipboard-file-channel-write-failed');
|
|
8796
|
-
}
|
|
8797
|
-
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
8798
|
-
}
|
|
8799
|
-
device.counters.commandsSent += 1;
|
|
8800
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8801
|
-
commandId,
|
|
8802
|
-
command: action,
|
|
8803
|
-
channel: 'file',
|
|
8804
|
-
operationId
|
|
8805
|
-
});
|
|
8806
|
-
|
|
8807
|
-
const awaited = await resultPromise;
|
|
8808
|
-
if (!awaited.ok) {
|
|
8809
|
-
return {
|
|
8810
|
-
ok: false,
|
|
8811
|
-
error: awaited.error || 'clipboard-command-failed',
|
|
8812
|
-
commandId
|
|
8813
|
-
};
|
|
8814
|
-
}
|
|
8815
|
-
if (device.fileSocket !== fileSocket) {
|
|
8816
|
-
return { ok: false, error: 'clipboard-file-channel-owner-changed' };
|
|
8817
|
-
}
|
|
8818
|
-
const currentBinding = validateCurrentClipboardBinding(
|
|
8819
|
-
deviceId,
|
|
8820
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8821
|
-
operation.contentKind || 'text',
|
|
8822
|
-
{ requireFileSocket: true });
|
|
8823
|
-
if (!currentBinding.ok) {
|
|
8824
|
-
return currentBinding;
|
|
8825
|
-
}
|
|
8826
|
-
|
|
8827
|
-
let response;
|
|
8828
|
-
try {
|
|
8829
|
-
response = normalizeRemoteClipboardCommandResult(action, operationId, awaited.result);
|
|
8830
|
-
} catch (error) {
|
|
8831
|
-
return {
|
|
8832
|
-
ok: false,
|
|
8833
|
-
error: safeString(error?.code || error?.message, 160) || 'clipboard-response-invalid'
|
|
8834
|
-
};
|
|
8835
|
-
}
|
|
8836
|
-
if (response.manifest) {
|
|
8837
|
-
const responseManifestLimitError = clipboardManifestCapabilityError(device, response.manifest);
|
|
8838
|
-
if (responseManifestLimitError) {
|
|
8839
|
-
clipboardOperations.delete(operationKey);
|
|
8840
|
-
return { ok: false, error: responseManifestLimitError };
|
|
8841
|
-
}
|
|
8842
|
-
const responsePolicy = validateCurrentClipboardBinding(
|
|
8843
|
-
deviceId,
|
|
8844
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8845
|
-
response.manifest.contentKind,
|
|
8846
|
-
{ requireFileSocket: true });
|
|
8847
|
-
if (!responsePolicy.ok) {
|
|
8848
|
-
clipboardOperations.delete(operationKey);
|
|
8849
|
-
return responsePolicy;
|
|
8850
|
-
}
|
|
8851
|
-
operation.manifest = response.manifest;
|
|
8852
|
-
operation.contentKind = response.manifest.contentKind;
|
|
8853
|
-
}
|
|
8854
|
-
if (action === 'clipboard.copy.chunk') {
|
|
8855
|
-
const item = operation.manifest?.items?.[response.itemIndex];
|
|
8856
|
-
const nextOffset = response.offset + response.bytes;
|
|
8857
|
-
if (!item
|
|
8858
|
-
|| response.itemIndex !== itemIndex
|
|
8859
|
-
|| response.offset !== expectedOffset
|
|
8860
|
-
|| (response.bytes === 0
|
|
8861
|
-
&& !(item.size === 0 && response.offset === 0 && response.final === true))
|
|
8862
|
-
|| nextOffset > item.size
|
|
8863
|
-
|| response.final !== (nextOffset === item.size)) {
|
|
8864
|
-
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8865
|
-
}
|
|
8866
|
-
if (response.bytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8867
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8868
|
-
}
|
|
8869
|
-
operation.offsets.set(response.itemIndex, nextOffset);
|
|
8870
|
-
} else if (action === 'clipboard.paste.chunk') {
|
|
8871
|
-
if (response.itemIndex !== itemIndex
|
|
8872
|
-
|| response.offset !== expectedOffset
|
|
8873
|
-
|| response.bytes !== outgoingChunkBytes
|
|
8874
|
-
|| response.totalBytes !== operation.manifest.items[itemIndex].size
|
|
8875
|
-
|| response.final !== request.payload.final) {
|
|
8876
|
-
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8877
|
-
}
|
|
8878
|
-
operation.offsets.set(itemIndex, expectedOffset + outgoingChunkBytes);
|
|
8879
|
-
}
|
|
8880
|
-
operation.status = response.status || operation.status;
|
|
8881
|
-
operation.touchedAt = Date.now();
|
|
8882
|
-
if (action === 'clipboard.operation.status' && operation.status === 'not-found') {
|
|
8883
|
-
clipboardOperations.delete(operationKey);
|
|
8884
|
-
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8885
|
-
}
|
|
8886
|
-
if (action === 'clipboard.operation.status'
|
|
8887
|
-
&& ['completed', 'complete', 'applied', 'pasted', 'cancelled', 'cleaned', 'failed', 'expired', 'already-ended']
|
|
8888
|
-
.includes(operation.status)) {
|
|
8889
|
-
clipboardOperations.delete(operationKey);
|
|
8890
|
-
}
|
|
8891
|
-
commandCompleted = true;
|
|
8892
|
-
return response;
|
|
8893
|
-
} finally {
|
|
8894
|
-
const currentOperation = clipboardOperations.get(operationKey);
|
|
8895
|
-
if (currentOperation === operation) {
|
|
8896
|
-
operation.inFlight = false;
|
|
8897
|
-
if (endsOperation || (beginsOperation && !commandCompleted)) {
|
|
8898
|
-
clipboardOperations.delete(operationKey);
|
|
8899
|
-
}
|
|
8900
|
-
}
|
|
8901
|
-
}
|
|
8902
|
-
}
|
|
8903
|
-
|
|
8904
|
-
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8641
|
+
return { ok: true, commandId };
|
|
8642
|
+
}
|
|
8643
|
+
|
|
8644
|
+
async function sendClipboardCommand(deviceId, request = {}) {
|
|
8645
|
+
pruneClipboardOperations();
|
|
8646
|
+
const action = safeString(request.action, 80).toLowerCase();
|
|
8647
|
+
if (!REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
8648
|
+
return { ok: false, error: 'clipboard-file-action-invalid' };
|
|
8649
|
+
}
|
|
8650
|
+
const operationId = safeString(request.operationId, 128);
|
|
8651
|
+
if (!operationId) {
|
|
8652
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
const direction = action.startsWith('clipboard.paste.')
|
|
8656
|
+
? 'paste'
|
|
8657
|
+
: action.startsWith('clipboard.copy.')
|
|
8658
|
+
? 'copy'
|
|
8659
|
+
: '';
|
|
8660
|
+
const operationKey = buildClipboardOperationKey(deviceId, request.binding?.controlSessionId, operationId);
|
|
8661
|
+
let operation = clipboardOperations.get(operationKey);
|
|
8662
|
+
const beginsOperation = action === 'clipboard.copy.begin' || action === 'clipboard.paste.begin';
|
|
8663
|
+
const endsOperation = action === 'clipboard.copy.end' || action === 'clipboard.paste.cancel';
|
|
8664
|
+
const requestedManifest = request.payload?.manifest || null;
|
|
8665
|
+
|
|
8666
|
+
if (beginsOperation) {
|
|
8667
|
+
if (operation) {
|
|
8668
|
+
return { ok: false, error: 'clipboard-operation-already-exists' };
|
|
8669
|
+
}
|
|
8670
|
+
const activeForDevice = [...clipboardOperations.values()].filter(candidate =>
|
|
8671
|
+
candidate.deviceId === String(deviceId || '')
|
|
8672
|
+
&& candidate.sessionId === request.binding?.controlSessionId);
|
|
8673
|
+
if (activeForDevice.some(candidate => candidate.direction === direction)) {
|
|
8674
|
+
return { ok: false, error: 'clipboard-operation-direction-busy' };
|
|
8675
|
+
}
|
|
8676
|
+
if (activeForDevice.length >= REMOTE_CLIPBOARD_LIMITS.maxOperationsPerDevice) {
|
|
8677
|
+
return { ok: false, error: 'clipboard-operation-capacity-reached' };
|
|
8678
|
+
}
|
|
8679
|
+
operation = {
|
|
8680
|
+
deviceId: String(deviceId || ''),
|
|
8681
|
+
sessionId: safeString(request.binding?.controlSessionId, 160),
|
|
8682
|
+
operationId,
|
|
8683
|
+
direction,
|
|
8684
|
+
hubConnectionId: safeString(request.hubConnectionId, 128),
|
|
8685
|
+
controlCommandId: safeString(request.binding?.controlCommandId, 128),
|
|
8686
|
+
captureGeneration: Number(request.binding?.captureGeneration || 0),
|
|
8687
|
+
monitorIndex: Number(request.binding?.monitorIndex),
|
|
8688
|
+
contentKind: requestedManifest?.contentKind || 'text',
|
|
8689
|
+
manifest: requestedManifest,
|
|
8690
|
+
offsets: new Map(),
|
|
8691
|
+
status: action === 'clipboard.paste.begin' ? 'preparing' : 'copying',
|
|
8692
|
+
inFlight: false,
|
|
8693
|
+
touchedAt: Date.now()
|
|
8694
|
+
};
|
|
8695
|
+
clipboardOperations.set(operationKey, operation);
|
|
8696
|
+
} else {
|
|
8697
|
+
if (!operation) {
|
|
8698
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8699
|
+
}
|
|
8700
|
+
const requestedDirection = direction || operation.direction;
|
|
8701
|
+
if (operation.direction !== requestedDirection
|
|
8702
|
+
|| operation.hubConnectionId !== safeString(request.hubConnectionId, 128)
|
|
8703
|
+
|| operation.controlCommandId !== safeString(request.binding?.controlCommandId, 128)
|
|
8704
|
+
|| operation.captureGeneration !== Number(request.binding?.captureGeneration || 0)
|
|
8705
|
+
|| operation.monitorIndex !== Number(request.binding?.monitorIndex)) {
|
|
8706
|
+
return { ok: false, error: 'clipboard-operation-owner-stale' };
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
if (operation.inFlight) {
|
|
8711
|
+
return { ok: false, error: 'clipboard-operation-busy' };
|
|
8712
|
+
}
|
|
8713
|
+
|
|
8714
|
+
const contentKind = operation.contentKind || requestedManifest?.contentKind || 'text';
|
|
8715
|
+
const bindingResult = validateCurrentClipboardBinding(
|
|
8716
|
+
deviceId,
|
|
8717
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8718
|
+
contentKind,
|
|
8719
|
+
{ requireFileSocket: true });
|
|
8720
|
+
if (!bindingResult.ok) {
|
|
8721
|
+
if (beginsOperation || bindingResult.error?.startsWith('STALE_') || bindingResult.error === 'input-owner-not-current') {
|
|
8722
|
+
clipboardOperations.delete(operationKey);
|
|
8723
|
+
}
|
|
8724
|
+
return bindingResult;
|
|
8725
|
+
}
|
|
8726
|
+
|
|
8727
|
+
const { device } = bindingResult;
|
|
8728
|
+
const fileSocket = device.fileSocket;
|
|
8729
|
+
const itemIndex = request.payload?.itemIndex;
|
|
8730
|
+
const expectedOffset = Number.isInteger(itemIndex)
|
|
8731
|
+
? Number(operation.offsets.get(itemIndex) || 0)
|
|
8732
|
+
: 0;
|
|
8733
|
+
let outgoingChunkBytes = 0;
|
|
8734
|
+
if (action === 'clipboard.copy.chunk' || action === 'clipboard.paste.chunk') {
|
|
8735
|
+
const item = operation.manifest?.items?.[itemIndex];
|
|
8736
|
+
if (!item) {
|
|
8737
|
+
return { ok: false, error: 'clipboard-item-not-found' };
|
|
8738
|
+
}
|
|
8739
|
+
if (request.payload.offset !== expectedOffset) {
|
|
8740
|
+
return { ok: false, error: 'clipboard-chunk-offset-stale', expectedOffset };
|
|
8741
|
+
}
|
|
8742
|
+
if (action === 'clipboard.copy.chunk'
|
|
8743
|
+
&& request.payload.maxBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8744
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8745
|
+
}
|
|
8746
|
+
if (action === 'clipboard.paste.chunk') {
|
|
8747
|
+
outgoingChunkBytes = Buffer.from(request.payload.dataBase64, 'base64').length;
|
|
8748
|
+
if (outgoingChunkBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8749
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8750
|
+
}
|
|
8751
|
+
const nextOffset = expectedOffset + outgoingChunkBytes;
|
|
8752
|
+
if (outgoingChunkBytes === 0
|
|
8753
|
+
&& !(item.size === 0 && expectedOffset === 0 && request.payload.final === true)) {
|
|
8754
|
+
return { ok: false, error: 'clipboard-empty-chunk-invalid' };
|
|
8755
|
+
}
|
|
8756
|
+
if (nextOffset > item.size
|
|
8757
|
+
|| request.payload.final !== (nextOffset === item.size)) {
|
|
8758
|
+
return { ok: false, error: 'clipboard-chunk-boundary-invalid' };
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
}
|
|
8762
|
+
|
|
8763
|
+
operation.inFlight = true;
|
|
8764
|
+
operation.touchedAt = Date.now();
|
|
8765
|
+
let commandCompleted = false;
|
|
8766
|
+
const commandId = crypto.randomUUID();
|
|
8767
|
+
const resultPromise = waitForCommandResult(
|
|
8768
|
+
device,
|
|
8769
|
+
commandId,
|
|
8770
|
+
REMOTE_CLIPBOARD_LIMITS.commandTimeoutMs,
|
|
8771
|
+
{ channel: 'file', operationId });
|
|
8772
|
+
const commandPayload = {
|
|
8773
|
+
type: 'command',
|
|
8774
|
+
commandId,
|
|
8775
|
+
command: action,
|
|
8776
|
+
payload: {
|
|
8777
|
+
operationId,
|
|
8778
|
+
controlSessionId: operation.sessionId,
|
|
8779
|
+
controlCommandId: operation.controlCommandId,
|
|
8780
|
+
captureGeneration: operation.captureGeneration,
|
|
8781
|
+
monitorIndex: operation.monitorIndex,
|
|
8782
|
+
hubConnectionId: operation.hubConnectionId,
|
|
8783
|
+
...(request.payload || {})
|
|
8784
|
+
},
|
|
8785
|
+
issuedAt: new Date().toISOString()
|
|
8786
|
+
};
|
|
8787
|
+
|
|
8788
|
+
try {
|
|
8789
|
+
try {
|
|
8790
|
+
// Clipboard payloads are never allowed onto the concurrent
|
|
8791
|
+
// main command socket. The dedicated file side channel owns
|
|
8792
|
+
// the one bounded command and its exact-session waiter.
|
|
8793
|
+
writeJsonLine(fileSocket, commandPayload);
|
|
8794
|
+
} catch {
|
|
8795
|
+
failPendingCommandResultWaiter(device, commandId, 'clipboard-file-channel-write-failed');
|
|
8796
|
+
if (device.fileSocket === fileSocket) {
|
|
8797
|
+
closeFileSocket(device, 'clipboard-file-channel-write-failed');
|
|
8798
|
+
}
|
|
8799
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
8800
|
+
}
|
|
8801
|
+
device.counters.commandsSent += 1;
|
|
8802
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8803
|
+
commandId,
|
|
8804
|
+
command: action,
|
|
8805
|
+
channel: 'file',
|
|
8806
|
+
operationId
|
|
8807
|
+
});
|
|
8808
|
+
|
|
8809
|
+
const awaited = await resultPromise;
|
|
8810
|
+
if (!awaited.ok) {
|
|
8811
|
+
return {
|
|
8812
|
+
ok: false,
|
|
8813
|
+
error: awaited.error || 'clipboard-command-failed',
|
|
8814
|
+
commandId
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
if (device.fileSocket !== fileSocket) {
|
|
8818
|
+
return { ok: false, error: 'clipboard-file-channel-owner-changed' };
|
|
8819
|
+
}
|
|
8820
|
+
const currentBinding = validateCurrentClipboardBinding(
|
|
8821
|
+
deviceId,
|
|
8822
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8823
|
+
operation.contentKind || 'text',
|
|
8824
|
+
{ requireFileSocket: true });
|
|
8825
|
+
if (!currentBinding.ok) {
|
|
8826
|
+
return currentBinding;
|
|
8827
|
+
}
|
|
8828
|
+
|
|
8829
|
+
let response;
|
|
8830
|
+
try {
|
|
8831
|
+
response = normalizeRemoteClipboardCommandResult(action, operationId, awaited.result);
|
|
8832
|
+
} catch (error) {
|
|
8833
|
+
return {
|
|
8834
|
+
ok: false,
|
|
8835
|
+
error: safeString(error?.code || error?.message, 160) || 'clipboard-response-invalid'
|
|
8836
|
+
};
|
|
8837
|
+
}
|
|
8838
|
+
if (response.manifest) {
|
|
8839
|
+
const responseManifestLimitError = clipboardManifestCapabilityError(device, response.manifest);
|
|
8840
|
+
if (responseManifestLimitError) {
|
|
8841
|
+
clipboardOperations.delete(operationKey);
|
|
8842
|
+
return { ok: false, error: responseManifestLimitError };
|
|
8843
|
+
}
|
|
8844
|
+
const responsePolicy = validateCurrentClipboardBinding(
|
|
8845
|
+
deviceId,
|
|
8846
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8847
|
+
response.manifest.contentKind,
|
|
8848
|
+
{ requireFileSocket: true });
|
|
8849
|
+
if (!responsePolicy.ok) {
|
|
8850
|
+
clipboardOperations.delete(operationKey);
|
|
8851
|
+
return responsePolicy;
|
|
8852
|
+
}
|
|
8853
|
+
operation.manifest = response.manifest;
|
|
8854
|
+
operation.contentKind = response.manifest.contentKind;
|
|
8855
|
+
}
|
|
8856
|
+
if (action === 'clipboard.copy.chunk') {
|
|
8857
|
+
const item = operation.manifest?.items?.[response.itemIndex];
|
|
8858
|
+
const nextOffset = response.offset + response.bytes;
|
|
8859
|
+
if (!item
|
|
8860
|
+
|| response.itemIndex !== itemIndex
|
|
8861
|
+
|| response.offset !== expectedOffset
|
|
8862
|
+
|| (response.bytes === 0
|
|
8863
|
+
&& !(item.size === 0 && response.offset === 0 && response.final === true))
|
|
8864
|
+
|| nextOffset > item.size
|
|
8865
|
+
|| response.final !== (nextOffset === item.size)) {
|
|
8866
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8867
|
+
}
|
|
8868
|
+
if (response.bytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8869
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8870
|
+
}
|
|
8871
|
+
operation.offsets.set(response.itemIndex, nextOffset);
|
|
8872
|
+
} else if (action === 'clipboard.paste.chunk') {
|
|
8873
|
+
if (response.itemIndex !== itemIndex
|
|
8874
|
+
|| response.offset !== expectedOffset
|
|
8875
|
+
|| response.bytes !== outgoingChunkBytes
|
|
8876
|
+
|| response.totalBytes !== operation.manifest.items[itemIndex].size
|
|
8877
|
+
|| response.final !== request.payload.final) {
|
|
8878
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8879
|
+
}
|
|
8880
|
+
operation.offsets.set(itemIndex, expectedOffset + outgoingChunkBytes);
|
|
8881
|
+
}
|
|
8882
|
+
operation.status = response.status || operation.status;
|
|
8883
|
+
operation.touchedAt = Date.now();
|
|
8884
|
+
if (action === 'clipboard.operation.status' && operation.status === 'not-found') {
|
|
8885
|
+
clipboardOperations.delete(operationKey);
|
|
8886
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8887
|
+
}
|
|
8888
|
+
if (action === 'clipboard.operation.status'
|
|
8889
|
+
&& ['completed', 'complete', 'applied', 'pasted', 'cancelled', 'cleaned', 'failed', 'expired', 'already-ended']
|
|
8890
|
+
.includes(operation.status)) {
|
|
8891
|
+
clipboardOperations.delete(operationKey);
|
|
8892
|
+
}
|
|
8893
|
+
commandCompleted = true;
|
|
8894
|
+
return response;
|
|
8895
|
+
} finally {
|
|
8896
|
+
const currentOperation = clipboardOperations.get(operationKey);
|
|
8897
|
+
if (currentOperation === operation) {
|
|
8898
|
+
operation.inFlight = false;
|
|
8899
|
+
if (endsOperation || (beginsOperation && !commandCompleted)) {
|
|
8900
|
+
clipboardOperations.delete(operationKey);
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8905
8907
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8906
8908
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8907
8909
|
: null;
|
|
8908
8910
|
let updated = 0;
|
|
8909
8911
|
let total = 0;
|
|
8910
|
-
for (const device of devices.values()) {
|
|
8911
|
-
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8912
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8913
|
-
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8914
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8915
|
-
continue;
|
|
8916
|
-
}
|
|
8917
|
-
total += 1;
|
|
8912
|
+
for (const device of devices.values()) {
|
|
8913
|
+
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8914
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8915
|
+
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8916
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8917
|
+
continue;
|
|
8918
|
+
}
|
|
8919
|
+
total += 1;
|
|
8918
8920
|
const effectivePolicy = getDevicePolicy(device);
|
|
8919
8921
|
if (writeJsonLine(device.socket, {
|
|
8920
8922
|
type: 'policy.update',
|
|
@@ -8933,14 +8935,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8933
8935
|
const device = devices.get(String(deviceId || ''));
|
|
8934
8936
|
const denied = policyError(device);
|
|
8935
8937
|
if (denied) return { ok: false, error: denied };
|
|
8936
|
-
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8937
|
-
if (device.synthetic !== true
|
|
8938
|
-
&& device.connected
|
|
8939
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
8940
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8941
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8942
|
-
}
|
|
8943
|
-
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
8938
|
+
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8939
|
+
if (device.synthetic !== true
|
|
8940
|
+
&& device.connected
|
|
8941
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
8942
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8943
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8944
|
+
}
|
|
8945
|
+
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
8944
8946
|
return { ok: false, error: 'device-not-connected' };
|
|
8945
8947
|
}
|
|
8946
8948
|
|
|
@@ -9026,40 +9028,65 @@ export function createRemoteHub(options = {}) {
|
|
|
9026
9028
|
};
|
|
9027
9029
|
}
|
|
9028
9030
|
|
|
9029
|
-
function getCurrentInputWriteOwner(device) {
|
|
9031
|
+
function getCurrentInputWriteOwner(device) {
|
|
9030
9032
|
if (!device?.inputSocket
|
|
9031
9033
|
|| device.inputSocket.destroyed
|
|
9032
9034
|
|| !device.inputWriteOwner
|
|
9033
9035
|
|| !safeString(device.inputSocketConnectionId, 128)) {
|
|
9034
9036
|
return null;
|
|
9035
9037
|
}
|
|
9036
|
-
return device.inputWriteOwner;
|
|
9037
|
-
}
|
|
9038
|
-
|
|
9039
|
-
function
|
|
9038
|
+
return device.inputWriteOwner;
|
|
9039
|
+
}
|
|
9040
|
+
|
|
9041
|
+
function getInputRouteState(deviceId) {
|
|
9040
9042
|
const device = devices.get(String(deviceId || ''));
|
|
9041
|
-
if (!device) {
|
|
9042
|
-
return {
|
|
9043
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
9044
|
+
return {
|
|
9045
|
+
deviceId: safeString(deviceId, 160),
|
|
9046
|
+
ready: false,
|
|
9047
|
+
dedicatedInputChannel: false,
|
|
9048
|
+
reason: 'device-not-connected'
|
|
9049
|
+
};
|
|
9043
9050
|
}
|
|
9044
9051
|
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9052
|
+
const dedicatedInputChannel = readCapabilityFlag(
|
|
9053
|
+
device.capabilities,
|
|
9054
|
+
'dedicatedInputChannel');
|
|
9055
|
+
const ready = dedicatedInputChannel
|
|
9056
|
+
? !!getCurrentInputWriteOwner(device)
|
|
9057
|
+
: true;
|
|
9058
|
+
return {
|
|
9059
|
+
deviceId: device.deviceId,
|
|
9060
|
+
ready,
|
|
9061
|
+
dedicatedInputChannel,
|
|
9062
|
+
reason: ready ? 'ready' : 'input-channel-not-connected'
|
|
9063
|
+
};
|
|
9064
|
+
}
|
|
9065
|
+
|
|
9066
|
+
function sendInputControl(deviceId, input = {}) {
|
|
9067
|
+
const device = devices.get(String(deviceId || ''));
|
|
9068
|
+
if (!device) {
|
|
9069
|
+
return { ok: false, error: 'device-not-found' };
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
if (device.synthetic !== true
|
|
9073
|
+
&& device.connected
|
|
9074
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
9075
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9076
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9077
|
+
}
|
|
9078
|
+
if (!device.connected) {
|
|
9079
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9080
|
+
}
|
|
9054
9081
|
|
|
9055
9082
|
const normalized = normalizeRemoteInputEvent(input);
|
|
9056
9083
|
if (!normalized.type) {
|
|
9057
9084
|
return { ok: false, error: 'missing-input-type' };
|
|
9058
9085
|
}
|
|
9059
9086
|
|
|
9060
|
-
// A reset
|
|
9061
|
-
// owner must be able to send it even when its
|
|
9062
|
-
// has just become stale during a monitor/capture-generation switch.
|
|
9087
|
+
// A reset releases native mouse-button and keyboard state. The
|
|
9088
|
+
// current browser input owner must be able to send it even when its
|
|
9089
|
+
// former Control binding has just become stale during a monitor/capture-generation switch.
|
|
9063
9090
|
const currentInputOwner = safeString(device.inputOwnerConnectionId, 128);
|
|
9064
9091
|
const isCurrentOwnerKeyboardReset = normalized.type.toLowerCase() === 'keyboard.reset'
|
|
9065
9092
|
&& normalized.hubConnectionId
|
|
@@ -9116,51 +9143,51 @@ export function createRemoteHub(options = {}) {
|
|
|
9116
9143
|
|| device.capabilities?.accessibilityPermission === false) {
|
|
9117
9144
|
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
9118
9145
|
}
|
|
9119
|
-
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
9120
|
-
return { ok: false, error: 'device-input-control-unavailable' };
|
|
9121
|
-
}
|
|
9122
|
-
|
|
9123
|
-
const normalizedInputType = normalized.type.toLowerCase();
|
|
9124
|
-
const isClipboardInput = normalizedInputType === 'clipboard.prepare'
|
|
9125
|
-
|| normalizedInputType === 'clipboard.copy'
|
|
9126
|
-
|| normalizedInputType === 'clipboard.paste';
|
|
9127
|
-
let clipboardManifest = null;
|
|
9128
|
-
if (isClipboardInput) {
|
|
9129
|
-
if (!normalized.clipboardOperationId) {
|
|
9130
|
-
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
9131
|
-
}
|
|
9132
|
-
if (!['copy', 'paste'].includes(normalized.clipboardDirection)) {
|
|
9133
|
-
return { ok: false, error: 'clipboard-direction-required' };
|
|
9134
|
-
}
|
|
9135
|
-
if ((normalizedInputType === 'clipboard.copy' && normalized.clipboardDirection !== 'copy')
|
|
9136
|
-
|| (normalizedInputType === 'clipboard.paste' && normalized.clipboardDirection !== 'paste')) {
|
|
9137
|
-
return { ok: false, error: 'clipboard-direction-mismatch' };
|
|
9138
|
-
}
|
|
9139
|
-
try {
|
|
9140
|
-
clipboardManifest = normalized.manifest
|
|
9141
|
-
? normalizeRemoteClipboardManifest(normalized.manifest, normalized.clipboardOperationId)
|
|
9142
|
-
: null;
|
|
9143
|
-
} catch (error) {
|
|
9144
|
-
return { ok: false, error: safeString(error?.code || error?.message, 160) || 'clipboard-manifest-invalid' };
|
|
9145
|
-
}
|
|
9146
|
-
const clipboardPolicy = getDevicePolicy(device);
|
|
9147
|
-
if (clipboardPolicy.allowClipboardText !== true) {
|
|
9148
|
-
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
9149
|
-
}
|
|
9150
|
-
const contentKind = clipboardManifest?.contentKind || normalized.contentKind || 'text';
|
|
9151
|
-
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
9152
|
-
&& clipboardPolicy.allowFileTransfer !== true) {
|
|
9153
|
-
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
9154
|
-
}
|
|
9155
|
-
const clipboardCapabilityDenied = clipboardCapabilityError(
|
|
9156
|
-
device,
|
|
9157
|
-
normalized.clipboardDirection,
|
|
9158
|
-
contentKind);
|
|
9159
|
-
if (clipboardCapabilityDenied) {
|
|
9160
|
-
return { ok: false, error: clipboardCapabilityDenied };
|
|
9161
|
-
}
|
|
9162
|
-
normalized.manifest = clipboardManifest;
|
|
9163
|
-
}
|
|
9146
|
+
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
9147
|
+
return { ok: false, error: 'device-input-control-unavailable' };
|
|
9148
|
+
}
|
|
9149
|
+
|
|
9150
|
+
const normalizedInputType = normalized.type.toLowerCase();
|
|
9151
|
+
const isClipboardInput = normalizedInputType === 'clipboard.prepare'
|
|
9152
|
+
|| normalizedInputType === 'clipboard.copy'
|
|
9153
|
+
|| normalizedInputType === 'clipboard.paste';
|
|
9154
|
+
let clipboardManifest = null;
|
|
9155
|
+
if (isClipboardInput) {
|
|
9156
|
+
if (!normalized.clipboardOperationId) {
|
|
9157
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
9158
|
+
}
|
|
9159
|
+
if (!['copy', 'paste'].includes(normalized.clipboardDirection)) {
|
|
9160
|
+
return { ok: false, error: 'clipboard-direction-required' };
|
|
9161
|
+
}
|
|
9162
|
+
if ((normalizedInputType === 'clipboard.copy' && normalized.clipboardDirection !== 'copy')
|
|
9163
|
+
|| (normalizedInputType === 'clipboard.paste' && normalized.clipboardDirection !== 'paste')) {
|
|
9164
|
+
return { ok: false, error: 'clipboard-direction-mismatch' };
|
|
9165
|
+
}
|
|
9166
|
+
try {
|
|
9167
|
+
clipboardManifest = normalized.manifest
|
|
9168
|
+
? normalizeRemoteClipboardManifest(normalized.manifest, normalized.clipboardOperationId)
|
|
9169
|
+
: null;
|
|
9170
|
+
} catch (error) {
|
|
9171
|
+
return { ok: false, error: safeString(error?.code || error?.message, 160) || 'clipboard-manifest-invalid' };
|
|
9172
|
+
}
|
|
9173
|
+
const clipboardPolicy = getDevicePolicy(device);
|
|
9174
|
+
if (clipboardPolicy.allowClipboardText !== true) {
|
|
9175
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
9176
|
+
}
|
|
9177
|
+
const contentKind = clipboardManifest?.contentKind || normalized.contentKind || 'text';
|
|
9178
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
9179
|
+
&& clipboardPolicy.allowFileTransfer !== true) {
|
|
9180
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
9181
|
+
}
|
|
9182
|
+
const clipboardCapabilityDenied = clipboardCapabilityError(
|
|
9183
|
+
device,
|
|
9184
|
+
normalized.clipboardDirection,
|
|
9185
|
+
contentKind);
|
|
9186
|
+
if (clipboardCapabilityDenied) {
|
|
9187
|
+
return { ok: false, error: clipboardCapabilityDenied };
|
|
9188
|
+
}
|
|
9189
|
+
normalized.manifest = clipboardManifest;
|
|
9190
|
+
}
|
|
9164
9191
|
|
|
9165
9192
|
const controlStream = getActiveControlStream(device);
|
|
9166
9193
|
if (!controlStream
|
|
@@ -9196,32 +9223,32 @@ export function createRemoteHub(options = {}) {
|
|
|
9196
9223
|
};
|
|
9197
9224
|
}
|
|
9198
9225
|
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
9199
|
-
if (!Number.isInteger(normalized.monitorIndex)
|
|
9200
|
-
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9226
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
9227
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9201
9228
|
return {
|
|
9202
9229
|
ok: false,
|
|
9203
9230
|
error: 'STALE_CONTROL_MONITOR',
|
|
9204
|
-
activeMonitorIndex
|
|
9205
|
-
};
|
|
9206
|
-
}
|
|
9207
|
-
|
|
9208
|
-
let clipboardOperationKey = '';
|
|
9209
|
-
if (normalizedInputType === 'clipboard.paste') {
|
|
9210
|
-
clipboardOperationKey = buildClipboardOperationKey(
|
|
9211
|
-
device.deviceId,
|
|
9212
|
-
device.sessionId,
|
|
9213
|
-
normalized.clipboardOperationId);
|
|
9214
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9215
|
-
if (!operation
|
|
9216
|
-
|| operation.direction !== 'paste'
|
|
9217
|
-
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9218
|
-
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9219
|
-
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9220
|
-
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9221
|
-
|| operation.status !== 'ready') {
|
|
9222
|
-
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9223
|
-
}
|
|
9224
|
-
}
|
|
9231
|
+
activeMonitorIndex
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
|
|
9235
|
+
let clipboardOperationKey = '';
|
|
9236
|
+
if (normalizedInputType === 'clipboard.paste') {
|
|
9237
|
+
clipboardOperationKey = buildClipboardOperationKey(
|
|
9238
|
+
device.deviceId,
|
|
9239
|
+
device.sessionId,
|
|
9240
|
+
normalized.clipboardOperationId);
|
|
9241
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9242
|
+
if (!operation
|
|
9243
|
+
|| operation.direction !== 'paste'
|
|
9244
|
+
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9245
|
+
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9246
|
+
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9247
|
+
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9248
|
+
|| operation.status !== 'ready') {
|
|
9249
|
+
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9225
9252
|
|
|
9226
9253
|
const inputSocket = device.inputSocket;
|
|
9227
9254
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9238,17 +9265,17 @@ export function createRemoteHub(options = {}) {
|
|
|
9238
9265
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
9239
9266
|
const bindingChanged = previousInputBindingKey
|
|
9240
9267
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
9241
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9242
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9243
|
-
if (ownerChanged) {
|
|
9244
|
-
clearClipboardOperationsForDevice(
|
|
9245
|
-
device,
|
|
9246
|
-
'browser-input-owner-changed',
|
|
9247
|
-
previousOwnerConnectionId);
|
|
9248
|
-
} else if (bindingChanged || writeBindingChanged) {
|
|
9249
|
-
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9250
|
-
}
|
|
9251
|
-
const resetSent = inputWriteOwner.replaceBinding(
|
|
9268
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9269
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9270
|
+
if (ownerChanged) {
|
|
9271
|
+
clearClipboardOperationsForDevice(
|
|
9272
|
+
device,
|
|
9273
|
+
'browser-input-owner-changed',
|
|
9274
|
+
previousOwnerConnectionId);
|
|
9275
|
+
} else if (bindingChanged || writeBindingChanged) {
|
|
9276
|
+
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9277
|
+
}
|
|
9278
|
+
const resetSent = inputWriteOwner.replaceBinding(
|
|
9252
9279
|
activeInputBindingKey,
|
|
9253
9280
|
buildRemoteInputResetMessage(
|
|
9254
9281
|
device,
|
|
@@ -9278,16 +9305,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9278
9305
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
9279
9306
|
}
|
|
9280
9307
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
9281
|
-
device.counters.commandsSent += 1;
|
|
9282
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
9283
|
-
if (clipboardOperationKey) {
|
|
9284
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9285
|
-
if (operation) {
|
|
9286
|
-
operation.status = 'pasting';
|
|
9287
|
-
operation.touchedAt = Date.now();
|
|
9288
|
-
}
|
|
9289
|
-
}
|
|
9290
|
-
return {
|
|
9308
|
+
device.counters.commandsSent += 1;
|
|
9309
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
9310
|
+
if (clipboardOperationKey) {
|
|
9311
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9312
|
+
if (operation) {
|
|
9313
|
+
operation.status = 'pasting';
|
|
9314
|
+
operation.touchedAt = Date.now();
|
|
9315
|
+
}
|
|
9316
|
+
}
|
|
9317
|
+
return {
|
|
9291
9318
|
ok: true,
|
|
9292
9319
|
inputSocket: true,
|
|
9293
9320
|
sessionId: device.sessionId,
|
|
@@ -9322,7 +9349,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9322
9349
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
9323
9350
|
}
|
|
9324
9351
|
});
|
|
9325
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9352
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9326
9353
|
schedulePendingInputFallback(device, {
|
|
9327
9354
|
commandId: fallback.commandId,
|
|
9328
9355
|
inputSeq: normalized.inputSeq,
|
|
@@ -9334,16 +9361,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9334
9361
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
9335
9362
|
hubForwardedAtEpochMs,
|
|
9336
9363
|
fallback: true,
|
|
9337
|
-
timeoutTimer: null
|
|
9338
|
-
});
|
|
9339
|
-
}
|
|
9340
|
-
if (fallback.ok && clipboardOperationKey) {
|
|
9341
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9342
|
-
if (operation) {
|
|
9343
|
-
operation.status = 'pasting';
|
|
9344
|
-
operation.touchedAt = Date.now();
|
|
9345
|
-
}
|
|
9346
|
-
}
|
|
9364
|
+
timeoutTimer: null
|
|
9365
|
+
});
|
|
9366
|
+
}
|
|
9367
|
+
if (fallback.ok && clipboardOperationKey) {
|
|
9368
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9369
|
+
if (operation) {
|
|
9370
|
+
operation.status = 'pasting';
|
|
9371
|
+
operation.touchedAt = Date.now();
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9347
9374
|
return fallback.ok
|
|
9348
9375
|
? {
|
|
9349
9376
|
...fallback,
|
|
@@ -9363,15 +9390,15 @@ export function createRemoteHub(options = {}) {
|
|
|
9363
9390
|
};
|
|
9364
9391
|
}
|
|
9365
9392
|
|
|
9366
|
-
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9393
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9367
9394
|
const device = devices.get(String(deviceId || ''));
|
|
9368
9395
|
const owner = safeString(ownerConnectionId, 128);
|
|
9369
|
-
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9370
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
9371
|
-
}
|
|
9372
|
-
|
|
9373
|
-
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9374
|
-
device.inputOwnerConnectionId = '';
|
|
9396
|
+
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9397
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
9398
|
+
}
|
|
9399
|
+
|
|
9400
|
+
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9401
|
+
device.inputOwnerConnectionId = '';
|
|
9375
9402
|
device.inputOwnerBindingKey = '';
|
|
9376
9403
|
const inputSocket = device.inputSocket;
|
|
9377
9404
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9415,12 +9442,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9415
9442
|
};
|
|
9416
9443
|
const results = targets.map(deviceId => {
|
|
9417
9444
|
const device = devices.get(deviceId);
|
|
9418
|
-
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9419
|
-
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9420
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9421
|
-
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9422
|
-
}
|
|
9423
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
9445
|
+
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9446
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9447
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9448
|
+
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9449
|
+
}
|
|
9450
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
9424
9451
|
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9425
9452
|
}
|
|
9426
9453
|
return writeJsonLine(device.socket, payload)
|
|
@@ -9452,7 +9479,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9452
9479
|
const denied = policyError(device, 'allowAgent');
|
|
9453
9480
|
if (denied) return { ok: false, error: denied };
|
|
9454
9481
|
|
|
9455
|
-
if (device?.synthetic === true && device.connected) {
|
|
9482
|
+
if (device?.synthetic === true && device.connected) {
|
|
9456
9483
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
9457
9484
|
if (!instruction) {
|
|
9458
9485
|
return { ok: false, error: 'missing-instruction' };
|
|
@@ -9512,14 +9539,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9512
9539
|
error: '',
|
|
9513
9540
|
synthetic: true
|
|
9514
9541
|
});
|
|
9515
|
-
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
9516
|
-
}
|
|
9517
|
-
|
|
9518
|
-
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
9519
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9520
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9521
|
-
}
|
|
9522
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
9542
|
+
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
9543
|
+
}
|
|
9544
|
+
|
|
9545
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
9546
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9547
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9548
|
+
}
|
|
9549
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
9523
9550
|
return { ok: false, error: 'device-not-connected' };
|
|
9524
9551
|
}
|
|
9525
9552
|
|
|
@@ -10380,17 +10407,17 @@ export function createRemoteHub(options = {}) {
|
|
|
10380
10407
|
};
|
|
10381
10408
|
}
|
|
10382
10409
|
|
|
10383
|
-
function startLiveStream(deviceId, options = {}) {
|
|
10384
|
-
const device = devices.get(String(deviceId || ''));
|
|
10385
|
-
const denied = policyError(device);
|
|
10386
|
-
if (denied) return { ok: false, error: denied };
|
|
10387
|
-
if (device?.synthetic !== true
|
|
10388
|
-
&& device?.connected
|
|
10389
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
10390
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10391
|
-
return { ok: false, error: 'device-not-connected' };
|
|
10392
|
-
}
|
|
10393
|
-
if (device?.liveCapturePause?.token) {
|
|
10410
|
+
function startLiveStream(deviceId, options = {}) {
|
|
10411
|
+
const device = devices.get(String(deviceId || ''));
|
|
10412
|
+
const denied = policyError(device);
|
|
10413
|
+
if (denied) return { ok: false, error: denied };
|
|
10414
|
+
if (device?.synthetic !== true
|
|
10415
|
+
&& device?.connected
|
|
10416
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10417
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10418
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10419
|
+
}
|
|
10420
|
+
if (device?.liveCapturePause?.token) {
|
|
10394
10421
|
return {
|
|
10395
10422
|
ok: false,
|
|
10396
10423
|
error: 'LIVE_CAPTURE_PAUSED',
|
|
@@ -10821,15 +10848,15 @@ export function createRemoteHub(options = {}) {
|
|
|
10821
10848
|
};
|
|
10822
10849
|
}
|
|
10823
10850
|
|
|
10824
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
10825
|
-
const device = devices.get(String(deviceId || ''));
|
|
10826
|
-
if (device?.synthetic !== true
|
|
10827
|
-
&& device?.connected
|
|
10828
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
10829
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10830
|
-
return { ok: false, error: 'device-not-connected' };
|
|
10831
|
-
}
|
|
10832
|
-
const stopAll = options.stopAll === true;
|
|
10851
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
10852
|
+
const device = devices.get(String(deviceId || ''));
|
|
10853
|
+
if (device?.synthetic !== true
|
|
10854
|
+
&& device?.connected
|
|
10855
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10856
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10857
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10858
|
+
}
|
|
10859
|
+
const stopAll = options.stopAll === true;
|
|
10833
10860
|
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24).toLowerCase();
|
|
10834
10861
|
const requestedStreamId = safeString(options.streamId, 128);
|
|
10835
10862
|
const purposeStreams = !stopAll && !requestedStreamId && streamPurpose && device
|
|
@@ -11430,18 +11457,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11430
11457
|
};
|
|
11431
11458
|
}
|
|
11432
11459
|
|
|
11433
|
-
function startAudioStream(deviceId, options = {}) {
|
|
11434
|
-
const device = devices.get(String(deviceId || ''));
|
|
11435
|
-
if (!device) {
|
|
11436
|
-
return { ok: false, error: 'device-not-found' };
|
|
11437
|
-
}
|
|
11438
|
-
if (device.synthetic !== true
|
|
11439
|
-
&& device.connected
|
|
11440
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
11441
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11442
|
-
return { ok: false, error: 'device-not-connected' };
|
|
11443
|
-
}
|
|
11444
|
-
if (!device.connected) {
|
|
11460
|
+
function startAudioStream(deviceId, options = {}) {
|
|
11461
|
+
const device = devices.get(String(deviceId || ''));
|
|
11462
|
+
if (!device) {
|
|
11463
|
+
return { ok: false, error: 'device-not-found' };
|
|
11464
|
+
}
|
|
11465
|
+
if (device.synthetic !== true
|
|
11466
|
+
&& device.connected
|
|
11467
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11468
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11469
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11470
|
+
}
|
|
11471
|
+
if (!device.connected) {
|
|
11445
11472
|
return { ok: false, error: 'device-not-connected' };
|
|
11446
11473
|
}
|
|
11447
11474
|
const denied = policyError(device, 'allowRemoteAudio');
|
|
@@ -11518,18 +11545,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11518
11545
|
return { ok: true, commandId, streamId };
|
|
11519
11546
|
}
|
|
11520
11547
|
|
|
11521
|
-
function stopAudioStream(deviceId, options = {}) {
|
|
11522
|
-
const device = devices.get(String(deviceId || ''));
|
|
11523
|
-
if (!device) {
|
|
11524
|
-
return { ok: false, error: 'device-not-found' };
|
|
11525
|
-
}
|
|
11526
|
-
if (device.synthetic !== true
|
|
11527
|
-
&& device.connected
|
|
11528
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
11529
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11530
|
-
return { ok: false, error: 'device-not-connected' };
|
|
11531
|
-
}
|
|
11532
|
-
if (!device.connected) {
|
|
11548
|
+
function stopAudioStream(deviceId, options = {}) {
|
|
11549
|
+
const device = devices.get(String(deviceId || ''));
|
|
11550
|
+
if (!device) {
|
|
11551
|
+
return { ok: false, error: 'device-not-found' };
|
|
11552
|
+
}
|
|
11553
|
+
if (device.synthetic !== true
|
|
11554
|
+
&& device.connected
|
|
11555
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11556
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11557
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11558
|
+
}
|
|
11559
|
+
if (!device.connected) {
|
|
11533
11560
|
return { ok: false, error: 'device-not-connected' };
|
|
11534
11561
|
}
|
|
11535
11562
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
@@ -11742,14 +11769,14 @@ export function createRemoteHub(options = {}) {
|
|
|
11742
11769
|
replacementTimerCount: device.liveStreamReplacementTimers instanceof Map
|
|
11743
11770
|
? device.liveStreamReplacementTimers.size
|
|
11744
11771
|
: 0,
|
|
11745
|
-
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11746
|
-
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11747
|
-
&& waiter.sessionId === device.sessionId)
|
|
11748
|
-
.length,
|
|
11749
|
-
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11750
|
-
.filter(operation => operation.deviceId === device.deviceId
|
|
11751
|
-
&& operation.sessionId === device.sessionId)
|
|
11752
|
-
.length,
|
|
11772
|
+
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11773
|
+
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11774
|
+
&& waiter.sessionId === device.sessionId)
|
|
11775
|
+
.length,
|
|
11776
|
+
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11777
|
+
.filter(operation => operation.deviceId === device.deviceId
|
|
11778
|
+
&& operation.sessionId === device.sessionId)
|
|
11779
|
+
.length,
|
|
11753
11780
|
retainedPayloadCount: retainedPayloads.size,
|
|
11754
11781
|
retainedPayloadBytes: [...retainedPayloads].reduce(
|
|
11755
11782
|
(total, payload) => total + payload.length,
|
|
@@ -11759,9 +11786,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11759
11786
|
&& snapshot.stopPendingCount === 0
|
|
11760
11787
|
&& snapshot.pendingStartCount === 0
|
|
11761
11788
|
&& snapshot.pendingStopPromiseCount === 0
|
|
11762
|
-
&& snapshot.replacementTimerCount === 0
|
|
11763
|
-
&& snapshot.commandResultWaiterCount === 0
|
|
11764
|
-
&& snapshot.clipboardOperationCount === 0
|
|
11789
|
+
&& snapshot.replacementTimerCount === 0
|
|
11790
|
+
&& snapshot.commandResultWaiterCount === 0
|
|
11791
|
+
&& snapshot.clipboardOperationCount === 0
|
|
11765
11792
|
&& snapshot.retainedPayloadCount === 0
|
|
11766
11793
|
&& snapshot.retainedPayloadBytes === 0;
|
|
11767
11794
|
return snapshot;
|
|
@@ -11773,9 +11800,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11773
11800
|
'stopPendingCount',
|
|
11774
11801
|
'pendingStartCount',
|
|
11775
11802
|
'pendingStopPromiseCount',
|
|
11776
|
-
'replacementTimerCount',
|
|
11777
|
-
'commandResultWaiterCount',
|
|
11778
|
-
'clipboardOperationCount',
|
|
11803
|
+
'replacementTimerCount',
|
|
11804
|
+
'commandResultWaiterCount',
|
|
11805
|
+
'clipboardOperationCount',
|
|
11779
11806
|
'retainedPayloadCount',
|
|
11780
11807
|
'retainedPayloadBytes'
|
|
11781
11808
|
]) {
|
|
@@ -11788,10 +11815,10 @@ export function createRemoteHub(options = {}) {
|
|
|
11788
11815
|
stopPendingCount: 0,
|
|
11789
11816
|
pendingStartCount: 0,
|
|
11790
11817
|
pendingStopPromiseCount: 0,
|
|
11791
|
-
replacementTimerCount: 0,
|
|
11792
|
-
commandResultWaiterCount: 0,
|
|
11793
|
-
clipboardOperationCount: 0,
|
|
11794
|
-
retainedPayloadCount: 0,
|
|
11818
|
+
replacementTimerCount: 0,
|
|
11819
|
+
commandResultWaiterCount: 0,
|
|
11820
|
+
clipboardOperationCount: 0,
|
|
11821
|
+
retainedPayloadCount: 0,
|
|
11795
11822
|
retainedPayloadBytes: 0
|
|
11796
11823
|
});
|
|
11797
11824
|
return {
|
|
@@ -11831,12 +11858,13 @@ export function createRemoteHub(options = {}) {
|
|
|
11831
11858
|
listDeviceFrames,
|
|
11832
11859
|
disconnectDevice,
|
|
11833
11860
|
assignDeviceSlot,
|
|
11834
|
-
sendCommand,
|
|
11835
|
-
sendClipboardCommand,
|
|
11836
|
-
refreshDevicePolicies,
|
|
11861
|
+
sendCommand,
|
|
11862
|
+
sendClipboardCommand,
|
|
11863
|
+
refreshDevicePolicies,
|
|
11837
11864
|
sendLegacyClientUpdate,
|
|
11838
|
-
sendInputControl,
|
|
11839
|
-
|
|
11865
|
+
sendInputControl,
|
|
11866
|
+
getInputRouteState,
|
|
11867
|
+
releaseInputOwner,
|
|
11840
11868
|
notifyAgentProgress,
|
|
11841
11869
|
requestAgentTask,
|
|
11842
11870
|
requestAgentTaskBatch,
|