@livedesk/hub 0.1.48 → 0.1.50
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 +30 -4
- 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 +1022 -1020
- package/src/server.js +143 -141
- 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) {
|
|
@@ -1246,6 +1246,8 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1246
1246
|
'file.write',
|
|
1247
1247
|
'file.delete',
|
|
1248
1248
|
'file.list',
|
|
1249
|
+
'file.search',
|
|
1250
|
+
'directory.create',
|
|
1249
1251
|
'command.run',
|
|
1250
1252
|
'script.run',
|
|
1251
1253
|
'software.install',
|
|
@@ -1278,7 +1280,7 @@ function readRawRemoteInputMonitorIndex(input) {
|
|
|
1278
1280
|
return undefined;
|
|
1279
1281
|
}
|
|
1280
1282
|
|
|
1281
|
-
function normalizeRemoteInputEvent(value = {}) {
|
|
1283
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
1282
1284
|
const input = value && typeof value === 'object' ? value : {};
|
|
1283
1285
|
const type = safeString(input.type || input.Type, 48);
|
|
1284
1286
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
@@ -1299,14 +1301,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1299
1301
|
.filter(Boolean))]
|
|
1300
1302
|
.slice(0, 64)
|
|
1301
1303
|
: null;
|
|
1302
|
-
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1303
|
-
const clipboardOperationId = safeString(
|
|
1304
|
-
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1305
|
-
128);
|
|
1306
|
-
const clipboardDirection = safeString(
|
|
1307
|
-
input.clipboardDirection || input.ClipboardDirection,
|
|
1308
|
-
16).toLowerCase();
|
|
1309
|
-
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;
|
|
1310
1312
|
let code = safeString(input.code ?? input.Code, 80);
|
|
1311
1313
|
if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
|
|
1312
1314
|
code = 'Space';
|
|
@@ -1331,13 +1333,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1331
1333
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
1332
1334
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
1333
1335
|
altKey: input.altKey === true || input.AltKey === true,
|
|
1334
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1335
|
-
clipboardOperationId,
|
|
1336
|
-
operationId: clipboardOperationId,
|
|
1337
|
-
clipboardDirection,
|
|
1338
|
-
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1339
|
-
? clipboardManifest
|
|
1340
|
-
: 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,
|
|
1341
1343
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
1342
1344
|
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
1343
1345
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
@@ -2329,29 +2331,29 @@ export function createRemoteHub(options = {}) {
|
|
|
2329
2331
|
|
|
2330
2332
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
2331
2333
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
2332
|
-
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2333
|
-
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2334
|
-
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2335
|
-
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2336
|
-
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2337
|
-
heartbeatMs * 4);
|
|
2338
|
-
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2339
|
-
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2340
|
-
: clampNumber(
|
|
2341
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2342
|
-
defaultDeviceConnectionStaleMs,
|
|
2343
|
-
10 * 60 * 1000,
|
|
2344
|
-
defaultDeviceConnectionStaleMs);
|
|
2345
|
-
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2346
|
-
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2347
|
-
: clampNumber(
|
|
2348
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2349
|
-
1000,
|
|
2350
|
-
60000,
|
|
2351
|
-
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2352
|
-
const taskTimeoutMs = clampNumber(
|
|
2353
|
-
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2354
|
-
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,
|
|
2355
2357
|
30 * 60 * 1000,
|
|
2356
2358
|
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
2357
2359
|
const inputAckTimeoutMs = clampNumber(
|
|
@@ -2414,18 +2416,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2414
2416
|
const audioSockets = new Map();
|
|
2415
2417
|
const fileSockets = new Map();
|
|
2416
2418
|
const allSockets = new Set();
|
|
2417
|
-
const agentBinaryIngressStates = new Set();
|
|
2418
|
-
const pendingCommandResultWaiters = new Map();
|
|
2419
|
-
const clipboardOperations = new Map();
|
|
2419
|
+
const agentBinaryIngressStates = new Set();
|
|
2420
|
+
const pendingCommandResultWaiters = new Map();
|
|
2421
|
+
const clipboardOperations = new Map();
|
|
2420
2422
|
const transportDiagnostics = new Map();
|
|
2421
2423
|
const transportDiagnosticStartingDevices = new Set();
|
|
2422
|
-
const duplicateDeviceLogAt = new Map();
|
|
2423
|
-
let server = null;
|
|
2424
|
-
let started = false;
|
|
2425
|
-
let deviceConnectionSweepTimer = null;
|
|
2426
|
-
let boundPort = requestedPort;
|
|
2427
|
-
let lastError = '';
|
|
2428
|
-
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;
|
|
2429
2431
|
let publicIPv4Cache = {
|
|
2430
2432
|
address: safeString(env.LIVEDESK_REMOTE_PUBLIC_IPV4 || env.LIVEDESK_PUBLIC_IPV4 || env.REMOTE_HUB_PUBLIC_IPV4, 64),
|
|
2431
2433
|
updatedAt: 0,
|
|
@@ -2746,14 +2748,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2746
2748
|
protocol: 'tcp-jsonl',
|
|
2747
2749
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2748
2750
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2749
|
-
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2750
|
-
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2751
|
-
heartbeatMs,
|
|
2752
|
-
deviceConnectionStaleMs,
|
|
2753
|
-
deviceConnectionSweepMs,
|
|
2754
|
-
taskTimeoutMs,
|
|
2755
|
-
managerPackage,
|
|
2756
|
-
managerVersion,
|
|
2751
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2752
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2753
|
+
heartbeatMs,
|
|
2754
|
+
deviceConnectionStaleMs,
|
|
2755
|
+
deviceConnectionSweepMs,
|
|
2756
|
+
taskTimeoutMs,
|
|
2757
|
+
managerPackage,
|
|
2758
|
+
managerVersion,
|
|
2757
2759
|
hostInstanceId,
|
|
2758
2760
|
agentPackage: '@livedesk/client',
|
|
2759
2761
|
agentEndpoint: routeInfo.endpoint,
|
|
@@ -3016,7 +3018,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3016
3018
|
return true;
|
|
3017
3019
|
}
|
|
3018
3020
|
|
|
3019
|
-
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3021
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3020
3022
|
if (!device?.deviceId) {
|
|
3021
3023
|
return;
|
|
3022
3024
|
}
|
|
@@ -3033,31 +3035,31 @@ export function createRemoteHub(options = {}) {
|
|
|
3033
3035
|
result: null,
|
|
3034
3036
|
error: safeString(error, 500) || 'command-result-unavailable'
|
|
3035
3037
|
});
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
|
|
3039
|
-
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3040
|
-
if (!device?.deviceId) {
|
|
3041
|
-
return;
|
|
3042
|
-
}
|
|
3043
|
-
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3044
|
-
if (waiter.deviceId !== device.deviceId
|
|
3045
|
-
|| waiter.sessionId !== device.sessionId
|
|
3046
|
-
|| waiter.channel !== 'file') {
|
|
3047
|
-
continue;
|
|
3048
|
-
}
|
|
3049
|
-
pendingCommandResultWaiters.delete(waiterKey);
|
|
3050
|
-
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3051
|
-
waiter.resolve({
|
|
3052
|
-
ok: false,
|
|
3053
|
-
commandId: waiter.commandId,
|
|
3054
|
-
result: null,
|
|
3055
|
-
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3056
|
-
});
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
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') {
|
|
3061
3063
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3062
3064
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3063
3065
|
device?.deviceId,
|
|
@@ -3078,7 +3080,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3078
3080
|
return true;
|
|
3079
3081
|
}
|
|
3080
3082
|
|
|
3081
|
-
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3083
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3082
3084
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3083
3085
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3084
3086
|
device?.deviceId,
|
|
@@ -3104,13 +3106,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3104
3106
|
error: 'command-result-wait-replaced'
|
|
3105
3107
|
});
|
|
3106
3108
|
}
|
|
3107
|
-
const waiterRecord = {
|
|
3108
|
-
deviceId: device.deviceId,
|
|
3109
|
-
sessionId: device.sessionId,
|
|
3110
|
-
commandId: normalizedCommandId,
|
|
3111
|
-
channel: safeString(metadata.channel, 24),
|
|
3112
|
-
operationId: safeString(metadata.operationId, 128),
|
|
3113
|
-
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,
|
|
3114
3116
|
deadlineImmediate: null,
|
|
3115
3117
|
resolve
|
|
3116
3118
|
};
|
|
@@ -3143,246 +3145,246 @@ export function createRemoteHub(options = {}) {
|
|
|
3143
3145
|
});
|
|
3144
3146
|
}, timeoutMs);
|
|
3145
3147
|
waiterRecord.timer.unref?.();
|
|
3146
|
-
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3147
|
-
});
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3151
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3152
|
-
const normalizedSessionId = safeString(sessionId, 160);
|
|
3153
|
-
const normalizedOperationId = safeString(operationId, 128);
|
|
3154
|
-
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3155
|
-
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3156
|
-
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3157
|
-
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3158
|
-
: '';
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3162
|
-
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3163
|
-
return false;
|
|
3164
|
-
}
|
|
3165
|
-
const cleanupCommand = operation.direction === 'paste'
|
|
3166
|
-
? 'clipboard.paste.cancel'
|
|
3167
|
-
: 'clipboard.copy.end';
|
|
3168
|
-
const commandId = crypto.randomUUID();
|
|
3169
|
-
try {
|
|
3170
|
-
writeJsonLine(device.fileSocket, {
|
|
3171
|
-
type: 'command',
|
|
3172
|
-
commandId,
|
|
3173
|
-
command: cleanupCommand,
|
|
3174
|
-
payload: {
|
|
3175
|
-
operationId: operation.operationId,
|
|
3176
|
-
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3177
|
-
},
|
|
3178
|
-
issuedAt: new Date().toISOString()
|
|
3179
|
-
});
|
|
3180
|
-
device.counters.commandsSent += 1;
|
|
3181
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3182
|
-
commandId,
|
|
3183
|
-
command: cleanupCommand,
|
|
3184
|
-
channel: 'file',
|
|
3185
|
-
operationId: operation.operationId,
|
|
3186
|
-
cleanup: true
|
|
3187
|
-
});
|
|
3188
|
-
return true;
|
|
3189
|
-
} catch {
|
|
3190
|
-
// The exact side channel is already closing. Native session
|
|
3191
|
-
// teardown remains the authoritative cleanup.
|
|
3192
|
-
return false;
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3197
|
-
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3198
|
-
let cleared = 0;
|
|
3199
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3200
|
-
if (operation.deviceId !== device?.deviceId
|
|
3201
|
-
|| operation.sessionId !== device?.sessionId
|
|
3202
|
-
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3203
|
-
continue;
|
|
3204
|
-
}
|
|
3205
|
-
queueClipboardAgentCleanup(device, operation, reason);
|
|
3206
|
-
clipboardOperations.delete(key);
|
|
3207
|
-
cleared += 1;
|
|
3208
|
-
}
|
|
3209
|
-
if (cleared > 0) {
|
|
3210
|
-
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3211
|
-
reason,
|
|
3212
|
-
ownerConnectionId: requestedOwner,
|
|
3213
|
-
count: cleared
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
return cleared;
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3220
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3221
|
-
const currentDevice = devices.get(operation.deviceId);
|
|
3222
|
-
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3223
|
-
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3224
|
-
const staleOwner = !currentDevice
|
|
3225
|
-
|| currentDevice.sessionId !== operation.sessionId
|
|
3226
|
-
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3227
|
-
if (expired || staleOwner) {
|
|
3228
|
-
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3229
|
-
queueClipboardAgentCleanup(
|
|
3230
|
-
currentDevice,
|
|
3231
|
-
operation,
|
|
3232
|
-
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3233
|
-
}
|
|
3234
|
-
clipboardOperations.delete(key);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3240
|
-
const capabilities = device?.capabilities || {};
|
|
3241
|
-
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3242
|
-
|| capabilities.clipboardText !== true) {
|
|
3243
|
-
return 'device-clipboard-text-unavailable';
|
|
3244
|
-
}
|
|
3245
|
-
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3246
|
-
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3247
|
-
}
|
|
3248
|
-
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3249
|
-
return 'device-clipboard-copy-unavailable';
|
|
3250
|
-
}
|
|
3251
|
-
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3252
|
-
return 'device-clipboard-paste-unavailable';
|
|
3253
|
-
}
|
|
3254
|
-
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3255
|
-
return 'device-clipboard-image-unavailable';
|
|
3256
|
-
}
|
|
3257
|
-
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3258
|
-
return 'device-clipboard-files-unavailable';
|
|
3259
|
-
}
|
|
3260
|
-
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3261
|
-
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3262
|
-
return 'device-clipboard-limits-unavailable';
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3266
|
-
return 'device-clipboard-chunk-limit-unavailable';
|
|
3267
|
-
}
|
|
3268
|
-
if (contentKind === 'image'
|
|
3269
|
-
&& (!Number.isSafeInteger(Number(
|
|
3270
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3271
|
-
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3272
|
-
return 'device-clipboard-limits-unavailable';
|
|
3273
|
-
}
|
|
3274
|
-
if (contentKind === 'files'
|
|
3275
|
-
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3276
|
-
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3277
|
-
return 'device-clipboard-limits-unavailable';
|
|
3278
|
-
}
|
|
3279
|
-
return '';
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
function clipboardManifestCapabilityError(device, manifest) {
|
|
3283
|
-
if (!manifest) {
|
|
3284
|
-
return '';
|
|
3285
|
-
}
|
|
3286
|
-
const capabilities = device?.capabilities || {};
|
|
3287
|
-
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3288
|
-
return 'device-clipboard-total-limit-exceeded';
|
|
3289
|
-
}
|
|
3290
|
-
if (manifest.contentKind === 'text'
|
|
3291
|
-
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3292
|
-
return 'device-clipboard-text-limit-exceeded';
|
|
3293
|
-
}
|
|
3294
|
-
if (manifest.contentKind === 'image'
|
|
3295
|
-
&& manifest.totalBytes > Number(
|
|
3296
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3297
|
-
return 'device-clipboard-image-limit-exceeded';
|
|
3298
|
-
}
|
|
3299
|
-
if (manifest.contentKind === 'files'
|
|
3300
|
-
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3301
|
-
return 'device-clipboard-file-count-limit-exceeded';
|
|
3302
|
-
}
|
|
3303
|
-
return '';
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3307
|
-
const device = devices.get(String(deviceId || ''));
|
|
3308
|
-
if (!device) {
|
|
3309
|
-
return { ok: false, error: 'device-not-found' };
|
|
3310
|
-
}
|
|
3311
|
-
if (device.synthetic === true) {
|
|
3312
|
-
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3313
|
-
}
|
|
3314
|
-
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3315
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3316
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3317
|
-
}
|
|
3318
|
-
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3319
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
const policy = getDevicePolicy(device);
|
|
3323
|
-
if (policy.accessMode === 'block-remote-access') {
|
|
3324
|
-
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3325
|
-
}
|
|
3326
|
-
if (policy.allowControl !== true) {
|
|
3327
|
-
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3328
|
-
}
|
|
3329
|
-
if (policy.allowClipboardText !== true) {
|
|
3330
|
-
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3331
|
-
}
|
|
3332
|
-
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3333
|
-
&& policy.allowFileTransfer !== true) {
|
|
3334
|
-
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3335
|
-
}
|
|
3336
|
-
|
|
3337
|
-
const direction = request.clipboardDirection
|
|
3338
|
-
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3339
|
-
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3340
|
-
if (capabilityError) {
|
|
3341
|
-
return { ok: false, error: capabilityError };
|
|
3342
|
-
}
|
|
3343
|
-
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3344
|
-
device,
|
|
3345
|
-
request.payload?.manifest || request.manifest);
|
|
3346
|
-
if (manifestCapabilityError) {
|
|
3347
|
-
return { ok: false, error: manifestCapabilityError };
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
const controlStream = getActiveControlStream(device);
|
|
3351
|
-
if (!controlStream
|
|
3352
|
-
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3353
|
-
|| !safeString(controlStream.commandId, 128)
|
|
3354
|
-
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3355
|
-
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3356
|
-
}
|
|
3357
|
-
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3358
|
-
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3359
|
-
}
|
|
3360
|
-
const binding = request.binding || request;
|
|
3361
|
-
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3362
|
-
!== safeString(device.sessionId, 160)
|
|
3363
|
-
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3364
|
-
!== safeString(controlStream.commandId, 128)) {
|
|
3365
|
-
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3366
|
-
}
|
|
3367
|
-
if (Number(binding.captureGeneration || 0)
|
|
3368
|
-
!== Number(controlStream.captureGeneration || 0)) {
|
|
3369
|
-
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3370
|
-
}
|
|
3371
|
-
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3372
|
-
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3373
|
-
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3374
|
-
}
|
|
3375
|
-
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3376
|
-
if (!hubConnectionId
|
|
3377
|
-
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3378
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
3379
|
-
}
|
|
3380
|
-
if (options.requireFileSocket === true
|
|
3381
|
-
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3382
|
-
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3383
|
-
}
|
|
3384
|
-
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3385
|
-
}
|
|
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
|
+
}
|
|
3386
3388
|
|
|
3387
3389
|
function inspectAnnexBH264(payload) {
|
|
3388
3390
|
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
@@ -4264,15 +4266,15 @@ export function createRemoteHub(options = {}) {
|
|
|
4264
4266
|
}
|
|
4265
4267
|
}
|
|
4266
4268
|
|
|
4267
|
-
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4268
|
-
const socket = device?.fileSocket;
|
|
4269
|
-
if (!socket) {
|
|
4270
|
-
return;
|
|
4271
|
-
}
|
|
4272
|
-
|
|
4273
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4274
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4275
|
-
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;
|
|
4276
4278
|
fileSockets.delete(socket);
|
|
4277
4279
|
try {
|
|
4278
4280
|
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
@@ -4286,14 +4288,14 @@ export function createRemoteHub(options = {}) {
|
|
|
4286
4288
|
}
|
|
4287
4289
|
}
|
|
4288
4290
|
|
|
4289
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4291
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4290
4292
|
const socket = device?.inputSocket;
|
|
4291
4293
|
if (!socket) {
|
|
4292
4294
|
return;
|
|
4293
4295
|
}
|
|
4294
4296
|
|
|
4295
|
-
const writeOwner = device.inputWriteOwner;
|
|
4296
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4297
|
+
const writeOwner = device.inputWriteOwner;
|
|
4298
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4297
4299
|
device.inputSocket = null;
|
|
4298
4300
|
device.inputWriteOwner = null;
|
|
4299
4301
|
device.inputSocketConnectionId = '';
|
|
@@ -4313,7 +4315,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4313
4315
|
}
|
|
4314
4316
|
}
|
|
4315
4317
|
|
|
4316
|
-
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4318
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4317
4319
|
const deviceId = inputSockets.get(socket);
|
|
4318
4320
|
inputSockets.delete(socket);
|
|
4319
4321
|
if (!deviceId) {
|
|
@@ -4325,8 +4327,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4325
4327
|
return;
|
|
4326
4328
|
}
|
|
4327
4329
|
|
|
4328
|
-
const writeOwner = device.inputWriteOwner;
|
|
4329
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4330
|
+
const writeOwner = device.inputWriteOwner;
|
|
4331
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4330
4332
|
device.inputSocket = null;
|
|
4331
4333
|
device.inputWriteOwner = null;
|
|
4332
4334
|
device.inputSocketConnectionId = '';
|
|
@@ -4371,7 +4373,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4371
4373
|
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
4372
4374
|
}
|
|
4373
4375
|
|
|
4374
|
-
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4376
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4375
4377
|
const deviceId = fileSockets.get(socket);
|
|
4376
4378
|
fileSockets.delete(socket);
|
|
4377
4379
|
if (!deviceId) {
|
|
@@ -4379,107 +4381,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4379
4381
|
}
|
|
4380
4382
|
|
|
4381
4383
|
const device = devices.get(deviceId);
|
|
4382
|
-
if (!device || device.fileSocket !== socket) {
|
|
4383
|
-
return;
|
|
4384
|
-
}
|
|
4385
|
-
|
|
4386
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4387
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4388
|
-
device.fileSocket = null;
|
|
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;
|
|
4389
4391
|
device.fileLastSeenAt = new Date().toISOString();
|
|
4390
|
-
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4391
|
-
}
|
|
4392
|
-
|
|
4393
|
-
function getDeviceConnectionHeartbeatMs(device) {
|
|
4394
|
-
return Date.parse(device?.lastStatusAt || '')
|
|
4395
|
-
|| Date.parse(device?.connectedAt || '')
|
|
4396
|
-
|| 0;
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
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()) {
|
|
4400
4402
|
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4401
|
-
return false;
|
|
4402
|
-
}
|
|
4403
|
-
|
|
4404
|
-
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4405
|
-
if (!lastHeartbeatMs) {
|
|
4406
|
-
return true;
|
|
4407
|
-
}
|
|
4408
|
-
|
|
4409
|
-
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4410
|
-
}
|
|
4411
|
-
|
|
4412
|
-
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4413
|
-
const deviceId = safeString(device?.deviceId, 160);
|
|
4414
|
-
const sessionId = safeString(device?.sessionId, 160);
|
|
4415
|
-
const socket = device?.socket;
|
|
4416
|
-
if (!deviceId
|
|
4417
|
-
|| !sessionId
|
|
4418
|
-
|| !socket
|
|
4419
|
-
|| socket.destroyed
|
|
4420
|
-
|| devices.get(deviceId) !== device
|
|
4421
|
-
|| device.socket !== socket
|
|
4422
|
-
|| device.sessionId !== sessionId) {
|
|
4423
|
-
return false;
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4426
|
-
closeInputSocket(device, reason);
|
|
4427
|
-
closeFrameSocket(device, reason);
|
|
4428
|
-
closeAudioSocket(device, reason);
|
|
4429
|
-
closeFileSocket(device, reason);
|
|
4430
|
-
try {
|
|
4431
|
-
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4432
|
-
} catch {
|
|
4433
|
-
// The stale connection is retired below even if the final notice fails.
|
|
4434
|
-
}
|
|
4435
|
-
detachSocket(socket, reason);
|
|
4436
|
-
try {
|
|
4437
|
-
socket.destroy?.();
|
|
4438
|
-
} catch {
|
|
4439
|
-
// detachSocket already made the exact session terminal.
|
|
4440
|
-
}
|
|
4441
|
-
return true;
|
|
4442
|
-
}
|
|
4443
|
-
|
|
4444
|
-
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4445
|
-
pruneClipboardOperations(nowMs);
|
|
4446
|
-
let retired = 0;
|
|
4447
|
-
for (const device of devices.values()) {
|
|
4448
|
-
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4449
|
-
continue;
|
|
4450
|
-
}
|
|
4451
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4452
|
-
continue;
|
|
4453
|
-
}
|
|
4454
|
-
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4455
|
-
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4456
|
-
retired += 1;
|
|
4457
|
-
logWarn(
|
|
4458
|
-
'remote',
|
|
4459
|
-
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4460
|
-
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4461
|
-
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
return retired;
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
function scheduleDeviceConnectionSweep() {
|
|
4468
|
-
if (!started || deviceConnectionSweepTimer) {
|
|
4469
|
-
return;
|
|
4470
|
-
}
|
|
4471
|
-
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4472
|
-
deviceConnectionSweepTimer = null;
|
|
4473
|
-
if (!started) {
|
|
4474
|
-
return;
|
|
4475
|
-
}
|
|
4476
|
-
sweepStaleDeviceConnections();
|
|
4477
|
-
scheduleDeviceConnectionSweep();
|
|
4478
|
-
}, deviceConnectionSweepMs);
|
|
4479
|
-
deviceConnectionSweepTimer.unref?.();
|
|
4480
|
-
}
|
|
4481
|
-
|
|
4482
|
-
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
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) {
|
|
4483
4485
|
const retryAfterMs = Math.max(heartbeatMs, duplicateDeviceRetryAfterMs);
|
|
4484
4486
|
writeJsonLine(socket, {
|
|
4485
4487
|
type: 'disconnect',
|
|
@@ -4931,20 +4933,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4931
4933
|
return device;
|
|
4932
4934
|
}
|
|
4933
4935
|
|
|
4934
|
-
function attachInputSocket(socket, hello) {
|
|
4935
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4936
|
-
const device = devices.get(deviceId);
|
|
4936
|
+
function attachInputSocket(socket, hello) {
|
|
4937
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4938
|
+
const device = devices.get(deviceId);
|
|
4937
4939
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4938
4940
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4939
4941
|
socket.destroy();
|
|
4940
|
-
return null;
|
|
4941
|
-
}
|
|
4942
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4943
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4944
|
-
return null;
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
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');
|
|
4948
4950
|
|
|
4949
4951
|
const now = new Date().toISOString();
|
|
4950
4952
|
const inputSocketConnectionId = crypto.randomUUID();
|
|
@@ -5058,20 +5060,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5058
5060
|
return device;
|
|
5059
5061
|
}
|
|
5060
5062
|
|
|
5061
|
-
function attachFileSocket(socket, hello) {
|
|
5062
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5063
|
-
const device = devices.get(deviceId);
|
|
5063
|
+
function attachFileSocket(socket, hello) {
|
|
5064
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5065
|
+
const device = devices.get(deviceId);
|
|
5064
5066
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5065
5067
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5066
5068
|
socket.destroy();
|
|
5067
|
-
return null;
|
|
5068
|
-
}
|
|
5069
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5070
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5071
|
-
return null;
|
|
5072
|
-
}
|
|
5073
|
-
|
|
5074
|
-
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
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');
|
|
5075
5077
|
const now = new Date().toISOString();
|
|
5076
5078
|
device.fileSocket = socket;
|
|
5077
5079
|
device.fileConnectedAt = now;
|
|
@@ -8374,9 +8376,9 @@ export function createRemoteHub(options = {}) {
|
|
|
8374
8376
|
started = true;
|
|
8375
8377
|
boundPort = candidateServer.address()?.port || port;
|
|
8376
8378
|
lastError = '';
|
|
8377
|
-
logEvent('remote', `
|
|
8379
|
+
logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
8378
8380
|
if (host === '0.0.0.0' || host === '::') {
|
|
8379
|
-
logWarn('remote', '
|
|
8381
|
+
logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
|
|
8380
8382
|
}
|
|
8381
8383
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8382
8384
|
resolve();
|
|
@@ -8394,14 +8396,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8394
8396
|
await listenOnPort(requestedPort);
|
|
8395
8397
|
try {
|
|
8396
8398
|
await relayControl?.start?.();
|
|
8397
|
-
} catch {
|
|
8398
|
-
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8399
|
-
}
|
|
8400
|
-
scheduleDeviceConnectionSweep();
|
|
8401
|
-
} catch (error) {
|
|
8402
|
-
if (error?.code === 'EADDRINUSE') {
|
|
8403
|
-
const conflict = new Error(
|
|
8404
|
-
`
|
|
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
|
+
`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.`);
|
|
8405
8407
|
conflict.code = 'EADDRINUSE';
|
|
8406
8408
|
conflict.port = requestedPort;
|
|
8407
8409
|
conflict.cause = error;
|
|
@@ -8412,16 +8414,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8412
8414
|
}
|
|
8413
8415
|
|
|
8414
8416
|
return getStatus({ includeSecrets: false });
|
|
8415
|
-
}
|
|
8416
|
-
|
|
8417
|
-
async function close() {
|
|
8418
|
-
if (deviceConnectionSweepTimer) {
|
|
8419
|
-
clearTimeout(deviceConnectionSweepTimer);
|
|
8420
|
-
deviceConnectionSweepTimer = null;
|
|
8421
|
-
}
|
|
8422
|
-
for (const state of [...agentBinaryIngressStates]) {
|
|
8423
|
-
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8424
|
-
}
|
|
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
|
+
}
|
|
8425
8427
|
|
|
8426
8428
|
for (const device of devices.values()) {
|
|
8427
8429
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
@@ -8556,12 +8558,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8556
8558
|
};
|
|
8557
8559
|
}
|
|
8558
8560
|
|
|
8559
|
-
function sendCommand(deviceId, command) {
|
|
8560
|
-
const device = devices.get(String(deviceId || ''));
|
|
8561
|
-
const commandName = safeString(command?.command || 'ping', 80);
|
|
8562
|
-
if (REMOTE_CLIPBOARD_FILE_ACTIONS.has(commandName)) {
|
|
8563
|
-
return { ok: false, error: 'clipboard-command-requires-awaited-file-channel' };
|
|
8564
|
-
}
|
|
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
|
+
}
|
|
8565
8567
|
const requiredPermission = commandName === 'input.control'
|
|
8566
8568
|
? 'allowControl'
|
|
8567
8569
|
: commandName === 'system.power'
|
|
@@ -8597,16 +8599,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8597
8599
|
result: { ok: true, synthetic: true },
|
|
8598
8600
|
error: ''
|
|
8599
8601
|
});
|
|
8600
|
-
return { ok: true, commandId, synthetic: true };
|
|
8601
|
-
}
|
|
8602
|
-
|
|
8603
|
-
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
8604
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8605
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8606
|
-
}
|
|
8607
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8608
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
-
}
|
|
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
|
+
}
|
|
8610
8612
|
|
|
8611
8613
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8612
8614
|
const payload = {
|
|
@@ -8619,300 +8621,300 @@ export function createRemoteHub(options = {}) {
|
|
|
8619
8621
|
|
|
8620
8622
|
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
8621
8623
|
&& device.fileSocket
|
|
8622
|
-
&& !device.fileSocket.destroyed
|
|
8623
|
-
? device.fileSocket
|
|
8624
|
-
: null;
|
|
8625
|
-
try {
|
|
8626
|
-
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8627
|
-
} catch {
|
|
8628
|
-
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8629
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8630
|
-
}
|
|
8631
|
-
device.counters.commandsSent += 1;
|
|
8632
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8633
|
-
commandId,
|
|
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,
|
|
8634
8636
|
command: payload.command,
|
|
8635
8637
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8636
8638
|
});
|
|
8637
|
-
return { ok: true, commandId };
|
|
8638
|
-
}
|
|
8639
|
-
|
|
8640
|
-
async function sendClipboardCommand(deviceId, request = {}) {
|
|
8641
|
-
pruneClipboardOperations();
|
|
8642
|
-
const action = safeString(request.action, 80).toLowerCase();
|
|
8643
|
-
if (!REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
8644
|
-
return { ok: false, error: 'clipboard-file-action-invalid' };
|
|
8645
|
-
}
|
|
8646
|
-
const operationId = safeString(request.operationId, 128);
|
|
8647
|
-
if (!operationId) {
|
|
8648
|
-
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
8649
|
-
}
|
|
8650
|
-
|
|
8651
|
-
const direction = action.startsWith('clipboard.paste.')
|
|
8652
|
-
? 'paste'
|
|
8653
|
-
: action.startsWith('clipboard.copy.')
|
|
8654
|
-
? 'copy'
|
|
8655
|
-
: '';
|
|
8656
|
-
const operationKey = buildClipboardOperationKey(deviceId, request.binding?.controlSessionId, operationId);
|
|
8657
|
-
let operation = clipboardOperations.get(operationKey);
|
|
8658
|
-
const beginsOperation = action === 'clipboard.copy.begin' || action === 'clipboard.paste.begin';
|
|
8659
|
-
const endsOperation = action === 'clipboard.copy.end' || action === 'clipboard.paste.cancel';
|
|
8660
|
-
const requestedManifest = request.payload?.manifest || null;
|
|
8661
|
-
|
|
8662
|
-
if (beginsOperation) {
|
|
8663
|
-
if (operation) {
|
|
8664
|
-
return { ok: false, error: 'clipboard-operation-already-exists' };
|
|
8665
|
-
}
|
|
8666
|
-
const activeForDevice = [...clipboardOperations.values()].filter(candidate =>
|
|
8667
|
-
candidate.deviceId === String(deviceId || '')
|
|
8668
|
-
&& candidate.sessionId === request.binding?.controlSessionId);
|
|
8669
|
-
if (activeForDevice.some(candidate => candidate.direction === direction)) {
|
|
8670
|
-
return { ok: false, error: 'clipboard-operation-direction-busy' };
|
|
8671
|
-
}
|
|
8672
|
-
if (activeForDevice.length >= REMOTE_CLIPBOARD_LIMITS.maxOperationsPerDevice) {
|
|
8673
|
-
return { ok: false, error: 'clipboard-operation-capacity-reached' };
|
|
8674
|
-
}
|
|
8675
|
-
operation = {
|
|
8676
|
-
deviceId: String(deviceId || ''),
|
|
8677
|
-
sessionId: safeString(request.binding?.controlSessionId, 160),
|
|
8678
|
-
operationId,
|
|
8679
|
-
direction,
|
|
8680
|
-
hubConnectionId: safeString(request.hubConnectionId, 128),
|
|
8681
|
-
controlCommandId: safeString(request.binding?.controlCommandId, 128),
|
|
8682
|
-
captureGeneration: Number(request.binding?.captureGeneration || 0),
|
|
8683
|
-
monitorIndex: Number(request.binding?.monitorIndex),
|
|
8684
|
-
contentKind: requestedManifest?.contentKind || 'text',
|
|
8685
|
-
manifest: requestedManifest,
|
|
8686
|
-
offsets: new Map(),
|
|
8687
|
-
status: action === 'clipboard.paste.begin' ? 'preparing' : 'copying',
|
|
8688
|
-
inFlight: false,
|
|
8689
|
-
touchedAt: Date.now()
|
|
8690
|
-
};
|
|
8691
|
-
clipboardOperations.set(operationKey, operation);
|
|
8692
|
-
} else {
|
|
8693
|
-
if (!operation) {
|
|
8694
|
-
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8695
|
-
}
|
|
8696
|
-
const requestedDirection = direction || operation.direction;
|
|
8697
|
-
if (operation.direction !== requestedDirection
|
|
8698
|
-
|| operation.hubConnectionId !== safeString(request.hubConnectionId, 128)
|
|
8699
|
-
|| operation.controlCommandId !== safeString(request.binding?.controlCommandId, 128)
|
|
8700
|
-
|| operation.captureGeneration !== Number(request.binding?.captureGeneration || 0)
|
|
8701
|
-
|| operation.monitorIndex !== Number(request.binding?.monitorIndex)) {
|
|
8702
|
-
return { ok: false, error: 'clipboard-operation-owner-stale' };
|
|
8703
|
-
}
|
|
8704
|
-
}
|
|
8705
|
-
|
|
8706
|
-
if (operation.inFlight) {
|
|
8707
|
-
return { ok: false, error: 'clipboard-operation-busy' };
|
|
8708
|
-
}
|
|
8709
|
-
|
|
8710
|
-
const contentKind = operation.contentKind || requestedManifest?.contentKind || 'text';
|
|
8711
|
-
const bindingResult = validateCurrentClipboardBinding(
|
|
8712
|
-
deviceId,
|
|
8713
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8714
|
-
contentKind,
|
|
8715
|
-
{ requireFileSocket: true });
|
|
8716
|
-
if (!bindingResult.ok) {
|
|
8717
|
-
if (beginsOperation || bindingResult.error?.startsWith('STALE_') || bindingResult.error === 'input-owner-not-current') {
|
|
8718
|
-
clipboardOperations.delete(operationKey);
|
|
8719
|
-
}
|
|
8720
|
-
return bindingResult;
|
|
8721
|
-
}
|
|
8722
|
-
|
|
8723
|
-
const { device } = bindingResult;
|
|
8724
|
-
const fileSocket = device.fileSocket;
|
|
8725
|
-
const itemIndex = request.payload?.itemIndex;
|
|
8726
|
-
const expectedOffset = Number.isInteger(itemIndex)
|
|
8727
|
-
? Number(operation.offsets.get(itemIndex) || 0)
|
|
8728
|
-
: 0;
|
|
8729
|
-
let outgoingChunkBytes = 0;
|
|
8730
|
-
if (action === 'clipboard.copy.chunk' || action === 'clipboard.paste.chunk') {
|
|
8731
|
-
const item = operation.manifest?.items?.[itemIndex];
|
|
8732
|
-
if (!item) {
|
|
8733
|
-
return { ok: false, error: 'clipboard-item-not-found' };
|
|
8734
|
-
}
|
|
8735
|
-
if (request.payload.offset !== expectedOffset) {
|
|
8736
|
-
return { ok: false, error: 'clipboard-chunk-offset-stale', expectedOffset };
|
|
8737
|
-
}
|
|
8738
|
-
if (action === 'clipboard.copy.chunk'
|
|
8739
|
-
&& request.payload.maxBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8740
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8741
|
-
}
|
|
8742
|
-
if (action === 'clipboard.paste.chunk') {
|
|
8743
|
-
outgoingChunkBytes = Buffer.from(request.payload.dataBase64, 'base64').length;
|
|
8744
|
-
if (outgoingChunkBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8745
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8746
|
-
}
|
|
8747
|
-
const nextOffset = expectedOffset + outgoingChunkBytes;
|
|
8748
|
-
if (outgoingChunkBytes === 0
|
|
8749
|
-
&& !(item.size === 0 && expectedOffset === 0 && request.payload.final === true)) {
|
|
8750
|
-
return { ok: false, error: 'clipboard-empty-chunk-invalid' };
|
|
8751
|
-
}
|
|
8752
|
-
if (nextOffset > item.size
|
|
8753
|
-
|| request.payload.final !== (nextOffset === item.size)) {
|
|
8754
|
-
return { ok: false, error: 'clipboard-chunk-boundary-invalid' };
|
|
8755
|
-
}
|
|
8756
|
-
}
|
|
8757
|
-
}
|
|
8758
|
-
|
|
8759
|
-
operation.inFlight = true;
|
|
8760
|
-
operation.touchedAt = Date.now();
|
|
8761
|
-
let commandCompleted = false;
|
|
8762
|
-
const commandId = crypto.randomUUID();
|
|
8763
|
-
const resultPromise = waitForCommandResult(
|
|
8764
|
-
device,
|
|
8765
|
-
commandId,
|
|
8766
|
-
REMOTE_CLIPBOARD_LIMITS.commandTimeoutMs,
|
|
8767
|
-
{ channel: 'file', operationId });
|
|
8768
|
-
const commandPayload = {
|
|
8769
|
-
type: 'command',
|
|
8770
|
-
commandId,
|
|
8771
|
-
command: action,
|
|
8772
|
-
payload: {
|
|
8773
|
-
operationId,
|
|
8774
|
-
controlSessionId: operation.sessionId,
|
|
8775
|
-
controlCommandId: operation.controlCommandId,
|
|
8776
|
-
captureGeneration: operation.captureGeneration,
|
|
8777
|
-
monitorIndex: operation.monitorIndex,
|
|
8778
|
-
hubConnectionId: operation.hubConnectionId,
|
|
8779
|
-
...(request.payload || {})
|
|
8780
|
-
},
|
|
8781
|
-
issuedAt: new Date().toISOString()
|
|
8782
|
-
};
|
|
8783
|
-
|
|
8784
|
-
try {
|
|
8785
|
-
try {
|
|
8786
|
-
// Clipboard payloads are never allowed onto the concurrent
|
|
8787
|
-
// main command socket. The dedicated file side channel owns
|
|
8788
|
-
// the one bounded command and its exact-session waiter.
|
|
8789
|
-
writeJsonLine(fileSocket, commandPayload);
|
|
8790
|
-
} catch {
|
|
8791
|
-
failPendingCommandResultWaiter(device, commandId, 'clipboard-file-channel-write-failed');
|
|
8792
|
-
if (device.fileSocket === fileSocket) {
|
|
8793
|
-
closeFileSocket(device, 'clipboard-file-channel-write-failed');
|
|
8794
|
-
}
|
|
8795
|
-
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
8796
|
-
}
|
|
8797
|
-
device.counters.commandsSent += 1;
|
|
8798
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8799
|
-
commandId,
|
|
8800
|
-
command: action,
|
|
8801
|
-
channel: 'file',
|
|
8802
|
-
operationId
|
|
8803
|
-
});
|
|
8804
|
-
|
|
8805
|
-
const awaited = await resultPromise;
|
|
8806
|
-
if (!awaited.ok) {
|
|
8807
|
-
return {
|
|
8808
|
-
ok: false,
|
|
8809
|
-
error: awaited.error || 'clipboard-command-failed',
|
|
8810
|
-
commandId
|
|
8811
|
-
};
|
|
8812
|
-
}
|
|
8813
|
-
if (device.fileSocket !== fileSocket) {
|
|
8814
|
-
return { ok: false, error: 'clipboard-file-channel-owner-changed' };
|
|
8815
|
-
}
|
|
8816
|
-
const currentBinding = validateCurrentClipboardBinding(
|
|
8817
|
-
deviceId,
|
|
8818
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8819
|
-
operation.contentKind || 'text',
|
|
8820
|
-
{ requireFileSocket: true });
|
|
8821
|
-
if (!currentBinding.ok) {
|
|
8822
|
-
return currentBinding;
|
|
8823
|
-
}
|
|
8824
|
-
|
|
8825
|
-
let response;
|
|
8826
|
-
try {
|
|
8827
|
-
response = normalizeRemoteClipboardCommandResult(action, operationId, awaited.result);
|
|
8828
|
-
} catch (error) {
|
|
8829
|
-
return {
|
|
8830
|
-
ok: false,
|
|
8831
|
-
error: safeString(error?.code || error?.message, 160) || 'clipboard-response-invalid'
|
|
8832
|
-
};
|
|
8833
|
-
}
|
|
8834
|
-
if (response.manifest) {
|
|
8835
|
-
const responseManifestLimitError = clipboardManifestCapabilityError(device, response.manifest);
|
|
8836
|
-
if (responseManifestLimitError) {
|
|
8837
|
-
clipboardOperations.delete(operationKey);
|
|
8838
|
-
return { ok: false, error: responseManifestLimitError };
|
|
8839
|
-
}
|
|
8840
|
-
const responsePolicy = validateCurrentClipboardBinding(
|
|
8841
|
-
deviceId,
|
|
8842
|
-
{ ...request, clipboardDirection: operation.direction },
|
|
8843
|
-
response.manifest.contentKind,
|
|
8844
|
-
{ requireFileSocket: true });
|
|
8845
|
-
if (!responsePolicy.ok) {
|
|
8846
|
-
clipboardOperations.delete(operationKey);
|
|
8847
|
-
return responsePolicy;
|
|
8848
|
-
}
|
|
8849
|
-
operation.manifest = response.manifest;
|
|
8850
|
-
operation.contentKind = response.manifest.contentKind;
|
|
8851
|
-
}
|
|
8852
|
-
if (action === 'clipboard.copy.chunk') {
|
|
8853
|
-
const item = operation.manifest?.items?.[response.itemIndex];
|
|
8854
|
-
const nextOffset = response.offset + response.bytes;
|
|
8855
|
-
if (!item
|
|
8856
|
-
|| response.itemIndex !== itemIndex
|
|
8857
|
-
|| response.offset !== expectedOffset
|
|
8858
|
-
|| (response.bytes === 0
|
|
8859
|
-
&& !(item.size === 0 && response.offset === 0 && response.final === true))
|
|
8860
|
-
|| nextOffset > item.size
|
|
8861
|
-
|| response.final !== (nextOffset === item.size)) {
|
|
8862
|
-
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8863
|
-
}
|
|
8864
|
-
if (response.bytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8865
|
-
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8866
|
-
}
|
|
8867
|
-
operation.offsets.set(response.itemIndex, nextOffset);
|
|
8868
|
-
} else if (action === 'clipboard.paste.chunk') {
|
|
8869
|
-
if (response.itemIndex !== itemIndex
|
|
8870
|
-
|| response.offset !== expectedOffset
|
|
8871
|
-
|| response.bytes !== outgoingChunkBytes
|
|
8872
|
-
|| response.totalBytes !== operation.manifest.items[itemIndex].size
|
|
8873
|
-
|| response.final !== request.payload.final) {
|
|
8874
|
-
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8875
|
-
}
|
|
8876
|
-
operation.offsets.set(itemIndex, expectedOffset + outgoingChunkBytes);
|
|
8877
|
-
}
|
|
8878
|
-
operation.status = response.status || operation.status;
|
|
8879
|
-
operation.touchedAt = Date.now();
|
|
8880
|
-
if (action === 'clipboard.operation.status' && operation.status === 'not-found') {
|
|
8881
|
-
clipboardOperations.delete(operationKey);
|
|
8882
|
-
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8883
|
-
}
|
|
8884
|
-
if (action === 'clipboard.operation.status'
|
|
8885
|
-
&& ['completed', 'complete', 'applied', 'pasted', 'cancelled', 'cleaned', 'failed', 'expired', 'already-ended']
|
|
8886
|
-
.includes(operation.status)) {
|
|
8887
|
-
clipboardOperations.delete(operationKey);
|
|
8888
|
-
}
|
|
8889
|
-
commandCompleted = true;
|
|
8890
|
-
return response;
|
|
8891
|
-
} finally {
|
|
8892
|
-
const currentOperation = clipboardOperations.get(operationKey);
|
|
8893
|
-
if (currentOperation === operation) {
|
|
8894
|
-
operation.inFlight = false;
|
|
8895
|
-
if (endsOperation || (beginsOperation && !commandCompleted)) {
|
|
8896
|
-
clipboardOperations.delete(operationKey);
|
|
8897
|
-
}
|
|
8898
|
-
}
|
|
8899
|
-
}
|
|
8900
|
-
}
|
|
8901
|
-
|
|
8902
|
-
function refreshDevicePolicies(deviceIds = undefined) {
|
|
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) {
|
|
8903
8905
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8904
8906
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8905
8907
|
: null;
|
|
8906
8908
|
let updated = 0;
|
|
8907
8909
|
let total = 0;
|
|
8908
|
-
for (const device of devices.values()) {
|
|
8909
|
-
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8910
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8911
|
-
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8912
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8913
|
-
continue;
|
|
8914
|
-
}
|
|
8915
|
-
total += 1;
|
|
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;
|
|
8916
8918
|
const effectivePolicy = getDevicePolicy(device);
|
|
8917
8919
|
if (writeJsonLine(device.socket, {
|
|
8918
8920
|
type: 'policy.update',
|
|
@@ -8931,14 +8933,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8931
8933
|
const device = devices.get(String(deviceId || ''));
|
|
8932
8934
|
const denied = policyError(device);
|
|
8933
8935
|
if (denied) return { ok: false, error: denied };
|
|
8934
|
-
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8935
|
-
if (device.synthetic !== true
|
|
8936
|
-
&& device.connected
|
|
8937
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
8938
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8939
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8940
|
-
}
|
|
8941
|
-
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
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) {
|
|
8942
8944
|
return { ok: false, error: 'device-not-connected' };
|
|
8943
8945
|
}
|
|
8944
8946
|
|
|
@@ -9034,21 +9036,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9034
9036
|
return device.inputWriteOwner;
|
|
9035
9037
|
}
|
|
9036
9038
|
|
|
9037
|
-
function sendInputControl(deviceId, input = {}) {
|
|
9038
|
-
const device = devices.get(String(deviceId || ''));
|
|
9039
|
-
if (!device) {
|
|
9040
|
-
return { ok: false, error: 'device-not-found' };
|
|
9041
|
-
}
|
|
9042
|
-
|
|
9043
|
-
if (device.synthetic !== true
|
|
9044
|
-
&& device.connected
|
|
9045
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
9046
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9047
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9048
|
-
}
|
|
9049
|
-
if (!device.connected) {
|
|
9050
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9051
|
-
}
|
|
9039
|
+
function sendInputControl(deviceId, input = {}) {
|
|
9040
|
+
const device = devices.get(String(deviceId || ''));
|
|
9041
|
+
if (!device) {
|
|
9042
|
+
return { ok: false, error: 'device-not-found' };
|
|
9043
|
+
}
|
|
9044
|
+
|
|
9045
|
+
if (device.synthetic !== true
|
|
9046
|
+
&& device.connected
|
|
9047
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
9048
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9049
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9050
|
+
}
|
|
9051
|
+
if (!device.connected) {
|
|
9052
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9053
|
+
}
|
|
9052
9054
|
|
|
9053
9055
|
const normalized = normalizeRemoteInputEvent(input);
|
|
9054
9056
|
if (!normalized.type) {
|
|
@@ -9114,51 +9116,51 @@ export function createRemoteHub(options = {}) {
|
|
|
9114
9116
|
|| device.capabilities?.accessibilityPermission === false) {
|
|
9115
9117
|
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
9116
9118
|
}
|
|
9117
|
-
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
9118
|
-
return { ok: false, error: 'device-input-control-unavailable' };
|
|
9119
|
-
}
|
|
9120
|
-
|
|
9121
|
-
const normalizedInputType = normalized.type.toLowerCase();
|
|
9122
|
-
const isClipboardInput = normalizedInputType === 'clipboard.prepare'
|
|
9123
|
-
|| normalizedInputType === 'clipboard.copy'
|
|
9124
|
-
|| normalizedInputType === 'clipboard.paste';
|
|
9125
|
-
let clipboardManifest = null;
|
|
9126
|
-
if (isClipboardInput) {
|
|
9127
|
-
if (!normalized.clipboardOperationId) {
|
|
9128
|
-
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
9129
|
-
}
|
|
9130
|
-
if (!['copy', 'paste'].includes(normalized.clipboardDirection)) {
|
|
9131
|
-
return { ok: false, error: 'clipboard-direction-required' };
|
|
9132
|
-
}
|
|
9133
|
-
if ((normalizedInputType === 'clipboard.copy' && normalized.clipboardDirection !== 'copy')
|
|
9134
|
-
|| (normalizedInputType === 'clipboard.paste' && normalized.clipboardDirection !== 'paste')) {
|
|
9135
|
-
return { ok: false, error: 'clipboard-direction-mismatch' };
|
|
9136
|
-
}
|
|
9137
|
-
try {
|
|
9138
|
-
clipboardManifest = normalized.manifest
|
|
9139
|
-
? normalizeRemoteClipboardManifest(normalized.manifest, normalized.clipboardOperationId)
|
|
9140
|
-
: null;
|
|
9141
|
-
} catch (error) {
|
|
9142
|
-
return { ok: false, error: safeString(error?.code || error?.message, 160) || 'clipboard-manifest-invalid' };
|
|
9143
|
-
}
|
|
9144
|
-
const clipboardPolicy = getDevicePolicy(device);
|
|
9145
|
-
if (clipboardPolicy.allowClipboardText !== true) {
|
|
9146
|
-
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
9147
|
-
}
|
|
9148
|
-
const contentKind = clipboardManifest?.contentKind || normalized.contentKind || 'text';
|
|
9149
|
-
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
9150
|
-
&& clipboardPolicy.allowFileTransfer !== true) {
|
|
9151
|
-
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
9152
|
-
}
|
|
9153
|
-
const clipboardCapabilityDenied = clipboardCapabilityError(
|
|
9154
|
-
device,
|
|
9155
|
-
normalized.clipboardDirection,
|
|
9156
|
-
contentKind);
|
|
9157
|
-
if (clipboardCapabilityDenied) {
|
|
9158
|
-
return { ok: false, error: clipboardCapabilityDenied };
|
|
9159
|
-
}
|
|
9160
|
-
normalized.manifest = clipboardManifest;
|
|
9161
|
-
}
|
|
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
|
+
}
|
|
9162
9164
|
|
|
9163
9165
|
const controlStream = getActiveControlStream(device);
|
|
9164
9166
|
if (!controlStream
|
|
@@ -9194,32 +9196,32 @@ export function createRemoteHub(options = {}) {
|
|
|
9194
9196
|
};
|
|
9195
9197
|
}
|
|
9196
9198
|
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
9197
|
-
if (!Number.isInteger(normalized.monitorIndex)
|
|
9198
|
-
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9199
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
9200
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9199
9201
|
return {
|
|
9200
9202
|
ok: false,
|
|
9201
9203
|
error: 'STALE_CONTROL_MONITOR',
|
|
9202
|
-
activeMonitorIndex
|
|
9203
|
-
};
|
|
9204
|
-
}
|
|
9205
|
-
|
|
9206
|
-
let clipboardOperationKey = '';
|
|
9207
|
-
if (normalizedInputType === 'clipboard.paste') {
|
|
9208
|
-
clipboardOperationKey = buildClipboardOperationKey(
|
|
9209
|
-
device.deviceId,
|
|
9210
|
-
device.sessionId,
|
|
9211
|
-
normalized.clipboardOperationId);
|
|
9212
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9213
|
-
if (!operation
|
|
9214
|
-
|| operation.direction !== 'paste'
|
|
9215
|
-
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9216
|
-
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9217
|
-
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9218
|
-
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9219
|
-
|| operation.status !== 'ready') {
|
|
9220
|
-
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9221
|
-
}
|
|
9222
|
-
}
|
|
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
|
+
}
|
|
9223
9225
|
|
|
9224
9226
|
const inputSocket = device.inputSocket;
|
|
9225
9227
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9236,17 +9238,17 @@ export function createRemoteHub(options = {}) {
|
|
|
9236
9238
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
9237
9239
|
const bindingChanged = previousInputBindingKey
|
|
9238
9240
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
9239
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9240
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9241
|
-
if (ownerChanged) {
|
|
9242
|
-
clearClipboardOperationsForDevice(
|
|
9243
|
-
device,
|
|
9244
|
-
'browser-input-owner-changed',
|
|
9245
|
-
previousOwnerConnectionId);
|
|
9246
|
-
} else if (bindingChanged || writeBindingChanged) {
|
|
9247
|
-
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9248
|
-
}
|
|
9249
|
-
const resetSent = inputWriteOwner.replaceBinding(
|
|
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(
|
|
9250
9252
|
activeInputBindingKey,
|
|
9251
9253
|
buildRemoteInputResetMessage(
|
|
9252
9254
|
device,
|
|
@@ -9276,16 +9278,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9276
9278
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
9277
9279
|
}
|
|
9278
9280
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
9279
|
-
device.counters.commandsSent += 1;
|
|
9280
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
9281
|
-
if (clipboardOperationKey) {
|
|
9282
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9283
|
-
if (operation) {
|
|
9284
|
-
operation.status = 'pasting';
|
|
9285
|
-
operation.touchedAt = Date.now();
|
|
9286
|
-
}
|
|
9287
|
-
}
|
|
9288
|
-
return {
|
|
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 {
|
|
9289
9291
|
ok: true,
|
|
9290
9292
|
inputSocket: true,
|
|
9291
9293
|
sessionId: device.sessionId,
|
|
@@ -9320,7 +9322,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9320
9322
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
9321
9323
|
}
|
|
9322
9324
|
});
|
|
9323
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9325
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9324
9326
|
schedulePendingInputFallback(device, {
|
|
9325
9327
|
commandId: fallback.commandId,
|
|
9326
9328
|
inputSeq: normalized.inputSeq,
|
|
@@ -9332,16 +9334,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9332
9334
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
9333
9335
|
hubForwardedAtEpochMs,
|
|
9334
9336
|
fallback: true,
|
|
9335
|
-
timeoutTimer: null
|
|
9336
|
-
});
|
|
9337
|
-
}
|
|
9338
|
-
if (fallback.ok && clipboardOperationKey) {
|
|
9339
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9340
|
-
if (operation) {
|
|
9341
|
-
operation.status = 'pasting';
|
|
9342
|
-
operation.touchedAt = Date.now();
|
|
9343
|
-
}
|
|
9344
|
-
}
|
|
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
|
+
}
|
|
9345
9347
|
return fallback.ok
|
|
9346
9348
|
? {
|
|
9347
9349
|
...fallback,
|
|
@@ -9361,15 +9363,15 @@ export function createRemoteHub(options = {}) {
|
|
|
9361
9363
|
};
|
|
9362
9364
|
}
|
|
9363
9365
|
|
|
9364
|
-
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9366
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9365
9367
|
const device = devices.get(String(deviceId || ''));
|
|
9366
9368
|
const owner = safeString(ownerConnectionId, 128);
|
|
9367
|
-
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9368
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
9369
|
-
}
|
|
9370
|
-
|
|
9371
|
-
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9372
|
-
device.inputOwnerConnectionId = '';
|
|
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 = '';
|
|
9373
9375
|
device.inputOwnerBindingKey = '';
|
|
9374
9376
|
const inputSocket = device.inputSocket;
|
|
9375
9377
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9413,12 +9415,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9413
9415
|
};
|
|
9414
9416
|
const results = targets.map(deviceId => {
|
|
9415
9417
|
const device = devices.get(deviceId);
|
|
9416
|
-
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9417
|
-
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9418
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9419
|
-
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9420
|
-
}
|
|
9421
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
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) {
|
|
9422
9424
|
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9423
9425
|
}
|
|
9424
9426
|
return writeJsonLine(device.socket, payload)
|
|
@@ -9450,7 +9452,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9450
9452
|
const denied = policyError(device, 'allowAgent');
|
|
9451
9453
|
if (denied) return { ok: false, error: denied };
|
|
9452
9454
|
|
|
9453
|
-
if (device?.synthetic === true && device.connected) {
|
|
9455
|
+
if (device?.synthetic === true && device.connected) {
|
|
9454
9456
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
9455
9457
|
if (!instruction) {
|
|
9456
9458
|
return { ok: false, error: 'missing-instruction' };
|
|
@@ -9510,14 +9512,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9510
9512
|
error: '',
|
|
9511
9513
|
synthetic: true
|
|
9512
9514
|
});
|
|
9513
|
-
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
9514
|
-
}
|
|
9515
|
-
|
|
9516
|
-
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
9517
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9518
|
-
return { ok: false, error: 'device-not-connected' };
|
|
9519
|
-
}
|
|
9520
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
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) {
|
|
9521
9523
|
return { ok: false, error: 'device-not-connected' };
|
|
9522
9524
|
}
|
|
9523
9525
|
|
|
@@ -10378,17 +10380,17 @@ export function createRemoteHub(options = {}) {
|
|
|
10378
10380
|
};
|
|
10379
10381
|
}
|
|
10380
10382
|
|
|
10381
|
-
function startLiveStream(deviceId, options = {}) {
|
|
10382
|
-
const device = devices.get(String(deviceId || ''));
|
|
10383
|
-
const denied = policyError(device);
|
|
10384
|
-
if (denied) return { ok: false, error: denied };
|
|
10385
|
-
if (device?.synthetic !== true
|
|
10386
|
-
&& device?.connected
|
|
10387
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
10388
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10389
|
-
return { ok: false, error: 'device-not-connected' };
|
|
10390
|
-
}
|
|
10391
|
-
if (device?.liveCapturePause?.token) {
|
|
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) {
|
|
10392
10394
|
return {
|
|
10393
10395
|
ok: false,
|
|
10394
10396
|
error: 'LIVE_CAPTURE_PAUSED',
|
|
@@ -10819,15 +10821,15 @@ export function createRemoteHub(options = {}) {
|
|
|
10819
10821
|
};
|
|
10820
10822
|
}
|
|
10821
10823
|
|
|
10822
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
10823
|
-
const device = devices.get(String(deviceId || ''));
|
|
10824
|
-
if (device?.synthetic !== true
|
|
10825
|
-
&& device?.connected
|
|
10826
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
10827
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10828
|
-
return { ok: false, error: 'device-not-connected' };
|
|
10829
|
-
}
|
|
10830
|
-
const stopAll = options.stopAll === true;
|
|
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;
|
|
10831
10833
|
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24).toLowerCase();
|
|
10832
10834
|
const requestedStreamId = safeString(options.streamId, 128);
|
|
10833
10835
|
const purposeStreams = !stopAll && !requestedStreamId && streamPurpose && device
|
|
@@ -11428,18 +11430,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11428
11430
|
};
|
|
11429
11431
|
}
|
|
11430
11432
|
|
|
11431
|
-
function startAudioStream(deviceId, options = {}) {
|
|
11432
|
-
const device = devices.get(String(deviceId || ''));
|
|
11433
|
-
if (!device) {
|
|
11434
|
-
return { ok: false, error: 'device-not-found' };
|
|
11435
|
-
}
|
|
11436
|
-
if (device.synthetic !== true
|
|
11437
|
-
&& device.connected
|
|
11438
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
11439
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11440
|
-
return { ok: false, error: 'device-not-connected' };
|
|
11441
|
-
}
|
|
11442
|
-
if (!device.connected) {
|
|
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) {
|
|
11443
11445
|
return { ok: false, error: 'device-not-connected' };
|
|
11444
11446
|
}
|
|
11445
11447
|
const denied = policyError(device, 'allowRemoteAudio');
|
|
@@ -11516,18 +11518,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11516
11518
|
return { ok: true, commandId, streamId };
|
|
11517
11519
|
}
|
|
11518
11520
|
|
|
11519
|
-
function stopAudioStream(deviceId, options = {}) {
|
|
11520
|
-
const device = devices.get(String(deviceId || ''));
|
|
11521
|
-
if (!device) {
|
|
11522
|
-
return { ok: false, error: 'device-not-found' };
|
|
11523
|
-
}
|
|
11524
|
-
if (device.synthetic !== true
|
|
11525
|
-
&& device.connected
|
|
11526
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
11527
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11528
|
-
return { ok: false, error: 'device-not-connected' };
|
|
11529
|
-
}
|
|
11530
|
-
if (!device.connected) {
|
|
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) {
|
|
11531
11533
|
return { ok: false, error: 'device-not-connected' };
|
|
11532
11534
|
}
|
|
11533
11535
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
@@ -11740,14 +11742,14 @@ export function createRemoteHub(options = {}) {
|
|
|
11740
11742
|
replacementTimerCount: device.liveStreamReplacementTimers instanceof Map
|
|
11741
11743
|
? device.liveStreamReplacementTimers.size
|
|
11742
11744
|
: 0,
|
|
11743
|
-
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11744
|
-
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11745
|
-
&& waiter.sessionId === device.sessionId)
|
|
11746
|
-
.length,
|
|
11747
|
-
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11748
|
-
.filter(operation => operation.deviceId === device.deviceId
|
|
11749
|
-
&& operation.sessionId === device.sessionId)
|
|
11750
|
-
.length,
|
|
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,
|
|
11751
11753
|
retainedPayloadCount: retainedPayloads.size,
|
|
11752
11754
|
retainedPayloadBytes: [...retainedPayloads].reduce(
|
|
11753
11755
|
(total, payload) => total + payload.length,
|
|
@@ -11757,9 +11759,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11757
11759
|
&& snapshot.stopPendingCount === 0
|
|
11758
11760
|
&& snapshot.pendingStartCount === 0
|
|
11759
11761
|
&& snapshot.pendingStopPromiseCount === 0
|
|
11760
|
-
&& snapshot.replacementTimerCount === 0
|
|
11761
|
-
&& snapshot.commandResultWaiterCount === 0
|
|
11762
|
-
&& snapshot.clipboardOperationCount === 0
|
|
11762
|
+
&& snapshot.replacementTimerCount === 0
|
|
11763
|
+
&& snapshot.commandResultWaiterCount === 0
|
|
11764
|
+
&& snapshot.clipboardOperationCount === 0
|
|
11763
11765
|
&& snapshot.retainedPayloadCount === 0
|
|
11764
11766
|
&& snapshot.retainedPayloadBytes === 0;
|
|
11765
11767
|
return snapshot;
|
|
@@ -11771,9 +11773,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11771
11773
|
'stopPendingCount',
|
|
11772
11774
|
'pendingStartCount',
|
|
11773
11775
|
'pendingStopPromiseCount',
|
|
11774
|
-
'replacementTimerCount',
|
|
11775
|
-
'commandResultWaiterCount',
|
|
11776
|
-
'clipboardOperationCount',
|
|
11776
|
+
'replacementTimerCount',
|
|
11777
|
+
'commandResultWaiterCount',
|
|
11778
|
+
'clipboardOperationCount',
|
|
11777
11779
|
'retainedPayloadCount',
|
|
11778
11780
|
'retainedPayloadBytes'
|
|
11779
11781
|
]) {
|
|
@@ -11786,10 +11788,10 @@ export function createRemoteHub(options = {}) {
|
|
|
11786
11788
|
stopPendingCount: 0,
|
|
11787
11789
|
pendingStartCount: 0,
|
|
11788
11790
|
pendingStopPromiseCount: 0,
|
|
11789
|
-
replacementTimerCount: 0,
|
|
11790
|
-
commandResultWaiterCount: 0,
|
|
11791
|
-
clipboardOperationCount: 0,
|
|
11792
|
-
retainedPayloadCount: 0,
|
|
11791
|
+
replacementTimerCount: 0,
|
|
11792
|
+
commandResultWaiterCount: 0,
|
|
11793
|
+
clipboardOperationCount: 0,
|
|
11794
|
+
retainedPayloadCount: 0,
|
|
11793
11795
|
retainedPayloadBytes: 0
|
|
11794
11796
|
});
|
|
11795
11797
|
return {
|
|
@@ -11829,9 +11831,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11829
11831
|
listDeviceFrames,
|
|
11830
11832
|
disconnectDevice,
|
|
11831
11833
|
assignDeviceSlot,
|
|
11832
|
-
sendCommand,
|
|
11833
|
-
sendClipboardCommand,
|
|
11834
|
-
refreshDevicePolicies,
|
|
11834
|
+
sendCommand,
|
|
11835
|
+
sendClipboardCommand,
|
|
11836
|
+
refreshDevicePolicies,
|
|
11835
11837
|
sendLegacyClientUpdate,
|
|
11836
11838
|
sendInputControl,
|
|
11837
11839
|
releaseInputOwner,
|