@livedesk/hub 0.1.49 → 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 +36 -36
- package/src/agents/codex-agent-runtime.js +16 -16
- package/src/agents/codex-mcp-server.js +5 -5
- package/src/filesystem/roots.js +20 -20
- package/src/filesystem/shared-folders.js +2 -2
- package/src/live-desk-update.js +9 -9
- package/src/remote-clipboard-contract.mjs +482 -482
- package/src/remote-hub.js +1025 -1025
- package/src/server.js +146 -146
- package/src/settings/effective-device-policy.js +31 -31
- package/src/settings/settings-schema.js +2 -2
package/src/remote-hub.js
CHANGED
|
@@ -3,18 +3,18 @@ import os from 'os';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
5
5
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
6
|
-
import {
|
|
7
|
-
BoundedSegmentedBuffer,
|
|
8
|
-
createBoundedAgentBinaryIngressLane
|
|
9
|
-
} from './transport/agent-binary-ingress.js';
|
|
10
|
-
import {
|
|
11
|
-
REMOTE_CLIPBOARD_FILE_ACTIONS,
|
|
12
|
-
REMOTE_CLIPBOARD_LIMITS,
|
|
13
|
-
REMOTE_CLIPBOARD_PROTOCOL,
|
|
14
|
-
normalizeRemoteClipboardCommandResult,
|
|
15
|
-
normalizeRemoteClipboardManifest,
|
|
16
|
-
remoteClipboardContentNeedsFileTransfer
|
|
17
|
-
} from './remote-clipboard-contract.mjs';
|
|
6
|
+
import {
|
|
7
|
+
BoundedSegmentedBuffer,
|
|
8
|
+
createBoundedAgentBinaryIngressLane
|
|
9
|
+
} from './transport/agent-binary-ingress.js';
|
|
10
|
+
import {
|
|
11
|
+
REMOTE_CLIPBOARD_FILE_ACTIONS,
|
|
12
|
+
REMOTE_CLIPBOARD_LIMITS,
|
|
13
|
+
REMOTE_CLIPBOARD_PROTOCOL,
|
|
14
|
+
normalizeRemoteClipboardCommandResult,
|
|
15
|
+
normalizeRemoteClipboardManifest,
|
|
16
|
+
remoteClipboardContentNeedsFileTransfer
|
|
17
|
+
} from './remote-clipboard-contract.mjs';
|
|
18
18
|
|
|
19
19
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
20
20
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
@@ -236,13 +236,13 @@ const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
|
236
236
|
]);
|
|
237
237
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
238
238
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
239
|
-
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
240
|
-
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
241
|
-
const DEFAULT_DEVICE_CONNECTION_STALE_MS = 20000;
|
|
242
|
-
const DEFAULT_DEVICE_CONNECTION_SWEEP_MS = 5000;
|
|
243
|
-
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
244
|
-
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
245
|
-
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 16;
|
|
239
|
+
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
240
|
+
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
241
|
+
const DEFAULT_DEVICE_CONNECTION_STALE_MS = 20000;
|
|
242
|
+
const DEFAULT_DEVICE_CONNECTION_SWEEP_MS = 5000;
|
|
243
|
+
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
244
|
+
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
245
|
+
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 16;
|
|
246
246
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_BYTES = 12 * 1024 * 1024;
|
|
247
247
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_AGE_MS = 120;
|
|
248
248
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
@@ -1089,7 +1089,7 @@ async function fetchTextWithTimeout(url, timeoutMs) {
|
|
|
1089
1089
|
signal: controller.signal,
|
|
1090
1090
|
headers: {
|
|
1091
1091
|
accept: 'application/json,text/plain;q=0.9,*/*;q=0.1',
|
|
1092
|
-
'user-agent': '
|
|
1092
|
+
'user-agent': 'VuvoDesk-Hub/0.1'
|
|
1093
1093
|
}
|
|
1094
1094
|
});
|
|
1095
1095
|
if (!response.ok) {
|
|
@@ -1244,11 +1244,11 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1244
1244
|
'application.close',
|
|
1245
1245
|
'file.read',
|
|
1246
1246
|
'file.write',
|
|
1247
|
-
'file.delete',
|
|
1248
|
-
'file.list',
|
|
1249
|
-
'file.search',
|
|
1250
|
-
'directory.create',
|
|
1251
|
-
'command.run',
|
|
1247
|
+
'file.delete',
|
|
1248
|
+
'file.list',
|
|
1249
|
+
'file.search',
|
|
1250
|
+
'directory.create',
|
|
1251
|
+
'command.run',
|
|
1252
1252
|
'script.run',
|
|
1253
1253
|
'software.install',
|
|
1254
1254
|
'network.status',
|
|
@@ -1280,7 +1280,7 @@ function readRawRemoteInputMonitorIndex(input) {
|
|
|
1280
1280
|
return undefined;
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
|
-
function normalizeRemoteInputEvent(value = {}) {
|
|
1283
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
1284
1284
|
const input = value && typeof value === 'object' ? value : {};
|
|
1285
1285
|
const type = safeString(input.type || input.Type, 48);
|
|
1286
1286
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
@@ -1301,14 +1301,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1301
1301
|
.filter(Boolean))]
|
|
1302
1302
|
.slice(0, 64)
|
|
1303
1303
|
: null;
|
|
1304
|
-
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1305
|
-
const clipboardOperationId = safeString(
|
|
1306
|
-
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1307
|
-
128);
|
|
1308
|
-
const clipboardDirection = safeString(
|
|
1309
|
-
input.clipboardDirection || input.ClipboardDirection,
|
|
1310
|
-
16).toLowerCase();
|
|
1311
|
-
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1304
|
+
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1305
|
+
const clipboardOperationId = safeString(
|
|
1306
|
+
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1307
|
+
128);
|
|
1308
|
+
const clipboardDirection = safeString(
|
|
1309
|
+
input.clipboardDirection || input.ClipboardDirection,
|
|
1310
|
+
16).toLowerCase();
|
|
1311
|
+
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1312
1312
|
let code = safeString(input.code ?? input.Code, 80);
|
|
1313
1313
|
if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
|
|
1314
1314
|
code = 'Space';
|
|
@@ -1333,13 +1333,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1333
1333
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
1334
1334
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
1335
1335
|
altKey: input.altKey === true || input.AltKey === true,
|
|
1336
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1337
|
-
clipboardOperationId,
|
|
1338
|
-
operationId: clipboardOperationId,
|
|
1339
|
-
clipboardDirection,
|
|
1340
|
-
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1341
|
-
? clipboardManifest
|
|
1342
|
-
: null,
|
|
1336
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1337
|
+
clipboardOperationId,
|
|
1338
|
+
operationId: clipboardOperationId,
|
|
1339
|
+
clipboardDirection,
|
|
1340
|
+
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1341
|
+
? clipboardManifest
|
|
1342
|
+
: null,
|
|
1343
1343
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
1344
1344
|
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
1345
1345
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
@@ -2331,29 +2331,29 @@ export function createRemoteHub(options = {}) {
|
|
|
2331
2331
|
|
|
2332
2332
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
2333
2333
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
2334
|
-
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2335
|
-
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2336
|
-
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2337
|
-
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2338
|
-
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2339
|
-
heartbeatMs * 4);
|
|
2340
|
-
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2341
|
-
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2342
|
-
: clampNumber(
|
|
2343
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2344
|
-
defaultDeviceConnectionStaleMs,
|
|
2345
|
-
10 * 60 * 1000,
|
|
2346
|
-
defaultDeviceConnectionStaleMs);
|
|
2347
|
-
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2348
|
-
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2349
|
-
: clampNumber(
|
|
2350
|
-
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2351
|
-
1000,
|
|
2352
|
-
60000,
|
|
2353
|
-
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2354
|
-
const taskTimeoutMs = clampNumber(
|
|
2355
|
-
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2356
|
-
50,
|
|
2334
|
+
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2335
|
+
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2336
|
+
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2337
|
+
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2338
|
+
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2339
|
+
heartbeatMs * 4);
|
|
2340
|
+
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2341
|
+
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2342
|
+
: clampNumber(
|
|
2343
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2344
|
+
defaultDeviceConnectionStaleMs,
|
|
2345
|
+
10 * 60 * 1000,
|
|
2346
|
+
defaultDeviceConnectionStaleMs);
|
|
2347
|
+
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2348
|
+
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2349
|
+
: clampNumber(
|
|
2350
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2351
|
+
1000,
|
|
2352
|
+
60000,
|
|
2353
|
+
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2354
|
+
const taskTimeoutMs = clampNumber(
|
|
2355
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2356
|
+
50,
|
|
2357
2357
|
30 * 60 * 1000,
|
|
2358
2358
|
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
2359
2359
|
const inputAckTimeoutMs = clampNumber(
|
|
@@ -2416,18 +2416,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2416
2416
|
const audioSockets = new Map();
|
|
2417
2417
|
const fileSockets = new Map();
|
|
2418
2418
|
const allSockets = new Set();
|
|
2419
|
-
const agentBinaryIngressStates = new Set();
|
|
2420
|
-
const pendingCommandResultWaiters = new Map();
|
|
2421
|
-
const clipboardOperations = new Map();
|
|
2419
|
+
const agentBinaryIngressStates = new Set();
|
|
2420
|
+
const pendingCommandResultWaiters = new Map();
|
|
2421
|
+
const clipboardOperations = new Map();
|
|
2422
2422
|
const transportDiagnostics = new Map();
|
|
2423
2423
|
const transportDiagnosticStartingDevices = new Set();
|
|
2424
|
-
const duplicateDeviceLogAt = new Map();
|
|
2425
|
-
let server = null;
|
|
2426
|
-
let started = false;
|
|
2427
|
-
let deviceConnectionSweepTimer = null;
|
|
2428
|
-
let boundPort = requestedPort;
|
|
2429
|
-
let lastError = '';
|
|
2430
|
-
let hostTarget = null;
|
|
2424
|
+
const duplicateDeviceLogAt = new Map();
|
|
2425
|
+
let server = null;
|
|
2426
|
+
let started = false;
|
|
2427
|
+
let deviceConnectionSweepTimer = null;
|
|
2428
|
+
let boundPort = requestedPort;
|
|
2429
|
+
let lastError = '';
|
|
2430
|
+
let hostTarget = null;
|
|
2431
2431
|
let publicIPv4Cache = {
|
|
2432
2432
|
address: safeString(env.LIVEDESK_REMOTE_PUBLIC_IPV4 || env.LIVEDESK_PUBLIC_IPV4 || env.REMOTE_HUB_PUBLIC_IPV4, 64),
|
|
2433
2433
|
updatedAt: 0,
|
|
@@ -2748,14 +2748,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2748
2748
|
protocol: 'tcp-jsonl',
|
|
2749
2749
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2750
2750
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2751
|
-
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2752
|
-
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2753
|
-
heartbeatMs,
|
|
2754
|
-
deviceConnectionStaleMs,
|
|
2755
|
-
deviceConnectionSweepMs,
|
|
2756
|
-
taskTimeoutMs,
|
|
2757
|
-
managerPackage,
|
|
2758
|
-
managerVersion,
|
|
2751
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2752
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2753
|
+
heartbeatMs,
|
|
2754
|
+
deviceConnectionStaleMs,
|
|
2755
|
+
deviceConnectionSweepMs,
|
|
2756
|
+
taskTimeoutMs,
|
|
2757
|
+
managerPackage,
|
|
2758
|
+
managerVersion,
|
|
2759
2759
|
hostInstanceId,
|
|
2760
2760
|
agentPackage: '@livedesk/client',
|
|
2761
2761
|
agentEndpoint: routeInfo.endpoint,
|
|
@@ -3018,7 +3018,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3018
3018
|
return true;
|
|
3019
3019
|
}
|
|
3020
3020
|
|
|
3021
|
-
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3021
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3022
3022
|
if (!device?.deviceId) {
|
|
3023
3023
|
return;
|
|
3024
3024
|
}
|
|
@@ -3035,31 +3035,31 @@ export function createRemoteHub(options = {}) {
|
|
|
3035
3035
|
result: null,
|
|
3036
3036
|
error: safeString(error, 500) || 'command-result-unavailable'
|
|
3037
3037
|
});
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3042
|
-
if (!device?.deviceId) {
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3046
|
-
if (waiter.deviceId !== device.deviceId
|
|
3047
|
-
|| waiter.sessionId !== device.sessionId
|
|
3048
|
-
|| waiter.channel !== 'file') {
|
|
3049
|
-
continue;
|
|
3050
|
-
}
|
|
3051
|
-
pendingCommandResultWaiters.delete(waiterKey);
|
|
3052
|
-
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3053
|
-
waiter.resolve({
|
|
3054
|
-
ok: false,
|
|
3055
|
-
commandId: waiter.commandId,
|
|
3056
|
-
result: null,
|
|
3057
|
-
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3042
|
+
if (!device?.deviceId) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3046
|
+
if (waiter.deviceId !== device.deviceId
|
|
3047
|
+
|| waiter.sessionId !== device.sessionId
|
|
3048
|
+
|| waiter.channel !== 'file') {
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
pendingCommandResultWaiters.delete(waiterKey);
|
|
3052
|
+
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3053
|
+
waiter.resolve({
|
|
3054
|
+
ok: false,
|
|
3055
|
+
commandId: waiter.commandId,
|
|
3056
|
+
result: null,
|
|
3057
|
+
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
3063
3063
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3064
3064
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3065
3065
|
device?.deviceId,
|
|
@@ -3080,7 +3080,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3080
3080
|
return true;
|
|
3081
3081
|
}
|
|
3082
3082
|
|
|
3083
|
-
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3083
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
3084
3084
|
const normalizedCommandId = safeString(commandId, 128);
|
|
3085
3085
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
3086
3086
|
device?.deviceId,
|
|
@@ -3106,13 +3106,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3106
3106
|
error: 'command-result-wait-replaced'
|
|
3107
3107
|
});
|
|
3108
3108
|
}
|
|
3109
|
-
const waiterRecord = {
|
|
3110
|
-
deviceId: device.deviceId,
|
|
3111
|
-
sessionId: device.sessionId,
|
|
3112
|
-
commandId: normalizedCommandId,
|
|
3113
|
-
channel: safeString(metadata.channel, 24),
|
|
3114
|
-
operationId: safeString(metadata.operationId, 128),
|
|
3115
|
-
timer: null,
|
|
3109
|
+
const waiterRecord = {
|
|
3110
|
+
deviceId: device.deviceId,
|
|
3111
|
+
sessionId: device.sessionId,
|
|
3112
|
+
commandId: normalizedCommandId,
|
|
3113
|
+
channel: safeString(metadata.channel, 24),
|
|
3114
|
+
operationId: safeString(metadata.operationId, 128),
|
|
3115
|
+
timer: null,
|
|
3116
3116
|
deadlineImmediate: null,
|
|
3117
3117
|
resolve
|
|
3118
3118
|
};
|
|
@@ -3145,246 +3145,246 @@ export function createRemoteHub(options = {}) {
|
|
|
3145
3145
|
});
|
|
3146
3146
|
}, timeoutMs);
|
|
3147
3147
|
waiterRecord.timer.unref?.();
|
|
3148
|
-
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3153
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3154
|
-
const normalizedSessionId = safeString(sessionId, 160);
|
|
3155
|
-
const normalizedOperationId = safeString(operationId, 128);
|
|
3156
|
-
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3157
|
-
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3158
|
-
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3159
|
-
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3160
|
-
: '';
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3164
|
-
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3165
|
-
return false;
|
|
3166
|
-
}
|
|
3167
|
-
const cleanupCommand = operation.direction === 'paste'
|
|
3168
|
-
? 'clipboard.paste.cancel'
|
|
3169
|
-
: 'clipboard.copy.end';
|
|
3170
|
-
const commandId = crypto.randomUUID();
|
|
3171
|
-
try {
|
|
3172
|
-
writeJsonLine(device.fileSocket, {
|
|
3173
|
-
type: 'command',
|
|
3174
|
-
commandId,
|
|
3175
|
-
command: cleanupCommand,
|
|
3176
|
-
payload: {
|
|
3177
|
-
operationId: operation.operationId,
|
|
3178
|
-
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3179
|
-
},
|
|
3180
|
-
issuedAt: new Date().toISOString()
|
|
3181
|
-
});
|
|
3182
|
-
device.counters.commandsSent += 1;
|
|
3183
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3184
|
-
commandId,
|
|
3185
|
-
command: cleanupCommand,
|
|
3186
|
-
channel: 'file',
|
|
3187
|
-
operationId: operation.operationId,
|
|
3188
|
-
cleanup: true
|
|
3189
|
-
});
|
|
3190
|
-
return true;
|
|
3191
|
-
} catch {
|
|
3192
|
-
// The exact side channel is already closing. Native session
|
|
3193
|
-
// teardown remains the authoritative cleanup.
|
|
3194
|
-
return false;
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3199
|
-
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3200
|
-
let cleared = 0;
|
|
3201
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3202
|
-
if (operation.deviceId !== device?.deviceId
|
|
3203
|
-
|| operation.sessionId !== device?.sessionId
|
|
3204
|
-
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3205
|
-
continue;
|
|
3206
|
-
}
|
|
3207
|
-
queueClipboardAgentCleanup(device, operation, reason);
|
|
3208
|
-
clipboardOperations.delete(key);
|
|
3209
|
-
cleared += 1;
|
|
3210
|
-
}
|
|
3211
|
-
if (cleared > 0) {
|
|
3212
|
-
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3213
|
-
reason,
|
|
3214
|
-
ownerConnectionId: requestedOwner,
|
|
3215
|
-
count: cleared
|
|
3216
|
-
});
|
|
3217
|
-
}
|
|
3218
|
-
return cleared;
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3222
|
-
for (const [key, operation] of clipboardOperations) {
|
|
3223
|
-
const currentDevice = devices.get(operation.deviceId);
|
|
3224
|
-
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3225
|
-
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3226
|
-
const staleOwner = !currentDevice
|
|
3227
|
-
|| currentDevice.sessionId !== operation.sessionId
|
|
3228
|
-
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3229
|
-
if (expired || staleOwner) {
|
|
3230
|
-
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3231
|
-
queueClipboardAgentCleanup(
|
|
3232
|
-
currentDevice,
|
|
3233
|
-
operation,
|
|
3234
|
-
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3235
|
-
}
|
|
3236
|
-
clipboardOperations.delete(key);
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
|
|
3241
|
-
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3242
|
-
const capabilities = device?.capabilities || {};
|
|
3243
|
-
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3244
|
-
|| capabilities.clipboardText !== true) {
|
|
3245
|
-
return 'device-clipboard-text-unavailable';
|
|
3246
|
-
}
|
|
3247
|
-
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3248
|
-
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3249
|
-
}
|
|
3250
|
-
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3251
|
-
return 'device-clipboard-copy-unavailable';
|
|
3252
|
-
}
|
|
3253
|
-
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3254
|
-
return 'device-clipboard-paste-unavailable';
|
|
3255
|
-
}
|
|
3256
|
-
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3257
|
-
return 'device-clipboard-image-unavailable';
|
|
3258
|
-
}
|
|
3259
|
-
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3260
|
-
return 'device-clipboard-files-unavailable';
|
|
3261
|
-
}
|
|
3262
|
-
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3263
|
-
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3264
|
-
return 'device-clipboard-limits-unavailable';
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3268
|
-
return 'device-clipboard-chunk-limit-unavailable';
|
|
3269
|
-
}
|
|
3270
|
-
if (contentKind === 'image'
|
|
3271
|
-
&& (!Number.isSafeInteger(Number(
|
|
3272
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3273
|
-
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3274
|
-
return 'device-clipboard-limits-unavailable';
|
|
3275
|
-
}
|
|
3276
|
-
if (contentKind === 'files'
|
|
3277
|
-
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3278
|
-
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3279
|
-
return 'device-clipboard-limits-unavailable';
|
|
3280
|
-
}
|
|
3281
|
-
return '';
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
function clipboardManifestCapabilityError(device, manifest) {
|
|
3285
|
-
if (!manifest) {
|
|
3286
|
-
return '';
|
|
3287
|
-
}
|
|
3288
|
-
const capabilities = device?.capabilities || {};
|
|
3289
|
-
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3290
|
-
return 'device-clipboard-total-limit-exceeded';
|
|
3291
|
-
}
|
|
3292
|
-
if (manifest.contentKind === 'text'
|
|
3293
|
-
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3294
|
-
return 'device-clipboard-text-limit-exceeded';
|
|
3295
|
-
}
|
|
3296
|
-
if (manifest.contentKind === 'image'
|
|
3297
|
-
&& manifest.totalBytes > Number(
|
|
3298
|
-
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3299
|
-
return 'device-clipboard-image-limit-exceeded';
|
|
3300
|
-
}
|
|
3301
|
-
if (manifest.contentKind === 'files'
|
|
3302
|
-
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3303
|
-
return 'device-clipboard-file-count-limit-exceeded';
|
|
3304
|
-
}
|
|
3305
|
-
return '';
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3309
|
-
const device = devices.get(String(deviceId || ''));
|
|
3310
|
-
if (!device) {
|
|
3311
|
-
return { ok: false, error: 'device-not-found' };
|
|
3312
|
-
}
|
|
3313
|
-
if (device.synthetic === true) {
|
|
3314
|
-
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3315
|
-
}
|
|
3316
|
-
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3317
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3318
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3319
|
-
}
|
|
3320
|
-
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3321
|
-
return { ok: false, error: 'device-not-connected' };
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
const policy = getDevicePolicy(device);
|
|
3325
|
-
if (policy.accessMode === 'block-remote-access') {
|
|
3326
|
-
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3327
|
-
}
|
|
3328
|
-
if (policy.allowControl !== true) {
|
|
3329
|
-
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3330
|
-
}
|
|
3331
|
-
if (policy.allowClipboardText !== true) {
|
|
3332
|
-
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3333
|
-
}
|
|
3334
|
-
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3335
|
-
&& policy.allowFileTransfer !== true) {
|
|
3336
|
-
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
const direction = request.clipboardDirection
|
|
3340
|
-
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3341
|
-
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3342
|
-
if (capabilityError) {
|
|
3343
|
-
return { ok: false, error: capabilityError };
|
|
3344
|
-
}
|
|
3345
|
-
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3346
|
-
device,
|
|
3347
|
-
request.payload?.manifest || request.manifest);
|
|
3348
|
-
if (manifestCapabilityError) {
|
|
3349
|
-
return { ok: false, error: manifestCapabilityError };
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
const controlStream = getActiveControlStream(device);
|
|
3353
|
-
if (!controlStream
|
|
3354
|
-
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3355
|
-
|| !safeString(controlStream.commandId, 128)
|
|
3356
|
-
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3357
|
-
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3358
|
-
}
|
|
3359
|
-
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3360
|
-
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3361
|
-
}
|
|
3362
|
-
const binding = request.binding || request;
|
|
3363
|
-
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3364
|
-
!== safeString(device.sessionId, 160)
|
|
3365
|
-
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3366
|
-
!== safeString(controlStream.commandId, 128)) {
|
|
3367
|
-
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3368
|
-
}
|
|
3369
|
-
if (Number(binding.captureGeneration || 0)
|
|
3370
|
-
!== Number(controlStream.captureGeneration || 0)) {
|
|
3371
|
-
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3372
|
-
}
|
|
3373
|
-
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3374
|
-
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3375
|
-
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3376
|
-
}
|
|
3377
|
-
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3378
|
-
if (!hubConnectionId
|
|
3379
|
-
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3380
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
3381
|
-
}
|
|
3382
|
-
if (options.requireFileSocket === true
|
|
3383
|
-
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3384
|
-
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3385
|
-
}
|
|
3386
|
-
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3387
|
-
}
|
|
3148
|
+
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3153
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3154
|
+
const normalizedSessionId = safeString(sessionId, 160);
|
|
3155
|
+
const normalizedOperationId = safeString(operationId, 128);
|
|
3156
|
+
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3157
|
+
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3158
|
+
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3159
|
+
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3160
|
+
: '';
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3164
|
+
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
const cleanupCommand = operation.direction === 'paste'
|
|
3168
|
+
? 'clipboard.paste.cancel'
|
|
3169
|
+
: 'clipboard.copy.end';
|
|
3170
|
+
const commandId = crypto.randomUUID();
|
|
3171
|
+
try {
|
|
3172
|
+
writeJsonLine(device.fileSocket, {
|
|
3173
|
+
type: 'command',
|
|
3174
|
+
commandId,
|
|
3175
|
+
command: cleanupCommand,
|
|
3176
|
+
payload: {
|
|
3177
|
+
operationId: operation.operationId,
|
|
3178
|
+
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3179
|
+
},
|
|
3180
|
+
issuedAt: new Date().toISOString()
|
|
3181
|
+
});
|
|
3182
|
+
device.counters.commandsSent += 1;
|
|
3183
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3184
|
+
commandId,
|
|
3185
|
+
command: cleanupCommand,
|
|
3186
|
+
channel: 'file',
|
|
3187
|
+
operationId: operation.operationId,
|
|
3188
|
+
cleanup: true
|
|
3189
|
+
});
|
|
3190
|
+
return true;
|
|
3191
|
+
} catch {
|
|
3192
|
+
// The exact side channel is already closing. Native session
|
|
3193
|
+
// teardown remains the authoritative cleanup.
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3199
|
+
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3200
|
+
let cleared = 0;
|
|
3201
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3202
|
+
if (operation.deviceId !== device?.deviceId
|
|
3203
|
+
|| operation.sessionId !== device?.sessionId
|
|
3204
|
+
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
queueClipboardAgentCleanup(device, operation, reason);
|
|
3208
|
+
clipboardOperations.delete(key);
|
|
3209
|
+
cleared += 1;
|
|
3210
|
+
}
|
|
3211
|
+
if (cleared > 0) {
|
|
3212
|
+
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3213
|
+
reason,
|
|
3214
|
+
ownerConnectionId: requestedOwner,
|
|
3215
|
+
count: cleared
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return cleared;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3222
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3223
|
+
const currentDevice = devices.get(operation.deviceId);
|
|
3224
|
+
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3225
|
+
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3226
|
+
const staleOwner = !currentDevice
|
|
3227
|
+
|| currentDevice.sessionId !== operation.sessionId
|
|
3228
|
+
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3229
|
+
if (expired || staleOwner) {
|
|
3230
|
+
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3231
|
+
queueClipboardAgentCleanup(
|
|
3232
|
+
currentDevice,
|
|
3233
|
+
operation,
|
|
3234
|
+
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3235
|
+
}
|
|
3236
|
+
clipboardOperations.delete(key);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3242
|
+
const capabilities = device?.capabilities || {};
|
|
3243
|
+
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3244
|
+
|| capabilities.clipboardText !== true) {
|
|
3245
|
+
return 'device-clipboard-text-unavailable';
|
|
3246
|
+
}
|
|
3247
|
+
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3248
|
+
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3249
|
+
}
|
|
3250
|
+
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3251
|
+
return 'device-clipboard-copy-unavailable';
|
|
3252
|
+
}
|
|
3253
|
+
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3254
|
+
return 'device-clipboard-paste-unavailable';
|
|
3255
|
+
}
|
|
3256
|
+
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3257
|
+
return 'device-clipboard-image-unavailable';
|
|
3258
|
+
}
|
|
3259
|
+
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3260
|
+
return 'device-clipboard-files-unavailable';
|
|
3261
|
+
}
|
|
3262
|
+
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3263
|
+
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3264
|
+
return 'device-clipboard-limits-unavailable';
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3268
|
+
return 'device-clipboard-chunk-limit-unavailable';
|
|
3269
|
+
}
|
|
3270
|
+
if (contentKind === 'image'
|
|
3271
|
+
&& (!Number.isSafeInteger(Number(
|
|
3272
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3273
|
+
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3274
|
+
return 'device-clipboard-limits-unavailable';
|
|
3275
|
+
}
|
|
3276
|
+
if (contentKind === 'files'
|
|
3277
|
+
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3278
|
+
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3279
|
+
return 'device-clipboard-limits-unavailable';
|
|
3280
|
+
}
|
|
3281
|
+
return '';
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function clipboardManifestCapabilityError(device, manifest) {
|
|
3285
|
+
if (!manifest) {
|
|
3286
|
+
return '';
|
|
3287
|
+
}
|
|
3288
|
+
const capabilities = device?.capabilities || {};
|
|
3289
|
+
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3290
|
+
return 'device-clipboard-total-limit-exceeded';
|
|
3291
|
+
}
|
|
3292
|
+
if (manifest.contentKind === 'text'
|
|
3293
|
+
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3294
|
+
return 'device-clipboard-text-limit-exceeded';
|
|
3295
|
+
}
|
|
3296
|
+
if (manifest.contentKind === 'image'
|
|
3297
|
+
&& manifest.totalBytes > Number(
|
|
3298
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3299
|
+
return 'device-clipboard-image-limit-exceeded';
|
|
3300
|
+
}
|
|
3301
|
+
if (manifest.contentKind === 'files'
|
|
3302
|
+
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3303
|
+
return 'device-clipboard-file-count-limit-exceeded';
|
|
3304
|
+
}
|
|
3305
|
+
return '';
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3309
|
+
const device = devices.get(String(deviceId || ''));
|
|
3310
|
+
if (!device) {
|
|
3311
|
+
return { ok: false, error: 'device-not-found' };
|
|
3312
|
+
}
|
|
3313
|
+
if (device.synthetic === true) {
|
|
3314
|
+
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3315
|
+
}
|
|
3316
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3317
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3318
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3319
|
+
}
|
|
3320
|
+
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3321
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
const policy = getDevicePolicy(device);
|
|
3325
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
3326
|
+
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3327
|
+
}
|
|
3328
|
+
if (policy.allowControl !== true) {
|
|
3329
|
+
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3330
|
+
}
|
|
3331
|
+
if (policy.allowClipboardText !== true) {
|
|
3332
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3333
|
+
}
|
|
3334
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3335
|
+
&& policy.allowFileTransfer !== true) {
|
|
3336
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const direction = request.clipboardDirection
|
|
3340
|
+
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3341
|
+
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3342
|
+
if (capabilityError) {
|
|
3343
|
+
return { ok: false, error: capabilityError };
|
|
3344
|
+
}
|
|
3345
|
+
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3346
|
+
device,
|
|
3347
|
+
request.payload?.manifest || request.manifest);
|
|
3348
|
+
if (manifestCapabilityError) {
|
|
3349
|
+
return { ok: false, error: manifestCapabilityError };
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
const controlStream = getActiveControlStream(device);
|
|
3353
|
+
if (!controlStream
|
|
3354
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3355
|
+
|| !safeString(controlStream.commandId, 128)
|
|
3356
|
+
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3357
|
+
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3358
|
+
}
|
|
3359
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3360
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3361
|
+
}
|
|
3362
|
+
const binding = request.binding || request;
|
|
3363
|
+
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3364
|
+
!== safeString(device.sessionId, 160)
|
|
3365
|
+
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3366
|
+
!== safeString(controlStream.commandId, 128)) {
|
|
3367
|
+
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3368
|
+
}
|
|
3369
|
+
if (Number(binding.captureGeneration || 0)
|
|
3370
|
+
!== Number(controlStream.captureGeneration || 0)) {
|
|
3371
|
+
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3372
|
+
}
|
|
3373
|
+
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3374
|
+
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3375
|
+
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3376
|
+
}
|
|
3377
|
+
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3378
|
+
if (!hubConnectionId
|
|
3379
|
+
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3380
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
3381
|
+
}
|
|
3382
|
+
if (options.requireFileSocket === true
|
|
3383
|
+
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3384
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3385
|
+
}
|
|
3386
|
+
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3387
|
+
}
|
|
3388
3388
|
|
|
3389
3389
|
function inspectAnnexBH264(payload) {
|
|
3390
3390
|
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
@@ -4266,15 +4266,15 @@ export function createRemoteHub(options = {}) {
|
|
|
4266
4266
|
}
|
|
4267
4267
|
}
|
|
4268
4268
|
|
|
4269
|
-
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4270
|
-
const socket = device?.fileSocket;
|
|
4271
|
-
if (!socket) {
|
|
4272
|
-
return;
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4276
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4277
|
-
device.fileSocket = null;
|
|
4269
|
+
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4270
|
+
const socket = device?.fileSocket;
|
|
4271
|
+
if (!socket) {
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4276
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4277
|
+
device.fileSocket = null;
|
|
4278
4278
|
fileSockets.delete(socket);
|
|
4279
4279
|
try {
|
|
4280
4280
|
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
@@ -4288,14 +4288,14 @@ export function createRemoteHub(options = {}) {
|
|
|
4288
4288
|
}
|
|
4289
4289
|
}
|
|
4290
4290
|
|
|
4291
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4291
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4292
4292
|
const socket = device?.inputSocket;
|
|
4293
4293
|
if (!socket) {
|
|
4294
4294
|
return;
|
|
4295
4295
|
}
|
|
4296
4296
|
|
|
4297
|
-
const writeOwner = device.inputWriteOwner;
|
|
4298
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4297
|
+
const writeOwner = device.inputWriteOwner;
|
|
4298
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4299
4299
|
device.inputSocket = null;
|
|
4300
4300
|
device.inputWriteOwner = null;
|
|
4301
4301
|
device.inputSocketConnectionId = '';
|
|
@@ -4315,7 +4315,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4315
4315
|
}
|
|
4316
4316
|
}
|
|
4317
4317
|
|
|
4318
|
-
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4318
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4319
4319
|
const deviceId = inputSockets.get(socket);
|
|
4320
4320
|
inputSockets.delete(socket);
|
|
4321
4321
|
if (!deviceId) {
|
|
@@ -4327,8 +4327,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4327
4327
|
return;
|
|
4328
4328
|
}
|
|
4329
4329
|
|
|
4330
|
-
const writeOwner = device.inputWriteOwner;
|
|
4331
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4330
|
+
const writeOwner = device.inputWriteOwner;
|
|
4331
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4332
4332
|
device.inputSocket = null;
|
|
4333
4333
|
device.inputWriteOwner = null;
|
|
4334
4334
|
device.inputSocketConnectionId = '';
|
|
@@ -4373,7 +4373,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4373
4373
|
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
4374
4374
|
}
|
|
4375
4375
|
|
|
4376
|
-
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4376
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4377
4377
|
const deviceId = fileSockets.get(socket);
|
|
4378
4378
|
fileSockets.delete(socket);
|
|
4379
4379
|
if (!deviceId) {
|
|
@@ -4381,107 +4381,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4381
4381
|
}
|
|
4382
4382
|
|
|
4383
4383
|
const device = devices.get(deviceId);
|
|
4384
|
-
if (!device || device.fileSocket !== socket) {
|
|
4385
|
-
return;
|
|
4386
|
-
}
|
|
4387
|
-
|
|
4388
|
-
clearClipboardOperationsForDevice(device, reason);
|
|
4389
|
-
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4390
|
-
device.fileSocket = null;
|
|
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;
|
|
4391
4391
|
device.fileLastSeenAt = new Date().toISOString();
|
|
4392
|
-
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4393
|
-
}
|
|
4394
|
-
|
|
4395
|
-
function getDeviceConnectionHeartbeatMs(device) {
|
|
4396
|
-
return Date.parse(device?.lastStatusAt || '')
|
|
4397
|
-
|| Date.parse(device?.connectedAt || '')
|
|
4398
|
-
|| 0;
|
|
4399
|
-
}
|
|
4400
|
-
|
|
4401
|
-
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
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()) {
|
|
4402
4402
|
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4403
|
-
return false;
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4407
|
-
if (!lastHeartbeatMs) {
|
|
4408
|
-
return true;
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
|
-
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4415
|
-
const deviceId = safeString(device?.deviceId, 160);
|
|
4416
|
-
const sessionId = safeString(device?.sessionId, 160);
|
|
4417
|
-
const socket = device?.socket;
|
|
4418
|
-
if (!deviceId
|
|
4419
|
-
|| !sessionId
|
|
4420
|
-
|| !socket
|
|
4421
|
-
|| socket.destroyed
|
|
4422
|
-
|| devices.get(deviceId) !== device
|
|
4423
|
-
|| device.socket !== socket
|
|
4424
|
-
|| device.sessionId !== sessionId) {
|
|
4425
|
-
return false;
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
closeInputSocket(device, reason);
|
|
4429
|
-
closeFrameSocket(device, reason);
|
|
4430
|
-
closeAudioSocket(device, reason);
|
|
4431
|
-
closeFileSocket(device, reason);
|
|
4432
|
-
try {
|
|
4433
|
-
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4434
|
-
} catch {
|
|
4435
|
-
// The stale connection is retired below even if the final notice fails.
|
|
4436
|
-
}
|
|
4437
|
-
detachSocket(socket, reason);
|
|
4438
|
-
try {
|
|
4439
|
-
socket.destroy?.();
|
|
4440
|
-
} catch {
|
|
4441
|
-
// detachSocket already made the exact session terminal.
|
|
4442
|
-
}
|
|
4443
|
-
return true;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4447
|
-
pruneClipboardOperations(nowMs);
|
|
4448
|
-
let retired = 0;
|
|
4449
|
-
for (const device of devices.values()) {
|
|
4450
|
-
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4451
|
-
continue;
|
|
4452
|
-
}
|
|
4453
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4454
|
-
continue;
|
|
4455
|
-
}
|
|
4456
|
-
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4457
|
-
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4458
|
-
retired += 1;
|
|
4459
|
-
logWarn(
|
|
4460
|
-
'remote',
|
|
4461
|
-
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4462
|
-
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4463
|
-
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
return retired;
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
function scheduleDeviceConnectionSweep() {
|
|
4470
|
-
if (!started || deviceConnectionSweepTimer) {
|
|
4471
|
-
return;
|
|
4472
|
-
}
|
|
4473
|
-
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4474
|
-
deviceConnectionSweepTimer = null;
|
|
4475
|
-
if (!started) {
|
|
4476
|
-
return;
|
|
4477
|
-
}
|
|
4478
|
-
sweepStaleDeviceConnections();
|
|
4479
|
-
scheduleDeviceConnectionSweep();
|
|
4480
|
-
}, deviceConnectionSweepMs);
|
|
4481
|
-
deviceConnectionSweepTimer.unref?.();
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
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) {
|
|
4485
4485
|
const retryAfterMs = Math.max(heartbeatMs, duplicateDeviceRetryAfterMs);
|
|
4486
4486
|
writeJsonLine(socket, {
|
|
4487
4487
|
type: 'disconnect',
|
|
@@ -4933,20 +4933,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4933
4933
|
return device;
|
|
4934
4934
|
}
|
|
4935
4935
|
|
|
4936
|
-
function attachInputSocket(socket, hello) {
|
|
4937
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4938
|
-
const device = devices.get(deviceId);
|
|
4936
|
+
function attachInputSocket(socket, hello) {
|
|
4937
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4938
|
+
const device = devices.get(deviceId);
|
|
4939
4939
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4940
4940
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4941
4941
|
socket.destroy();
|
|
4942
|
-
return null;
|
|
4943
|
-
}
|
|
4944
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4945
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4946
|
-
return null;
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
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');
|
|
4950
4950
|
|
|
4951
4951
|
const now = new Date().toISOString();
|
|
4952
4952
|
const inputSocketConnectionId = crypto.randomUUID();
|
|
@@ -5060,20 +5060,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5060
5060
|
return device;
|
|
5061
5061
|
}
|
|
5062
5062
|
|
|
5063
|
-
function attachFileSocket(socket, hello) {
|
|
5064
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5065
|
-
const device = devices.get(deviceId);
|
|
5063
|
+
function attachFileSocket(socket, hello) {
|
|
5064
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5065
|
+
const device = devices.get(deviceId);
|
|
5066
5066
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
5067
5067
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
5068
5068
|
socket.destroy();
|
|
5069
|
-
return null;
|
|
5070
|
-
}
|
|
5071
|
-
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5072
|
-
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5073
|
-
return null;
|
|
5074
|
-
}
|
|
5075
|
-
|
|
5076
|
-
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
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');
|
|
5077
5077
|
const now = new Date().toISOString();
|
|
5078
5078
|
device.fileSocket = socket;
|
|
5079
5079
|
device.fileConnectedAt = now;
|
|
@@ -8376,9 +8376,9 @@ export function createRemoteHub(options = {}) {
|
|
|
8376
8376
|
started = true;
|
|
8377
8377
|
boundPort = candidateServer.address()?.port || port;
|
|
8378
8378
|
lastError = '';
|
|
8379
|
-
logEvent('remote', `
|
|
8379
|
+
logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
8380
8380
|
if (host === '0.0.0.0' || host === '::') {
|
|
8381
|
-
logWarn('remote', '
|
|
8381
|
+
logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
|
|
8382
8382
|
}
|
|
8383
8383
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8384
8384
|
resolve();
|
|
@@ -8396,14 +8396,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8396
8396
|
await listenOnPort(requestedPort);
|
|
8397
8397
|
try {
|
|
8398
8398
|
await relayControl?.start?.();
|
|
8399
|
-
} catch {
|
|
8400
|
-
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8401
|
-
}
|
|
8402
|
-
scheduleDeviceConnectionSweep();
|
|
8403
|
-
} catch (error) {
|
|
8404
|
-
if (error?.code === 'EADDRINUSE') {
|
|
8405
|
-
const conflict = new Error(
|
|
8406
|
-
`
|
|
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.`);
|
|
8407
8407
|
conflict.code = 'EADDRINUSE';
|
|
8408
8408
|
conflict.port = requestedPort;
|
|
8409
8409
|
conflict.cause = error;
|
|
@@ -8414,16 +8414,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8414
8414
|
}
|
|
8415
8415
|
|
|
8416
8416
|
return getStatus({ includeSecrets: false });
|
|
8417
|
-
}
|
|
8418
|
-
|
|
8419
|
-
async function close() {
|
|
8420
|
-
if (deviceConnectionSweepTimer) {
|
|
8421
|
-
clearTimeout(deviceConnectionSweepTimer);
|
|
8422
|
-
deviceConnectionSweepTimer = null;
|
|
8423
|
-
}
|
|
8424
|
-
for (const state of [...agentBinaryIngressStates]) {
|
|
8425
|
-
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8426
|
-
}
|
|
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
|
+
}
|
|
8427
8427
|
|
|
8428
8428
|
for (const device of devices.values()) {
|
|
8429
8429
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
@@ -8558,12 +8558,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8558
8558
|
};
|
|
8559
8559
|
}
|
|
8560
8560
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
8567
8567
|
const requiredPermission = commandName === 'input.control'
|
|
8568
8568
|
? 'allowControl'
|
|
8569
8569
|
: commandName === 'system.power'
|
|
@@ -8599,16 +8599,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8599
8599
|
result: { ok: true, synthetic: true },
|
|
8600
8600
|
error: ''
|
|
8601
8601
|
});
|
|
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
|
-
}
|
|
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
|
+
}
|
|
8612
8612
|
|
|
8613
8613
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8614
8614
|
const payload = {
|
|
@@ -8621,300 +8621,300 @@ export function createRemoteHub(options = {}) {
|
|
|
8621
8621
|
|
|
8622
8622
|
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
8623
8623
|
&& device.fileSocket
|
|
8624
|
-
&& !device.fileSocket.destroyed
|
|
8625
|
-
? device.fileSocket
|
|
8626
|
-
: null;
|
|
8627
|
-
try {
|
|
8628
|
-
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8629
|
-
} catch {
|
|
8630
|
-
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8631
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8632
|
-
}
|
|
8633
|
-
device.counters.commandsSent += 1;
|
|
8634
|
-
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8635
|
-
commandId,
|
|
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,
|
|
8636
8636
|
command: payload.command,
|
|
8637
8637
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8638
8638
|
});
|
|
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) {
|
|
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) {
|
|
8905
8905
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8906
8906
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8907
8907
|
: null;
|
|
8908
8908
|
let updated = 0;
|
|
8909
8909
|
let total = 0;
|
|
8910
|
-
for (const device of devices.values()) {
|
|
8911
|
-
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8912
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8913
|
-
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8914
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8915
|
-
continue;
|
|
8916
|
-
}
|
|
8917
|
-
total += 1;
|
|
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;
|
|
8918
8918
|
const effectivePolicy = getDevicePolicy(device);
|
|
8919
8919
|
if (writeJsonLine(device.socket, {
|
|
8920
8920
|
type: 'policy.update',
|
|
@@ -8933,14 +8933,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8933
8933
|
const device = devices.get(String(deviceId || ''));
|
|
8934
8934
|
const denied = policyError(device);
|
|
8935
8935
|
if (denied) return { ok: false, error: denied };
|
|
8936
|
-
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8937
|
-
if (device.synthetic !== true
|
|
8938
|
-
&& device.connected
|
|
8939
|
-
&& !isDeviceConnectionFresh(device)) {
|
|
8940
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8941
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8942
|
-
}
|
|
8943
|
-
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
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) {
|
|
8944
8944
|
return { ok: false, error: 'device-not-connected' };
|
|
8945
8945
|
}
|
|
8946
8946
|
|
|
@@ -9036,21 +9036,21 @@ export function createRemoteHub(options = {}) {
|
|
|
9036
9036
|
return device.inputWriteOwner;
|
|
9037
9037
|
}
|
|
9038
9038
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
9054
9054
|
|
|
9055
9055
|
const normalized = normalizeRemoteInputEvent(input);
|
|
9056
9056
|
if (!normalized.type) {
|
|
@@ -9116,51 +9116,51 @@ export function createRemoteHub(options = {}) {
|
|
|
9116
9116
|
|| device.capabilities?.accessibilityPermission === false) {
|
|
9117
9117
|
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
9118
9118
|
}
|
|
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
|
-
}
|
|
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
|
+
}
|
|
9164
9164
|
|
|
9165
9165
|
const controlStream = getActiveControlStream(device);
|
|
9166
9166
|
if (!controlStream
|
|
@@ -9196,32 +9196,32 @@ export function createRemoteHub(options = {}) {
|
|
|
9196
9196
|
};
|
|
9197
9197
|
}
|
|
9198
9198
|
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
9199
|
-
if (!Number.isInteger(normalized.monitorIndex)
|
|
9200
|
-
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9199
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
9200
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9201
9201
|
return {
|
|
9202
9202
|
ok: false,
|
|
9203
9203
|
error: 'STALE_CONTROL_MONITOR',
|
|
9204
|
-
activeMonitorIndex
|
|
9205
|
-
};
|
|
9206
|
-
}
|
|
9207
|
-
|
|
9208
|
-
let clipboardOperationKey = '';
|
|
9209
|
-
if (normalizedInputType === 'clipboard.paste') {
|
|
9210
|
-
clipboardOperationKey = buildClipboardOperationKey(
|
|
9211
|
-
device.deviceId,
|
|
9212
|
-
device.sessionId,
|
|
9213
|
-
normalized.clipboardOperationId);
|
|
9214
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9215
|
-
if (!operation
|
|
9216
|
-
|| operation.direction !== 'paste'
|
|
9217
|
-
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9218
|
-
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9219
|
-
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9220
|
-
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9221
|
-
|| operation.status !== 'ready') {
|
|
9222
|
-
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9223
|
-
}
|
|
9224
|
-
}
|
|
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
|
+
}
|
|
9225
9225
|
|
|
9226
9226
|
const inputSocket = device.inputSocket;
|
|
9227
9227
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9238,17 +9238,17 @@ export function createRemoteHub(options = {}) {
|
|
|
9238
9238
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
9239
9239
|
const bindingChanged = previousInputBindingKey
|
|
9240
9240
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
9241
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9242
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9243
|
-
if (ownerChanged) {
|
|
9244
|
-
clearClipboardOperationsForDevice(
|
|
9245
|
-
device,
|
|
9246
|
-
'browser-input-owner-changed',
|
|
9247
|
-
previousOwnerConnectionId);
|
|
9248
|
-
} else if (bindingChanged || writeBindingChanged) {
|
|
9249
|
-
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9250
|
-
}
|
|
9251
|
-
const resetSent = inputWriteOwner.replaceBinding(
|
|
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(
|
|
9252
9252
|
activeInputBindingKey,
|
|
9253
9253
|
buildRemoteInputResetMessage(
|
|
9254
9254
|
device,
|
|
@@ -9278,16 +9278,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9278
9278
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
9279
9279
|
}
|
|
9280
9280
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
9281
|
-
device.counters.commandsSent += 1;
|
|
9282
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
9283
|
-
if (clipboardOperationKey) {
|
|
9284
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9285
|
-
if (operation) {
|
|
9286
|
-
operation.status = 'pasting';
|
|
9287
|
-
operation.touchedAt = Date.now();
|
|
9288
|
-
}
|
|
9289
|
-
}
|
|
9290
|
-
return {
|
|
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 {
|
|
9291
9291
|
ok: true,
|
|
9292
9292
|
inputSocket: true,
|
|
9293
9293
|
sessionId: device.sessionId,
|
|
@@ -9322,7 +9322,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9322
9322
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
9323
9323
|
}
|
|
9324
9324
|
});
|
|
9325
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9325
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9326
9326
|
schedulePendingInputFallback(device, {
|
|
9327
9327
|
commandId: fallback.commandId,
|
|
9328
9328
|
inputSeq: normalized.inputSeq,
|
|
@@ -9334,16 +9334,16 @@ export function createRemoteHub(options = {}) {
|
|
|
9334
9334
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
9335
9335
|
hubForwardedAtEpochMs,
|
|
9336
9336
|
fallback: true,
|
|
9337
|
-
timeoutTimer: null
|
|
9338
|
-
});
|
|
9339
|
-
}
|
|
9340
|
-
if (fallback.ok && clipboardOperationKey) {
|
|
9341
|
-
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9342
|
-
if (operation) {
|
|
9343
|
-
operation.status = 'pasting';
|
|
9344
|
-
operation.touchedAt = Date.now();
|
|
9345
|
-
}
|
|
9346
|
-
}
|
|
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
|
+
}
|
|
9347
9347
|
return fallback.ok
|
|
9348
9348
|
? {
|
|
9349
9349
|
...fallback,
|
|
@@ -9363,15 +9363,15 @@ export function createRemoteHub(options = {}) {
|
|
|
9363
9363
|
};
|
|
9364
9364
|
}
|
|
9365
9365
|
|
|
9366
|
-
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9366
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9367
9367
|
const device = devices.get(String(deviceId || ''));
|
|
9368
9368
|
const owner = safeString(ownerConnectionId, 128);
|
|
9369
|
-
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9370
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
9371
|
-
}
|
|
9372
|
-
|
|
9373
|
-
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9374
|
-
device.inputOwnerConnectionId = '';
|
|
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 = '';
|
|
9375
9375
|
device.inputOwnerBindingKey = '';
|
|
9376
9376
|
const inputSocket = device.inputSocket;
|
|
9377
9377
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -9415,12 +9415,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9415
9415
|
};
|
|
9416
9416
|
const results = targets.map(deviceId => {
|
|
9417
9417
|
const device = devices.get(deviceId);
|
|
9418
|
-
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9419
|
-
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9420
|
-
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9421
|
-
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9422
|
-
}
|
|
9423
|
-
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
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) {
|
|
9424
9424
|
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9425
9425
|
}
|
|
9426
9426
|
return writeJsonLine(device.socket, payload)
|
|
@@ -9452,7 +9452,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9452
9452
|
const denied = policyError(device, 'allowAgent');
|
|
9453
9453
|
if (denied) return { ok: false, error: denied };
|
|
9454
9454
|
|
|
9455
|
-
if (device?.synthetic === true && device.connected) {
|
|
9455
|
+
if (device?.synthetic === true && device.connected) {
|
|
9456
9456
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
9457
9457
|
if (!instruction) {
|
|
9458
9458
|
return { ok: false, error: 'missing-instruction' };
|
|
@@ -9512,14 +9512,14 @@ export function createRemoteHub(options = {}) {
|
|
|
9512
9512
|
error: '',
|
|
9513
9513
|
synthetic: true
|
|
9514
9514
|
});
|
|
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) {
|
|
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) {
|
|
9523
9523
|
return { ok: false, error: 'device-not-connected' };
|
|
9524
9524
|
}
|
|
9525
9525
|
|
|
@@ -10380,17 +10380,17 @@ export function createRemoteHub(options = {}) {
|
|
|
10380
10380
|
};
|
|
10381
10381
|
}
|
|
10382
10382
|
|
|
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) {
|
|
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) {
|
|
10394
10394
|
return {
|
|
10395
10395
|
ok: false,
|
|
10396
10396
|
error: 'LIVE_CAPTURE_PAUSED',
|
|
@@ -10821,15 +10821,15 @@ export function createRemoteHub(options = {}) {
|
|
|
10821
10821
|
};
|
|
10822
10822
|
}
|
|
10823
10823
|
|
|
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;
|
|
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;
|
|
10833
10833
|
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24).toLowerCase();
|
|
10834
10834
|
const requestedStreamId = safeString(options.streamId, 128);
|
|
10835
10835
|
const purposeStreams = !stopAll && !requestedStreamId && streamPurpose && device
|
|
@@ -11430,18 +11430,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11430
11430
|
};
|
|
11431
11431
|
}
|
|
11432
11432
|
|
|
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) {
|
|
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) {
|
|
11445
11445
|
return { ok: false, error: 'device-not-connected' };
|
|
11446
11446
|
}
|
|
11447
11447
|
const denied = policyError(device, 'allowRemoteAudio');
|
|
@@ -11518,18 +11518,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11518
11518
|
return { ok: true, commandId, streamId };
|
|
11519
11519
|
}
|
|
11520
11520
|
|
|
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) {
|
|
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) {
|
|
11533
11533
|
return { ok: false, error: 'device-not-connected' };
|
|
11534
11534
|
}
|
|
11535
11535
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
@@ -11742,14 +11742,14 @@ export function createRemoteHub(options = {}) {
|
|
|
11742
11742
|
replacementTimerCount: device.liveStreamReplacementTimers instanceof Map
|
|
11743
11743
|
? device.liveStreamReplacementTimers.size
|
|
11744
11744
|
: 0,
|
|
11745
|
-
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11746
|
-
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11747
|
-
&& waiter.sessionId === device.sessionId)
|
|
11748
|
-
.length,
|
|
11749
|
-
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11750
|
-
.filter(operation => operation.deviceId === device.deviceId
|
|
11751
|
-
&& operation.sessionId === device.sessionId)
|
|
11752
|
-
.length,
|
|
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,
|
|
11753
11753
|
retainedPayloadCount: retainedPayloads.size,
|
|
11754
11754
|
retainedPayloadBytes: [...retainedPayloads].reduce(
|
|
11755
11755
|
(total, payload) => total + payload.length,
|
|
@@ -11759,9 +11759,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11759
11759
|
&& snapshot.stopPendingCount === 0
|
|
11760
11760
|
&& snapshot.pendingStartCount === 0
|
|
11761
11761
|
&& snapshot.pendingStopPromiseCount === 0
|
|
11762
|
-
&& snapshot.replacementTimerCount === 0
|
|
11763
|
-
&& snapshot.commandResultWaiterCount === 0
|
|
11764
|
-
&& snapshot.clipboardOperationCount === 0
|
|
11762
|
+
&& snapshot.replacementTimerCount === 0
|
|
11763
|
+
&& snapshot.commandResultWaiterCount === 0
|
|
11764
|
+
&& snapshot.clipboardOperationCount === 0
|
|
11765
11765
|
&& snapshot.retainedPayloadCount === 0
|
|
11766
11766
|
&& snapshot.retainedPayloadBytes === 0;
|
|
11767
11767
|
return snapshot;
|
|
@@ -11773,9 +11773,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11773
11773
|
'stopPendingCount',
|
|
11774
11774
|
'pendingStartCount',
|
|
11775
11775
|
'pendingStopPromiseCount',
|
|
11776
|
-
'replacementTimerCount',
|
|
11777
|
-
'commandResultWaiterCount',
|
|
11778
|
-
'clipboardOperationCount',
|
|
11776
|
+
'replacementTimerCount',
|
|
11777
|
+
'commandResultWaiterCount',
|
|
11778
|
+
'clipboardOperationCount',
|
|
11779
11779
|
'retainedPayloadCount',
|
|
11780
11780
|
'retainedPayloadBytes'
|
|
11781
11781
|
]) {
|
|
@@ -11788,10 +11788,10 @@ export function createRemoteHub(options = {}) {
|
|
|
11788
11788
|
stopPendingCount: 0,
|
|
11789
11789
|
pendingStartCount: 0,
|
|
11790
11790
|
pendingStopPromiseCount: 0,
|
|
11791
|
-
replacementTimerCount: 0,
|
|
11792
|
-
commandResultWaiterCount: 0,
|
|
11793
|
-
clipboardOperationCount: 0,
|
|
11794
|
-
retainedPayloadCount: 0,
|
|
11791
|
+
replacementTimerCount: 0,
|
|
11792
|
+
commandResultWaiterCount: 0,
|
|
11793
|
+
clipboardOperationCount: 0,
|
|
11794
|
+
retainedPayloadCount: 0,
|
|
11795
11795
|
retainedPayloadBytes: 0
|
|
11796
11796
|
});
|
|
11797
11797
|
return {
|
|
@@ -11831,9 +11831,9 @@ export function createRemoteHub(options = {}) {
|
|
|
11831
11831
|
listDeviceFrames,
|
|
11832
11832
|
disconnectDevice,
|
|
11833
11833
|
assignDeviceSlot,
|
|
11834
|
-
sendCommand,
|
|
11835
|
-
sendClipboardCommand,
|
|
11836
|
-
refreshDevicePolicies,
|
|
11834
|
+
sendCommand,
|
|
11835
|
+
sendClipboardCommand,
|
|
11836
|
+
refreshDevicePolicies,
|
|
11837
11837
|
sendLegacyClientUpdate,
|
|
11838
11838
|
sendInputControl,
|
|
11839
11839
|
releaseInputOwner,
|