@livedesk/hub 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/agents/agent-result-enrichment.js +66 -66
- package/src/agents/codex-agent-runtime.js +4 -4
- package/src/console-relay.js +424 -424
- package/src/filesystem/roots.js +20 -2
- package/src/live-desk-update.js +24 -24
- package/src/remote-clipboard-contract.mjs +482 -0
- package/src/remote-hub.js +1137 -216
- package/src/server.js +227 -99
- package/src/settings/effective-device-policy.js +31 -9
- package/src/settings/settings-schema.js +3 -2
package/src/remote-hub.js
CHANGED
|
@@ -3,10 +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';
|
|
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';
|
|
10
18
|
|
|
11
19
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
12
20
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
@@ -228,12 +236,13 @@ const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
|
228
236
|
]);
|
|
229
237
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
230
238
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
231
|
-
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
232
|
-
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
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;
|
|
237
246
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_BYTES = 12 * 1024 * 1024;
|
|
238
247
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_AGE_MS = 120;
|
|
239
248
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
@@ -797,6 +806,25 @@ function normalizeIncomingFrameModeProfile(value) {
|
|
|
797
806
|
};
|
|
798
807
|
}
|
|
799
808
|
|
|
809
|
+
function deviceExplicitlyRejectsFrameMode(device, frameMode) {
|
|
810
|
+
const requestedMode = safeString(frameMode, 80).toLowerCase();
|
|
811
|
+
if (!requestedMode) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const advertisedModes = [
|
|
816
|
+
...(Array.isArray(device?.frameModes) ? device.frameModes : []),
|
|
817
|
+
...(Array.isArray(device?.frameProtocol?.modes) ? device.frameProtocol.modes : []),
|
|
818
|
+
...(Array.isArray(device?.capabilities?.frameModes) ? device.capabilities.frameModes : []),
|
|
819
|
+
...(Array.isArray(device?.capabilities?.frameProtocol?.modes)
|
|
820
|
+
? device.capabilities.frameProtocol.modes
|
|
821
|
+
: [])
|
|
822
|
+
];
|
|
823
|
+
return advertisedModes.some(profile =>
|
|
824
|
+
safeString(profile?.mode || profile?.frameMode, 80).toLowerCase() === requestedMode
|
|
825
|
+
&& profile?.implemented === false);
|
|
826
|
+
}
|
|
827
|
+
|
|
800
828
|
function isLoopbackHost(value) {
|
|
801
829
|
const host = String(value || '').trim().toLowerCase();
|
|
802
830
|
return host === 'localhost'
|
|
@@ -1166,7 +1194,9 @@ function normalizeRemoteKeyboardKey(value) {
|
|
|
1166
1194
|
|
|
1167
1195
|
function normalizeRemoteKeyboardText(value) {
|
|
1168
1196
|
const raw = String(value ?? '').replace(/\0/g, '');
|
|
1169
|
-
|
|
1197
|
+
// Remote text is input data, not a label. Preserve leading/trailing spaces
|
|
1198
|
+
// and IME whitespace while keeping every ordered message bounded.
|
|
1199
|
+
return raw.slice(0, 256);
|
|
1170
1200
|
}
|
|
1171
1201
|
|
|
1172
1202
|
function safeTaskData(value) {
|
|
@@ -1248,7 +1278,7 @@ function readRawRemoteInputMonitorIndex(input) {
|
|
|
1248
1278
|
return undefined;
|
|
1249
1279
|
}
|
|
1250
1280
|
|
|
1251
|
-
function normalizeRemoteInputEvent(value = {}) {
|
|
1281
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
1252
1282
|
const input = value && typeof value === 'object' ? value : {};
|
|
1253
1283
|
const type = safeString(input.type || input.Type, 48);
|
|
1254
1284
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
@@ -1269,7 +1299,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1269
1299
|
.filter(Boolean))]
|
|
1270
1300
|
.slice(0, 64)
|
|
1271
1301
|
: null;
|
|
1272
|
-
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1302
|
+
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1303
|
+
const clipboardOperationId = safeString(
|
|
1304
|
+
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1305
|
+
128);
|
|
1306
|
+
const clipboardDirection = safeString(
|
|
1307
|
+
input.clipboardDirection || input.ClipboardDirection,
|
|
1308
|
+
16).toLowerCase();
|
|
1309
|
+
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1273
1310
|
let code = safeString(input.code ?? input.Code, 80);
|
|
1274
1311
|
if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
|
|
1275
1312
|
code = 'Space';
|
|
@@ -1294,7 +1331,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1294
1331
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
1295
1332
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
1296
1333
|
altKey: input.altKey === true || input.AltKey === true,
|
|
1297
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1334
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1335
|
+
clipboardOperationId,
|
|
1336
|
+
operationId: clipboardOperationId,
|
|
1337
|
+
clipboardDirection,
|
|
1338
|
+
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1339
|
+
? clipboardManifest
|
|
1340
|
+
: null,
|
|
1298
1341
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
1299
1342
|
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
1300
1343
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
@@ -1716,6 +1759,26 @@ function writeJsonLine(socket, payload) {
|
|
|
1716
1759
|
return true;
|
|
1717
1760
|
}
|
|
1718
1761
|
|
|
1762
|
+
function writeJsonLineAndClose(socket, payload) {
|
|
1763
|
+
if (!socket || socket.destroyed) {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (socket.__remoteHubWebSocket === true) {
|
|
1768
|
+
const written = writeJsonLine(socket, payload);
|
|
1769
|
+
socket.destroy();
|
|
1770
|
+
return written;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
try {
|
|
1774
|
+
socket.end(`${JSON.stringify(payload)}\n`);
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch {
|
|
1777
|
+
socket.destroy();
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1719
1782
|
function isRemoteHubWebSocketUpgradeStart(buffer) {
|
|
1720
1783
|
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
1721
1784
|
return false;
|
|
@@ -2266,12 +2329,29 @@ export function createRemoteHub(options = {}) {
|
|
|
2266
2329
|
|
|
2267
2330
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
2268
2331
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
2269
|
-
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2270
|
-
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2271
|
-
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
|
|
2332
|
+
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2333
|
+
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2334
|
+
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2335
|
+
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2336
|
+
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2337
|
+
heartbeatMs * 4);
|
|
2338
|
+
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2339
|
+
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2340
|
+
: clampNumber(
|
|
2341
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2342
|
+
defaultDeviceConnectionStaleMs,
|
|
2343
|
+
10 * 60 * 1000,
|
|
2344
|
+
defaultDeviceConnectionStaleMs);
|
|
2345
|
+
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2346
|
+
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2347
|
+
: clampNumber(
|
|
2348
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2349
|
+
1000,
|
|
2350
|
+
60000,
|
|
2351
|
+
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2352
|
+
const taskTimeoutMs = clampNumber(
|
|
2353
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2354
|
+
50,
|
|
2275
2355
|
30 * 60 * 1000,
|
|
2276
2356
|
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
2277
2357
|
const inputAckTimeoutMs = clampNumber(
|
|
@@ -2334,16 +2414,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2334
2414
|
const audioSockets = new Map();
|
|
2335
2415
|
const fileSockets = new Map();
|
|
2336
2416
|
const allSockets = new Set();
|
|
2337
|
-
const agentBinaryIngressStates = new Set();
|
|
2338
|
-
const pendingCommandResultWaiters = new Map();
|
|
2417
|
+
const agentBinaryIngressStates = new Set();
|
|
2418
|
+
const pendingCommandResultWaiters = new Map();
|
|
2419
|
+
const clipboardOperations = new Map();
|
|
2339
2420
|
const transportDiagnostics = new Map();
|
|
2340
2421
|
const transportDiagnosticStartingDevices = new Set();
|
|
2341
|
-
const duplicateDeviceLogAt = new Map();
|
|
2342
|
-
let server = null;
|
|
2343
|
-
let started = false;
|
|
2344
|
-
let
|
|
2345
|
-
let
|
|
2346
|
-
let
|
|
2422
|
+
const duplicateDeviceLogAt = new Map();
|
|
2423
|
+
let server = null;
|
|
2424
|
+
let started = false;
|
|
2425
|
+
let deviceConnectionSweepTimer = null;
|
|
2426
|
+
let boundPort = requestedPort;
|
|
2427
|
+
let lastError = '';
|
|
2428
|
+
let hostTarget = null;
|
|
2347
2429
|
let publicIPv4Cache = {
|
|
2348
2430
|
address: safeString(env.LIVEDESK_REMOTE_PUBLIC_IPV4 || env.LIVEDESK_PUBLIC_IPV4 || env.REMOTE_HUB_PUBLIC_IPV4, 64),
|
|
2349
2431
|
updatedAt: 0,
|
|
@@ -2664,12 +2746,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2664
2746
|
protocol: 'tcp-jsonl',
|
|
2665
2747
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2666
2748
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2667
|
-
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2668
|
-
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2669
|
-
heartbeatMs,
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2749
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2750
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2751
|
+
heartbeatMs,
|
|
2752
|
+
deviceConnectionStaleMs,
|
|
2753
|
+
deviceConnectionSweepMs,
|
|
2754
|
+
taskTimeoutMs,
|
|
2755
|
+
managerPackage,
|
|
2756
|
+
managerVersion,
|
|
2673
2757
|
hostInstanceId,
|
|
2674
2758
|
agentPackage: '@livedesk/client',
|
|
2675
2759
|
agentEndpoint: routeInfo.endpoint,
|
|
@@ -2932,7 +3016,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2932
3016
|
return true;
|
|
2933
3017
|
}
|
|
2934
3018
|
|
|
2935
|
-
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3019
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
2936
3020
|
if (!device?.deviceId) {
|
|
2937
3021
|
return;
|
|
2938
3022
|
}
|
|
@@ -2949,10 +3033,31 @@ export function createRemoteHub(options = {}) {
|
|
|
2949
3033
|
result: null,
|
|
2950
3034
|
error: safeString(error, 500) || 'command-result-unavailable'
|
|
2951
3035
|
});
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
function
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3040
|
+
if (!device?.deviceId) {
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3044
|
+
if (waiter.deviceId !== device.deviceId
|
|
3045
|
+
|| waiter.sessionId !== device.sessionId
|
|
3046
|
+
|| waiter.channel !== 'file') {
|
|
3047
|
+
continue;
|
|
3048
|
+
}
|
|
3049
|
+
pendingCommandResultWaiters.delete(waiterKey);
|
|
3050
|
+
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3051
|
+
waiter.resolve({
|
|
3052
|
+
ok: false,
|
|
3053
|
+
commandId: waiter.commandId,
|
|
3054
|
+
result: null,
|
|
3055
|
+
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
2956
3061
|
const normalizedCommandId = safeString(commandId, 128);
|
|
2957
3062
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
2958
3063
|
device?.deviceId,
|
|
@@ -2973,7 +3078,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2973
3078
|
return true;
|
|
2974
3079
|
}
|
|
2975
3080
|
|
|
2976
|
-
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs) {
|
|
3081
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
2977
3082
|
const normalizedCommandId = safeString(commandId, 128);
|
|
2978
3083
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
2979
3084
|
device?.deviceId,
|
|
@@ -2999,11 +3104,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2999
3104
|
error: 'command-result-wait-replaced'
|
|
3000
3105
|
});
|
|
3001
3106
|
}
|
|
3002
|
-
const waiterRecord = {
|
|
3003
|
-
deviceId: device.deviceId,
|
|
3004
|
-
sessionId: device.sessionId,
|
|
3005
|
-
commandId: normalizedCommandId,
|
|
3006
|
-
|
|
3107
|
+
const waiterRecord = {
|
|
3108
|
+
deviceId: device.deviceId,
|
|
3109
|
+
sessionId: device.sessionId,
|
|
3110
|
+
commandId: normalizedCommandId,
|
|
3111
|
+
channel: safeString(metadata.channel, 24),
|
|
3112
|
+
operationId: safeString(metadata.operationId, 128),
|
|
3113
|
+
timer: null,
|
|
3007
3114
|
deadlineImmediate: null,
|
|
3008
3115
|
resolve
|
|
3009
3116
|
};
|
|
@@ -3036,9 +3143,246 @@ export function createRemoteHub(options = {}) {
|
|
|
3036
3143
|
});
|
|
3037
3144
|
}, timeoutMs);
|
|
3038
3145
|
waiterRecord.timer.unref?.();
|
|
3039
|
-
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3040
|
-
});
|
|
3041
|
-
}
|
|
3146
|
+
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3151
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3152
|
+
const normalizedSessionId = safeString(sessionId, 160);
|
|
3153
|
+
const normalizedOperationId = safeString(operationId, 128);
|
|
3154
|
+
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3155
|
+
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3156
|
+
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3157
|
+
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3158
|
+
: '';
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3162
|
+
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3163
|
+
return false;
|
|
3164
|
+
}
|
|
3165
|
+
const cleanupCommand = operation.direction === 'paste'
|
|
3166
|
+
? 'clipboard.paste.cancel'
|
|
3167
|
+
: 'clipboard.copy.end';
|
|
3168
|
+
const commandId = crypto.randomUUID();
|
|
3169
|
+
try {
|
|
3170
|
+
writeJsonLine(device.fileSocket, {
|
|
3171
|
+
type: 'command',
|
|
3172
|
+
commandId,
|
|
3173
|
+
command: cleanupCommand,
|
|
3174
|
+
payload: {
|
|
3175
|
+
operationId: operation.operationId,
|
|
3176
|
+
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3177
|
+
},
|
|
3178
|
+
issuedAt: new Date().toISOString()
|
|
3179
|
+
});
|
|
3180
|
+
device.counters.commandsSent += 1;
|
|
3181
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3182
|
+
commandId,
|
|
3183
|
+
command: cleanupCommand,
|
|
3184
|
+
channel: 'file',
|
|
3185
|
+
operationId: operation.operationId,
|
|
3186
|
+
cleanup: true
|
|
3187
|
+
});
|
|
3188
|
+
return true;
|
|
3189
|
+
} catch {
|
|
3190
|
+
// The exact side channel is already closing. Native session
|
|
3191
|
+
// teardown remains the authoritative cleanup.
|
|
3192
|
+
return false;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3197
|
+
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3198
|
+
let cleared = 0;
|
|
3199
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3200
|
+
if (operation.deviceId !== device?.deviceId
|
|
3201
|
+
|| operation.sessionId !== device?.sessionId
|
|
3202
|
+
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
queueClipboardAgentCleanup(device, operation, reason);
|
|
3206
|
+
clipboardOperations.delete(key);
|
|
3207
|
+
cleared += 1;
|
|
3208
|
+
}
|
|
3209
|
+
if (cleared > 0) {
|
|
3210
|
+
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3211
|
+
reason,
|
|
3212
|
+
ownerConnectionId: requestedOwner,
|
|
3213
|
+
count: cleared
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
return cleared;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3220
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3221
|
+
const currentDevice = devices.get(operation.deviceId);
|
|
3222
|
+
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3223
|
+
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3224
|
+
const staleOwner = !currentDevice
|
|
3225
|
+
|| currentDevice.sessionId !== operation.sessionId
|
|
3226
|
+
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3227
|
+
if (expired || staleOwner) {
|
|
3228
|
+
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3229
|
+
queueClipboardAgentCleanup(
|
|
3230
|
+
currentDevice,
|
|
3231
|
+
operation,
|
|
3232
|
+
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3233
|
+
}
|
|
3234
|
+
clipboardOperations.delete(key);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3240
|
+
const capabilities = device?.capabilities || {};
|
|
3241
|
+
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3242
|
+
|| capabilities.clipboardText !== true) {
|
|
3243
|
+
return 'device-clipboard-text-unavailable';
|
|
3244
|
+
}
|
|
3245
|
+
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3246
|
+
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3247
|
+
}
|
|
3248
|
+
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3249
|
+
return 'device-clipboard-copy-unavailable';
|
|
3250
|
+
}
|
|
3251
|
+
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3252
|
+
return 'device-clipboard-paste-unavailable';
|
|
3253
|
+
}
|
|
3254
|
+
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3255
|
+
return 'device-clipboard-image-unavailable';
|
|
3256
|
+
}
|
|
3257
|
+
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3258
|
+
return 'device-clipboard-files-unavailable';
|
|
3259
|
+
}
|
|
3260
|
+
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3261
|
+
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3262
|
+
return 'device-clipboard-limits-unavailable';
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3266
|
+
return 'device-clipboard-chunk-limit-unavailable';
|
|
3267
|
+
}
|
|
3268
|
+
if (contentKind === 'image'
|
|
3269
|
+
&& (!Number.isSafeInteger(Number(
|
|
3270
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3271
|
+
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3272
|
+
return 'device-clipboard-limits-unavailable';
|
|
3273
|
+
}
|
|
3274
|
+
if (contentKind === 'files'
|
|
3275
|
+
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3276
|
+
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3277
|
+
return 'device-clipboard-limits-unavailable';
|
|
3278
|
+
}
|
|
3279
|
+
return '';
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function clipboardManifestCapabilityError(device, manifest) {
|
|
3283
|
+
if (!manifest) {
|
|
3284
|
+
return '';
|
|
3285
|
+
}
|
|
3286
|
+
const capabilities = device?.capabilities || {};
|
|
3287
|
+
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3288
|
+
return 'device-clipboard-total-limit-exceeded';
|
|
3289
|
+
}
|
|
3290
|
+
if (manifest.contentKind === 'text'
|
|
3291
|
+
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3292
|
+
return 'device-clipboard-text-limit-exceeded';
|
|
3293
|
+
}
|
|
3294
|
+
if (manifest.contentKind === 'image'
|
|
3295
|
+
&& manifest.totalBytes > Number(
|
|
3296
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3297
|
+
return 'device-clipboard-image-limit-exceeded';
|
|
3298
|
+
}
|
|
3299
|
+
if (manifest.contentKind === 'files'
|
|
3300
|
+
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3301
|
+
return 'device-clipboard-file-count-limit-exceeded';
|
|
3302
|
+
}
|
|
3303
|
+
return '';
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3307
|
+
const device = devices.get(String(deviceId || ''));
|
|
3308
|
+
if (!device) {
|
|
3309
|
+
return { ok: false, error: 'device-not-found' };
|
|
3310
|
+
}
|
|
3311
|
+
if (device.synthetic === true) {
|
|
3312
|
+
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3313
|
+
}
|
|
3314
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3315
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3316
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3317
|
+
}
|
|
3318
|
+
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3319
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
const policy = getDevicePolicy(device);
|
|
3323
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
3324
|
+
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3325
|
+
}
|
|
3326
|
+
if (policy.allowControl !== true) {
|
|
3327
|
+
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3328
|
+
}
|
|
3329
|
+
if (policy.allowClipboardText !== true) {
|
|
3330
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3331
|
+
}
|
|
3332
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3333
|
+
&& policy.allowFileTransfer !== true) {
|
|
3334
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
const direction = request.clipboardDirection
|
|
3338
|
+
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3339
|
+
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3340
|
+
if (capabilityError) {
|
|
3341
|
+
return { ok: false, error: capabilityError };
|
|
3342
|
+
}
|
|
3343
|
+
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3344
|
+
device,
|
|
3345
|
+
request.payload?.manifest || request.manifest);
|
|
3346
|
+
if (manifestCapabilityError) {
|
|
3347
|
+
return { ok: false, error: manifestCapabilityError };
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
const controlStream = getActiveControlStream(device);
|
|
3351
|
+
if (!controlStream
|
|
3352
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3353
|
+
|| !safeString(controlStream.commandId, 128)
|
|
3354
|
+
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3355
|
+
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3356
|
+
}
|
|
3357
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3358
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3359
|
+
}
|
|
3360
|
+
const binding = request.binding || request;
|
|
3361
|
+
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3362
|
+
!== safeString(device.sessionId, 160)
|
|
3363
|
+
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3364
|
+
!== safeString(controlStream.commandId, 128)) {
|
|
3365
|
+
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3366
|
+
}
|
|
3367
|
+
if (Number(binding.captureGeneration || 0)
|
|
3368
|
+
!== Number(controlStream.captureGeneration || 0)) {
|
|
3369
|
+
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3370
|
+
}
|
|
3371
|
+
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3372
|
+
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3373
|
+
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3374
|
+
}
|
|
3375
|
+
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3376
|
+
if (!hubConnectionId
|
|
3377
|
+
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3378
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
3379
|
+
}
|
|
3380
|
+
if (options.requireFileSocket === true
|
|
3381
|
+
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3382
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3383
|
+
}
|
|
3384
|
+
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3385
|
+
}
|
|
3042
3386
|
|
|
3043
3387
|
function inspectAnnexBH264(payload) {
|
|
3044
3388
|
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
@@ -3920,13 +4264,15 @@ export function createRemoteHub(options = {}) {
|
|
|
3920
4264
|
}
|
|
3921
4265
|
}
|
|
3922
4266
|
|
|
3923
|
-
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
3924
|
-
const socket = device?.fileSocket;
|
|
3925
|
-
if (!socket) {
|
|
3926
|
-
return;
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
device
|
|
4267
|
+
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4268
|
+
const socket = device?.fileSocket;
|
|
4269
|
+
if (!socket) {
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4274
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4275
|
+
device.fileSocket = null;
|
|
3930
4276
|
fileSockets.delete(socket);
|
|
3931
4277
|
try {
|
|
3932
4278
|
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
@@ -3940,13 +4286,14 @@ export function createRemoteHub(options = {}) {
|
|
|
3940
4286
|
}
|
|
3941
4287
|
}
|
|
3942
4288
|
|
|
3943
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4289
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
3944
4290
|
const socket = device?.inputSocket;
|
|
3945
4291
|
if (!socket) {
|
|
3946
4292
|
return;
|
|
3947
4293
|
}
|
|
3948
4294
|
|
|
3949
|
-
const writeOwner = device.inputWriteOwner;
|
|
4295
|
+
const writeOwner = device.inputWriteOwner;
|
|
4296
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
3950
4297
|
device.inputSocket = null;
|
|
3951
4298
|
device.inputWriteOwner = null;
|
|
3952
4299
|
device.inputSocketConnectionId = '';
|
|
@@ -3966,7 +4313,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3966
4313
|
}
|
|
3967
4314
|
}
|
|
3968
4315
|
|
|
3969
|
-
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4316
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
3970
4317
|
const deviceId = inputSockets.get(socket);
|
|
3971
4318
|
inputSockets.delete(socket);
|
|
3972
4319
|
if (!deviceId) {
|
|
@@ -3978,7 +4325,8 @@ export function createRemoteHub(options = {}) {
|
|
|
3978
4325
|
return;
|
|
3979
4326
|
}
|
|
3980
4327
|
|
|
3981
|
-
const writeOwner = device.inputWriteOwner;
|
|
4328
|
+
const writeOwner = device.inputWriteOwner;
|
|
4329
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
3982
4330
|
device.inputSocket = null;
|
|
3983
4331
|
device.inputWriteOwner = null;
|
|
3984
4332
|
device.inputSocketConnectionId = '';
|
|
@@ -4023,7 +4371,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4023
4371
|
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
4024
4372
|
}
|
|
4025
4373
|
|
|
4026
|
-
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4374
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4027
4375
|
const deviceId = fileSockets.get(socket);
|
|
4028
4376
|
fileSockets.delete(socket);
|
|
4029
4377
|
if (!deviceId) {
|
|
@@ -4031,38 +4379,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4031
4379
|
}
|
|
4032
4380
|
|
|
4033
4381
|
const device = devices.get(deviceId);
|
|
4034
|
-
if (!device || device.fileSocket !== socket) {
|
|
4035
|
-
return;
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
device
|
|
4382
|
+
if (!device || device.fileSocket !== socket) {
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4387
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4388
|
+
device.fileSocket = null;
|
|
4039
4389
|
device.fileLastSeenAt = new Date().toISOString();
|
|
4040
|
-
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
|
-
function
|
|
4044
|
-
return
|
|
4045
|
-
Date.parse(device?.
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4390
|
+
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function getDeviceConnectionHeartbeatMs(device) {
|
|
4394
|
+
return Date.parse(device?.lastStatusAt || '')
|
|
4395
|
+
|| Date.parse(device?.connectedAt || '')
|
|
4396
|
+
|| 0;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4052
4400
|
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4053
|
-
return false;
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
|
-
const
|
|
4057
|
-
if (!
|
|
4058
|
-
return true;
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4401
|
+
return false;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4405
|
+
if (!lastHeartbeatMs) {
|
|
4406
|
+
return true;
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4413
|
+
const deviceId = safeString(device?.deviceId, 160);
|
|
4414
|
+
const sessionId = safeString(device?.sessionId, 160);
|
|
4415
|
+
const socket = device?.socket;
|
|
4416
|
+
if (!deviceId
|
|
4417
|
+
|| !sessionId
|
|
4418
|
+
|| !socket
|
|
4419
|
+
|| socket.destroyed
|
|
4420
|
+
|| devices.get(deviceId) !== device
|
|
4421
|
+
|| device.socket !== socket
|
|
4422
|
+
|| device.sessionId !== sessionId) {
|
|
4423
|
+
return false;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
closeInputSocket(device, reason);
|
|
4427
|
+
closeFrameSocket(device, reason);
|
|
4428
|
+
closeAudioSocket(device, reason);
|
|
4429
|
+
closeFileSocket(device, reason);
|
|
4430
|
+
try {
|
|
4431
|
+
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4432
|
+
} catch {
|
|
4433
|
+
// The stale connection is retired below even if the final notice fails.
|
|
4434
|
+
}
|
|
4435
|
+
detachSocket(socket, reason);
|
|
4436
|
+
try {
|
|
4437
|
+
socket.destroy?.();
|
|
4438
|
+
} catch {
|
|
4439
|
+
// detachSocket already made the exact session terminal.
|
|
4440
|
+
}
|
|
4441
|
+
return true;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4445
|
+
pruneClipboardOperations(nowMs);
|
|
4446
|
+
let retired = 0;
|
|
4447
|
+
for (const device of devices.values()) {
|
|
4448
|
+
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4455
|
+
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4456
|
+
retired += 1;
|
|
4457
|
+
logWarn(
|
|
4458
|
+
'remote',
|
|
4459
|
+
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4460
|
+
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4461
|
+
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
return retired;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
function scheduleDeviceConnectionSweep() {
|
|
4468
|
+
if (!started || deviceConnectionSweepTimer) {
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4472
|
+
deviceConnectionSweepTimer = null;
|
|
4473
|
+
if (!started) {
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
sweepStaleDeviceConnections();
|
|
4477
|
+
scheduleDeviceConnectionSweep();
|
|
4478
|
+
}, deviceConnectionSweepMs);
|
|
4479
|
+
deviceConnectionSweepTimer.unref?.();
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
4066
4483
|
const retryAfterMs = Math.max(heartbeatMs, duplicateDeviceRetryAfterMs);
|
|
4067
4484
|
writeJsonLine(socket, {
|
|
4068
4485
|
type: 'disconnect',
|
|
@@ -4514,16 +4931,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4514
4931
|
return device;
|
|
4515
4932
|
}
|
|
4516
4933
|
|
|
4517
|
-
function attachInputSocket(socket, hello) {
|
|
4518
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4519
|
-
const device = devices.get(deviceId);
|
|
4934
|
+
function attachInputSocket(socket, hello) {
|
|
4935
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4936
|
+
const device = devices.get(deviceId);
|
|
4520
4937
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4521
4938
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4522
4939
|
socket.destroy();
|
|
4523
|
-
return null;
|
|
4524
|
-
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4940
|
+
return null;
|
|
4941
|
+
}
|
|
4942
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4943
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
4527
4948
|
|
|
4528
4949
|
const now = new Date().toISOString();
|
|
4529
4950
|
const inputSocketConnectionId = crypto.randomUUID();
|
|
@@ -4637,16 +5058,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4637
5058
|
return device;
|
|
4638
5059
|
}
|
|
4639
5060
|
|
|
4640
|
-
function attachFileSocket(socket, hello) {
|
|
4641
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4642
|
-
const device = devices.get(deviceId);
|
|
5061
|
+
function attachFileSocket(socket, hello) {
|
|
5062
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5063
|
+
const device = devices.get(deviceId);
|
|
4643
5064
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4644
5065
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4645
5066
|
socket.destroy();
|
|
4646
|
-
return null;
|
|
4647
|
-
}
|
|
4648
|
-
|
|
4649
|
-
|
|
5067
|
+
return null;
|
|
5068
|
+
}
|
|
5069
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5070
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
4650
5075
|
const now = new Date().toISOString();
|
|
4651
5076
|
device.fileSocket = socket;
|
|
4652
5077
|
device.fileConnectedAt = now;
|
|
@@ -7969,12 +8394,13 @@ export function createRemoteHub(options = {}) {
|
|
|
7969
8394
|
await listenOnPort(requestedPort);
|
|
7970
8395
|
try {
|
|
7971
8396
|
await relayControl?.start?.();
|
|
7972
|
-
} catch {
|
|
7973
|
-
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
7974
|
-
}
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
8397
|
+
} catch {
|
|
8398
|
+
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8399
|
+
}
|
|
8400
|
+
scheduleDeviceConnectionSweep();
|
|
8401
|
+
} catch (error) {
|
|
8402
|
+
if (error?.code === 'EADDRINUSE') {
|
|
8403
|
+
const conflict = new Error(
|
|
7978
8404
|
`LiveDesk 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.`);
|
|
7979
8405
|
conflict.code = 'EADDRINUSE';
|
|
7980
8406
|
conflict.port = requestedPort;
|
|
@@ -7986,12 +8412,16 @@ export function createRemoteHub(options = {}) {
|
|
|
7986
8412
|
}
|
|
7987
8413
|
|
|
7988
8414
|
return getStatus({ includeSecrets: false });
|
|
7989
|
-
}
|
|
7990
|
-
|
|
7991
|
-
async function close() {
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8417
|
+
async function close() {
|
|
8418
|
+
if (deviceConnectionSweepTimer) {
|
|
8419
|
+
clearTimeout(deviceConnectionSweepTimer);
|
|
8420
|
+
deviceConnectionSweepTimer = null;
|
|
8421
|
+
}
|
|
8422
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
8423
|
+
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8424
|
+
}
|
|
7995
8425
|
|
|
7996
8426
|
for (const device of devices.values()) {
|
|
7997
8427
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
@@ -8126,9 +8556,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8126
8556
|
};
|
|
8127
8557
|
}
|
|
8128
8558
|
|
|
8129
|
-
function sendCommand(deviceId, command) {
|
|
8130
|
-
const device = devices.get(String(deviceId || ''));
|
|
8131
|
-
const commandName = safeString(command?.command || 'ping', 80);
|
|
8559
|
+
function sendCommand(deviceId, command) {
|
|
8560
|
+
const device = devices.get(String(deviceId || ''));
|
|
8561
|
+
const commandName = safeString(command?.command || 'ping', 80);
|
|
8562
|
+
if (REMOTE_CLIPBOARD_FILE_ACTIONS.has(commandName)) {
|
|
8563
|
+
return { ok: false, error: 'clipboard-command-requires-awaited-file-channel' };
|
|
8564
|
+
}
|
|
8132
8565
|
const requiredPermission = commandName === 'input.control'
|
|
8133
8566
|
? 'allowControl'
|
|
8134
8567
|
: commandName === 'system.power'
|
|
@@ -8164,12 +8597,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8164
8597
|
result: { ok: true, synthetic: true },
|
|
8165
8598
|
error: ''
|
|
8166
8599
|
});
|
|
8167
|
-
return { ok: true, commandId, synthetic: true };
|
|
8168
|
-
}
|
|
8169
|
-
|
|
8170
|
-
if (
|
|
8171
|
-
|
|
8172
|
-
|
|
8600
|
+
return { ok: true, commandId, synthetic: true };
|
|
8601
|
+
}
|
|
8602
|
+
|
|
8603
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
8604
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8605
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8606
|
+
}
|
|
8607
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8608
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
+
}
|
|
8173
8610
|
|
|
8174
8611
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8175
8612
|
const payload = {
|
|
@@ -8182,29 +8619,300 @@ export function createRemoteHub(options = {}) {
|
|
|
8182
8619
|
|
|
8183
8620
|
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
8184
8621
|
&& device.fileSocket
|
|
8185
|
-
&& !device.fileSocket.destroyed
|
|
8186
|
-
? device.fileSocket
|
|
8187
|
-
: null;
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8622
|
+
&& !device.fileSocket.destroyed
|
|
8623
|
+
? device.fileSocket
|
|
8624
|
+
: null;
|
|
8625
|
+
try {
|
|
8626
|
+
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8627
|
+
} catch {
|
|
8628
|
+
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8629
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8630
|
+
}
|
|
8631
|
+
device.counters.commandsSent += 1;
|
|
8632
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8633
|
+
commandId,
|
|
8192
8634
|
command: payload.command,
|
|
8193
8635
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8194
8636
|
});
|
|
8195
|
-
return { ok: true, commandId };
|
|
8196
|
-
}
|
|
8197
|
-
|
|
8198
|
-
function
|
|
8637
|
+
return { ok: true, commandId };
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
async function sendClipboardCommand(deviceId, request = {}) {
|
|
8641
|
+
pruneClipboardOperations();
|
|
8642
|
+
const action = safeString(request.action, 80).toLowerCase();
|
|
8643
|
+
if (!REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
8644
|
+
return { ok: false, error: 'clipboard-file-action-invalid' };
|
|
8645
|
+
}
|
|
8646
|
+
const operationId = safeString(request.operationId, 128);
|
|
8647
|
+
if (!operationId) {
|
|
8648
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
8649
|
+
}
|
|
8650
|
+
|
|
8651
|
+
const direction = action.startsWith('clipboard.paste.')
|
|
8652
|
+
? 'paste'
|
|
8653
|
+
: action.startsWith('clipboard.copy.')
|
|
8654
|
+
? 'copy'
|
|
8655
|
+
: '';
|
|
8656
|
+
const operationKey = buildClipboardOperationKey(deviceId, request.binding?.controlSessionId, operationId);
|
|
8657
|
+
let operation = clipboardOperations.get(operationKey);
|
|
8658
|
+
const beginsOperation = action === 'clipboard.copy.begin' || action === 'clipboard.paste.begin';
|
|
8659
|
+
const endsOperation = action === 'clipboard.copy.end' || action === 'clipboard.paste.cancel';
|
|
8660
|
+
const requestedManifest = request.payload?.manifest || null;
|
|
8661
|
+
|
|
8662
|
+
if (beginsOperation) {
|
|
8663
|
+
if (operation) {
|
|
8664
|
+
return { ok: false, error: 'clipboard-operation-already-exists' };
|
|
8665
|
+
}
|
|
8666
|
+
const activeForDevice = [...clipboardOperations.values()].filter(candidate =>
|
|
8667
|
+
candidate.deviceId === String(deviceId || '')
|
|
8668
|
+
&& candidate.sessionId === request.binding?.controlSessionId);
|
|
8669
|
+
if (activeForDevice.some(candidate => candidate.direction === direction)) {
|
|
8670
|
+
return { ok: false, error: 'clipboard-operation-direction-busy' };
|
|
8671
|
+
}
|
|
8672
|
+
if (activeForDevice.length >= REMOTE_CLIPBOARD_LIMITS.maxOperationsPerDevice) {
|
|
8673
|
+
return { ok: false, error: 'clipboard-operation-capacity-reached' };
|
|
8674
|
+
}
|
|
8675
|
+
operation = {
|
|
8676
|
+
deviceId: String(deviceId || ''),
|
|
8677
|
+
sessionId: safeString(request.binding?.controlSessionId, 160),
|
|
8678
|
+
operationId,
|
|
8679
|
+
direction,
|
|
8680
|
+
hubConnectionId: safeString(request.hubConnectionId, 128),
|
|
8681
|
+
controlCommandId: safeString(request.binding?.controlCommandId, 128),
|
|
8682
|
+
captureGeneration: Number(request.binding?.captureGeneration || 0),
|
|
8683
|
+
monitorIndex: Number(request.binding?.monitorIndex),
|
|
8684
|
+
contentKind: requestedManifest?.contentKind || 'text',
|
|
8685
|
+
manifest: requestedManifest,
|
|
8686
|
+
offsets: new Map(),
|
|
8687
|
+
status: action === 'clipboard.paste.begin' ? 'preparing' : 'copying',
|
|
8688
|
+
inFlight: false,
|
|
8689
|
+
touchedAt: Date.now()
|
|
8690
|
+
};
|
|
8691
|
+
clipboardOperations.set(operationKey, operation);
|
|
8692
|
+
} else {
|
|
8693
|
+
if (!operation) {
|
|
8694
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8695
|
+
}
|
|
8696
|
+
const requestedDirection = direction || operation.direction;
|
|
8697
|
+
if (operation.direction !== requestedDirection
|
|
8698
|
+
|| operation.hubConnectionId !== safeString(request.hubConnectionId, 128)
|
|
8699
|
+
|| operation.controlCommandId !== safeString(request.binding?.controlCommandId, 128)
|
|
8700
|
+
|| operation.captureGeneration !== Number(request.binding?.captureGeneration || 0)
|
|
8701
|
+
|| operation.monitorIndex !== Number(request.binding?.monitorIndex)) {
|
|
8702
|
+
return { ok: false, error: 'clipboard-operation-owner-stale' };
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
if (operation.inFlight) {
|
|
8707
|
+
return { ok: false, error: 'clipboard-operation-busy' };
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
const contentKind = operation.contentKind || requestedManifest?.contentKind || 'text';
|
|
8711
|
+
const bindingResult = validateCurrentClipboardBinding(
|
|
8712
|
+
deviceId,
|
|
8713
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8714
|
+
contentKind,
|
|
8715
|
+
{ requireFileSocket: true });
|
|
8716
|
+
if (!bindingResult.ok) {
|
|
8717
|
+
if (beginsOperation || bindingResult.error?.startsWith('STALE_') || bindingResult.error === 'input-owner-not-current') {
|
|
8718
|
+
clipboardOperations.delete(operationKey);
|
|
8719
|
+
}
|
|
8720
|
+
return bindingResult;
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8723
|
+
const { device } = bindingResult;
|
|
8724
|
+
const fileSocket = device.fileSocket;
|
|
8725
|
+
const itemIndex = request.payload?.itemIndex;
|
|
8726
|
+
const expectedOffset = Number.isInteger(itemIndex)
|
|
8727
|
+
? Number(operation.offsets.get(itemIndex) || 0)
|
|
8728
|
+
: 0;
|
|
8729
|
+
let outgoingChunkBytes = 0;
|
|
8730
|
+
if (action === 'clipboard.copy.chunk' || action === 'clipboard.paste.chunk') {
|
|
8731
|
+
const item = operation.manifest?.items?.[itemIndex];
|
|
8732
|
+
if (!item) {
|
|
8733
|
+
return { ok: false, error: 'clipboard-item-not-found' };
|
|
8734
|
+
}
|
|
8735
|
+
if (request.payload.offset !== expectedOffset) {
|
|
8736
|
+
return { ok: false, error: 'clipboard-chunk-offset-stale', expectedOffset };
|
|
8737
|
+
}
|
|
8738
|
+
if (action === 'clipboard.copy.chunk'
|
|
8739
|
+
&& request.payload.maxBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8740
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8741
|
+
}
|
|
8742
|
+
if (action === 'clipboard.paste.chunk') {
|
|
8743
|
+
outgoingChunkBytes = Buffer.from(request.payload.dataBase64, 'base64').length;
|
|
8744
|
+
if (outgoingChunkBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8745
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8746
|
+
}
|
|
8747
|
+
const nextOffset = expectedOffset + outgoingChunkBytes;
|
|
8748
|
+
if (outgoingChunkBytes === 0
|
|
8749
|
+
&& !(item.size === 0 && expectedOffset === 0 && request.payload.final === true)) {
|
|
8750
|
+
return { ok: false, error: 'clipboard-empty-chunk-invalid' };
|
|
8751
|
+
}
|
|
8752
|
+
if (nextOffset > item.size
|
|
8753
|
+
|| request.payload.final !== (nextOffset === item.size)) {
|
|
8754
|
+
return { ok: false, error: 'clipboard-chunk-boundary-invalid' };
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
|
|
8759
|
+
operation.inFlight = true;
|
|
8760
|
+
operation.touchedAt = Date.now();
|
|
8761
|
+
let commandCompleted = false;
|
|
8762
|
+
const commandId = crypto.randomUUID();
|
|
8763
|
+
const resultPromise = waitForCommandResult(
|
|
8764
|
+
device,
|
|
8765
|
+
commandId,
|
|
8766
|
+
REMOTE_CLIPBOARD_LIMITS.commandTimeoutMs,
|
|
8767
|
+
{ channel: 'file', operationId });
|
|
8768
|
+
const commandPayload = {
|
|
8769
|
+
type: 'command',
|
|
8770
|
+
commandId,
|
|
8771
|
+
command: action,
|
|
8772
|
+
payload: {
|
|
8773
|
+
operationId,
|
|
8774
|
+
controlSessionId: operation.sessionId,
|
|
8775
|
+
controlCommandId: operation.controlCommandId,
|
|
8776
|
+
captureGeneration: operation.captureGeneration,
|
|
8777
|
+
monitorIndex: operation.monitorIndex,
|
|
8778
|
+
hubConnectionId: operation.hubConnectionId,
|
|
8779
|
+
...(request.payload || {})
|
|
8780
|
+
},
|
|
8781
|
+
issuedAt: new Date().toISOString()
|
|
8782
|
+
};
|
|
8783
|
+
|
|
8784
|
+
try {
|
|
8785
|
+
try {
|
|
8786
|
+
// Clipboard payloads are never allowed onto the concurrent
|
|
8787
|
+
// main command socket. The dedicated file side channel owns
|
|
8788
|
+
// the one bounded command and its exact-session waiter.
|
|
8789
|
+
writeJsonLine(fileSocket, commandPayload);
|
|
8790
|
+
} catch {
|
|
8791
|
+
failPendingCommandResultWaiter(device, commandId, 'clipboard-file-channel-write-failed');
|
|
8792
|
+
if (device.fileSocket === fileSocket) {
|
|
8793
|
+
closeFileSocket(device, 'clipboard-file-channel-write-failed');
|
|
8794
|
+
}
|
|
8795
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
8796
|
+
}
|
|
8797
|
+
device.counters.commandsSent += 1;
|
|
8798
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8799
|
+
commandId,
|
|
8800
|
+
command: action,
|
|
8801
|
+
channel: 'file',
|
|
8802
|
+
operationId
|
|
8803
|
+
});
|
|
8804
|
+
|
|
8805
|
+
const awaited = await resultPromise;
|
|
8806
|
+
if (!awaited.ok) {
|
|
8807
|
+
return {
|
|
8808
|
+
ok: false,
|
|
8809
|
+
error: awaited.error || 'clipboard-command-failed',
|
|
8810
|
+
commandId
|
|
8811
|
+
};
|
|
8812
|
+
}
|
|
8813
|
+
if (device.fileSocket !== fileSocket) {
|
|
8814
|
+
return { ok: false, error: 'clipboard-file-channel-owner-changed' };
|
|
8815
|
+
}
|
|
8816
|
+
const currentBinding = validateCurrentClipboardBinding(
|
|
8817
|
+
deviceId,
|
|
8818
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8819
|
+
operation.contentKind || 'text',
|
|
8820
|
+
{ requireFileSocket: true });
|
|
8821
|
+
if (!currentBinding.ok) {
|
|
8822
|
+
return currentBinding;
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8825
|
+
let response;
|
|
8826
|
+
try {
|
|
8827
|
+
response = normalizeRemoteClipboardCommandResult(action, operationId, awaited.result);
|
|
8828
|
+
} catch (error) {
|
|
8829
|
+
return {
|
|
8830
|
+
ok: false,
|
|
8831
|
+
error: safeString(error?.code || error?.message, 160) || 'clipboard-response-invalid'
|
|
8832
|
+
};
|
|
8833
|
+
}
|
|
8834
|
+
if (response.manifest) {
|
|
8835
|
+
const responseManifestLimitError = clipboardManifestCapabilityError(device, response.manifest);
|
|
8836
|
+
if (responseManifestLimitError) {
|
|
8837
|
+
clipboardOperations.delete(operationKey);
|
|
8838
|
+
return { ok: false, error: responseManifestLimitError };
|
|
8839
|
+
}
|
|
8840
|
+
const responsePolicy = validateCurrentClipboardBinding(
|
|
8841
|
+
deviceId,
|
|
8842
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8843
|
+
response.manifest.contentKind,
|
|
8844
|
+
{ requireFileSocket: true });
|
|
8845
|
+
if (!responsePolicy.ok) {
|
|
8846
|
+
clipboardOperations.delete(operationKey);
|
|
8847
|
+
return responsePolicy;
|
|
8848
|
+
}
|
|
8849
|
+
operation.manifest = response.manifest;
|
|
8850
|
+
operation.contentKind = response.manifest.contentKind;
|
|
8851
|
+
}
|
|
8852
|
+
if (action === 'clipboard.copy.chunk') {
|
|
8853
|
+
const item = operation.manifest?.items?.[response.itemIndex];
|
|
8854
|
+
const nextOffset = response.offset + response.bytes;
|
|
8855
|
+
if (!item
|
|
8856
|
+
|| response.itemIndex !== itemIndex
|
|
8857
|
+
|| response.offset !== expectedOffset
|
|
8858
|
+
|| (response.bytes === 0
|
|
8859
|
+
&& !(item.size === 0 && response.offset === 0 && response.final === true))
|
|
8860
|
+
|| nextOffset > item.size
|
|
8861
|
+
|| response.final !== (nextOffset === item.size)) {
|
|
8862
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8863
|
+
}
|
|
8864
|
+
if (response.bytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8865
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8866
|
+
}
|
|
8867
|
+
operation.offsets.set(response.itemIndex, nextOffset);
|
|
8868
|
+
} else if (action === 'clipboard.paste.chunk') {
|
|
8869
|
+
if (response.itemIndex !== itemIndex
|
|
8870
|
+
|| response.offset !== expectedOffset
|
|
8871
|
+
|| response.bytes !== outgoingChunkBytes
|
|
8872
|
+
|| response.totalBytes !== operation.manifest.items[itemIndex].size
|
|
8873
|
+
|| response.final !== request.payload.final) {
|
|
8874
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8875
|
+
}
|
|
8876
|
+
operation.offsets.set(itemIndex, expectedOffset + outgoingChunkBytes);
|
|
8877
|
+
}
|
|
8878
|
+
operation.status = response.status || operation.status;
|
|
8879
|
+
operation.touchedAt = Date.now();
|
|
8880
|
+
if (action === 'clipboard.operation.status' && operation.status === 'not-found') {
|
|
8881
|
+
clipboardOperations.delete(operationKey);
|
|
8882
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8883
|
+
}
|
|
8884
|
+
if (action === 'clipboard.operation.status'
|
|
8885
|
+
&& ['completed', 'complete', 'applied', 'pasted', 'cancelled', 'cleaned', 'failed', 'expired', 'already-ended']
|
|
8886
|
+
.includes(operation.status)) {
|
|
8887
|
+
clipboardOperations.delete(operationKey);
|
|
8888
|
+
}
|
|
8889
|
+
commandCompleted = true;
|
|
8890
|
+
return response;
|
|
8891
|
+
} finally {
|
|
8892
|
+
const currentOperation = clipboardOperations.get(operationKey);
|
|
8893
|
+
if (currentOperation === operation) {
|
|
8894
|
+
operation.inFlight = false;
|
|
8895
|
+
if (endsOperation || (beginsOperation && !commandCompleted)) {
|
|
8896
|
+
clipboardOperations.delete(operationKey);
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
|
|
8902
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8199
8903
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8200
8904
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8201
8905
|
: null;
|
|
8202
8906
|
let updated = 0;
|
|
8203
8907
|
let total = 0;
|
|
8204
|
-
for (const device of devices.values()) {
|
|
8205
|
-
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8206
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8207
|
-
|
|
8908
|
+
for (const device of devices.values()) {
|
|
8909
|
+
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8910
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8911
|
+
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8912
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8913
|
+
continue;
|
|
8914
|
+
}
|
|
8915
|
+
total += 1;
|
|
8208
8916
|
const effectivePolicy = getDevicePolicy(device);
|
|
8209
8917
|
if (writeJsonLine(device.socket, {
|
|
8210
8918
|
type: 'policy.update',
|
|
@@ -8223,8 +8931,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8223
8931
|
const device = devices.get(String(deviceId || ''));
|
|
8224
8932
|
const denied = policyError(device);
|
|
8225
8933
|
if (denied) return { ok: false, error: denied };
|
|
8226
|
-
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8227
|
-
if (
|
|
8934
|
+
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8935
|
+
if (device.synthetic !== true
|
|
8936
|
+
&& device.connected
|
|
8937
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
8938
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8939
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8940
|
+
}
|
|
8941
|
+
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
8228
8942
|
return { ok: false, error: 'device-not-connected' };
|
|
8229
8943
|
}
|
|
8230
8944
|
|
|
@@ -8320,15 +9034,21 @@ export function createRemoteHub(options = {}) {
|
|
|
8320
9034
|
return device.inputWriteOwner;
|
|
8321
9035
|
}
|
|
8322
9036
|
|
|
8323
|
-
function sendInputControl(deviceId, input = {}) {
|
|
8324
|
-
const device = devices.get(String(deviceId || ''));
|
|
8325
|
-
if (!device) {
|
|
8326
|
-
return { ok: false, error: 'device-not-found' };
|
|
8327
|
-
}
|
|
8328
|
-
|
|
8329
|
-
if (
|
|
8330
|
-
|
|
8331
|
-
|
|
9037
|
+
function sendInputControl(deviceId, input = {}) {
|
|
9038
|
+
const device = devices.get(String(deviceId || ''));
|
|
9039
|
+
if (!device) {
|
|
9040
|
+
return { ok: false, error: 'device-not-found' };
|
|
9041
|
+
}
|
|
9042
|
+
|
|
9043
|
+
if (device.synthetic !== true
|
|
9044
|
+
&& device.connected
|
|
9045
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
9046
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9047
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9048
|
+
}
|
|
9049
|
+
if (!device.connected) {
|
|
9050
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9051
|
+
}
|
|
8332
9052
|
|
|
8333
9053
|
const normalized = normalizeRemoteInputEvent(input);
|
|
8334
9054
|
if (!normalized.type) {
|
|
@@ -8394,9 +9114,51 @@ export function createRemoteHub(options = {}) {
|
|
|
8394
9114
|
|| device.capabilities?.accessibilityPermission === false) {
|
|
8395
9115
|
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
8396
9116
|
}
|
|
8397
|
-
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
8398
|
-
return { ok: false, error: 'device-input-control-unavailable' };
|
|
8399
|
-
}
|
|
9117
|
+
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
9118
|
+
return { ok: false, error: 'device-input-control-unavailable' };
|
|
9119
|
+
}
|
|
9120
|
+
|
|
9121
|
+
const normalizedInputType = normalized.type.toLowerCase();
|
|
9122
|
+
const isClipboardInput = normalizedInputType === 'clipboard.prepare'
|
|
9123
|
+
|| normalizedInputType === 'clipboard.copy'
|
|
9124
|
+
|| normalizedInputType === 'clipboard.paste';
|
|
9125
|
+
let clipboardManifest = null;
|
|
9126
|
+
if (isClipboardInput) {
|
|
9127
|
+
if (!normalized.clipboardOperationId) {
|
|
9128
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
9129
|
+
}
|
|
9130
|
+
if (!['copy', 'paste'].includes(normalized.clipboardDirection)) {
|
|
9131
|
+
return { ok: false, error: 'clipboard-direction-required' };
|
|
9132
|
+
}
|
|
9133
|
+
if ((normalizedInputType === 'clipboard.copy' && normalized.clipboardDirection !== 'copy')
|
|
9134
|
+
|| (normalizedInputType === 'clipboard.paste' && normalized.clipboardDirection !== 'paste')) {
|
|
9135
|
+
return { ok: false, error: 'clipboard-direction-mismatch' };
|
|
9136
|
+
}
|
|
9137
|
+
try {
|
|
9138
|
+
clipboardManifest = normalized.manifest
|
|
9139
|
+
? normalizeRemoteClipboardManifest(normalized.manifest, normalized.clipboardOperationId)
|
|
9140
|
+
: null;
|
|
9141
|
+
} catch (error) {
|
|
9142
|
+
return { ok: false, error: safeString(error?.code || error?.message, 160) || 'clipboard-manifest-invalid' };
|
|
9143
|
+
}
|
|
9144
|
+
const clipboardPolicy = getDevicePolicy(device);
|
|
9145
|
+
if (clipboardPolicy.allowClipboardText !== true) {
|
|
9146
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
9147
|
+
}
|
|
9148
|
+
const contentKind = clipboardManifest?.contentKind || normalized.contentKind || 'text';
|
|
9149
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
9150
|
+
&& clipboardPolicy.allowFileTransfer !== true) {
|
|
9151
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
9152
|
+
}
|
|
9153
|
+
const clipboardCapabilityDenied = clipboardCapabilityError(
|
|
9154
|
+
device,
|
|
9155
|
+
normalized.clipboardDirection,
|
|
9156
|
+
contentKind);
|
|
9157
|
+
if (clipboardCapabilityDenied) {
|
|
9158
|
+
return { ok: false, error: clipboardCapabilityDenied };
|
|
9159
|
+
}
|
|
9160
|
+
normalized.manifest = clipboardManifest;
|
|
9161
|
+
}
|
|
8400
9162
|
|
|
8401
9163
|
const controlStream = getActiveControlStream(device);
|
|
8402
9164
|
if (!controlStream
|
|
@@ -8432,14 +9194,32 @@ export function createRemoteHub(options = {}) {
|
|
|
8432
9194
|
};
|
|
8433
9195
|
}
|
|
8434
9196
|
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
8435
|
-
if (!Number.isInteger(normalized.monitorIndex)
|
|
8436
|
-
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9197
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
9198
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
8437
9199
|
return {
|
|
8438
9200
|
ok: false,
|
|
8439
9201
|
error: 'STALE_CONTROL_MONITOR',
|
|
8440
|
-
activeMonitorIndex
|
|
8441
|
-
};
|
|
8442
|
-
}
|
|
9202
|
+
activeMonitorIndex
|
|
9203
|
+
};
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
let clipboardOperationKey = '';
|
|
9207
|
+
if (normalizedInputType === 'clipboard.paste') {
|
|
9208
|
+
clipboardOperationKey = buildClipboardOperationKey(
|
|
9209
|
+
device.deviceId,
|
|
9210
|
+
device.sessionId,
|
|
9211
|
+
normalized.clipboardOperationId);
|
|
9212
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9213
|
+
if (!operation
|
|
9214
|
+
|| operation.direction !== 'paste'
|
|
9215
|
+
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9216
|
+
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9217
|
+
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9218
|
+
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9219
|
+
|| operation.status !== 'ready') {
|
|
9220
|
+
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9221
|
+
}
|
|
9222
|
+
}
|
|
8443
9223
|
|
|
8444
9224
|
const inputSocket = device.inputSocket;
|
|
8445
9225
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -8456,9 +9236,17 @@ export function createRemoteHub(options = {}) {
|
|
|
8456
9236
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8457
9237
|
const bindingChanged = previousInputBindingKey
|
|
8458
9238
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8459
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8460
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8461
|
-
|
|
9239
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9240
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9241
|
+
if (ownerChanged) {
|
|
9242
|
+
clearClipboardOperationsForDevice(
|
|
9243
|
+
device,
|
|
9244
|
+
'browser-input-owner-changed',
|
|
9245
|
+
previousOwnerConnectionId);
|
|
9246
|
+
} else if (bindingChanged || writeBindingChanged) {
|
|
9247
|
+
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9248
|
+
}
|
|
9249
|
+
const resetSent = inputWriteOwner.replaceBinding(
|
|
8462
9250
|
activeInputBindingKey,
|
|
8463
9251
|
buildRemoteInputResetMessage(
|
|
8464
9252
|
device,
|
|
@@ -8488,9 +9276,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8488
9276
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8489
9277
|
}
|
|
8490
9278
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8491
|
-
device.counters.commandsSent += 1;
|
|
8492
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
8493
|
-
|
|
9279
|
+
device.counters.commandsSent += 1;
|
|
9280
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
9281
|
+
if (clipboardOperationKey) {
|
|
9282
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9283
|
+
if (operation) {
|
|
9284
|
+
operation.status = 'pasting';
|
|
9285
|
+
operation.touchedAt = Date.now();
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
return {
|
|
8494
9289
|
ok: true,
|
|
8495
9290
|
inputSocket: true,
|
|
8496
9291
|
sessionId: device.sessionId,
|
|
@@ -8525,7 +9320,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8525
9320
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8526
9321
|
}
|
|
8527
9322
|
});
|
|
8528
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9323
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8529
9324
|
schedulePendingInputFallback(device, {
|
|
8530
9325
|
commandId: fallback.commandId,
|
|
8531
9326
|
inputSeq: normalized.inputSeq,
|
|
@@ -8537,9 +9332,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8537
9332
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8538
9333
|
hubForwardedAtEpochMs,
|
|
8539
9334
|
fallback: true,
|
|
8540
|
-
timeoutTimer: null
|
|
8541
|
-
});
|
|
8542
|
-
}
|
|
9335
|
+
timeoutTimer: null
|
|
9336
|
+
});
|
|
9337
|
+
}
|
|
9338
|
+
if (fallback.ok && clipboardOperationKey) {
|
|
9339
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9340
|
+
if (operation) {
|
|
9341
|
+
operation.status = 'pasting';
|
|
9342
|
+
operation.touchedAt = Date.now();
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
8543
9345
|
return fallback.ok
|
|
8544
9346
|
? {
|
|
8545
9347
|
...fallback,
|
|
@@ -8559,14 +9361,15 @@ export function createRemoteHub(options = {}) {
|
|
|
8559
9361
|
};
|
|
8560
9362
|
}
|
|
8561
9363
|
|
|
8562
|
-
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9364
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
8563
9365
|
const device = devices.get(String(deviceId || ''));
|
|
8564
9366
|
const owner = safeString(ownerConnectionId, 128);
|
|
8565
|
-
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
8566
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
8567
|
-
}
|
|
8568
|
-
|
|
8569
|
-
device
|
|
9367
|
+
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9368
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
9369
|
+
}
|
|
9370
|
+
|
|
9371
|
+
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9372
|
+
device.inputOwnerConnectionId = '';
|
|
8570
9373
|
device.inputOwnerBindingKey = '';
|
|
8571
9374
|
const inputSocket = device.inputSocket;
|
|
8572
9375
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -8610,8 +9413,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8610
9413
|
};
|
|
8611
9414
|
const results = targets.map(deviceId => {
|
|
8612
9415
|
const device = devices.get(deviceId);
|
|
8613
|
-
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
8614
|
-
if (
|
|
9416
|
+
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9417
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9418
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9419
|
+
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9420
|
+
}
|
|
9421
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
8615
9422
|
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
8616
9423
|
}
|
|
8617
9424
|
return writeJsonLine(device.socket, payload)
|
|
@@ -8643,7 +9450,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8643
9450
|
const denied = policyError(device, 'allowAgent');
|
|
8644
9451
|
if (denied) return { ok: false, error: denied };
|
|
8645
9452
|
|
|
8646
|
-
if (device?.synthetic === true && device.connected) {
|
|
9453
|
+
if (device?.synthetic === true && device.connected) {
|
|
8647
9454
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
8648
9455
|
if (!instruction) {
|
|
8649
9456
|
return { ok: false, error: 'missing-instruction' };
|
|
@@ -8703,10 +9510,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8703
9510
|
error: '',
|
|
8704
9511
|
synthetic: true
|
|
8705
9512
|
});
|
|
8706
|
-
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
8707
|
-
}
|
|
8708
|
-
|
|
8709
|
-
if (
|
|
9513
|
+
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
9514
|
+
}
|
|
9515
|
+
|
|
9516
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
9517
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9518
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9519
|
+
}
|
|
9520
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8710
9521
|
return { ok: false, error: 'device-not-connected' };
|
|
8711
9522
|
}
|
|
8712
9523
|
|
|
@@ -9526,11 +10337,58 @@ export function createRemoteHub(options = {}) {
|
|
|
9526
10337
|
=== normalized.streamPurpose;
|
|
9527
10338
|
}
|
|
9528
10339
|
|
|
9529
|
-
function
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
10340
|
+
function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
|
|
10341
|
+
if (!activeLiveStream) {
|
|
10342
|
+
return false;
|
|
10343
|
+
}
|
|
10344
|
+
return activeLiveStream.frameMode === normalized.transfer.frameMode
|
|
10345
|
+
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
|
|
10346
|
+
&& (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
|
|
10347
|
+
=== normalized.streamPurpose;
|
|
10348
|
+
}
|
|
10349
|
+
|
|
10350
|
+
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
10351
|
+
return {
|
|
10352
|
+
ok: true,
|
|
10353
|
+
commandId: activeLiveStream.commandId,
|
|
10354
|
+
sessionId: device.sessionId,
|
|
10355
|
+
streamId: activeLiveStream.streamId,
|
|
10356
|
+
streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
|
|
10357
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
10358
|
+
mode: activeLiveStream.mode || normalized.transfer.mode,
|
|
10359
|
+
frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
|
|
10360
|
+
monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
|
|
10361
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10362
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
10363
|
+
reused: true,
|
|
10364
|
+
sharedProfileReused: true,
|
|
10365
|
+
requestedProfile: {
|
|
10366
|
+
fps: normalized.fps,
|
|
10367
|
+
maxWidth: normalized.maxWidth,
|
|
10368
|
+
maxHeight: normalized.maxHeight,
|
|
10369
|
+
quality: normalized.quality
|
|
10370
|
+
},
|
|
10371
|
+
effectiveProfile: {
|
|
10372
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
10373
|
+
maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
10374
|
+
maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
|
|
10375
|
+
quality: Number(activeLiveStream.quality || normalized.quality)
|
|
10376
|
+
},
|
|
10377
|
+
...extra
|
|
10378
|
+
};
|
|
10379
|
+
}
|
|
10380
|
+
|
|
10381
|
+
function startLiveStream(deviceId, options = {}) {
|
|
10382
|
+
const device = devices.get(String(deviceId || ''));
|
|
10383
|
+
const denied = policyError(device);
|
|
10384
|
+
if (denied) return { ok: false, error: denied };
|
|
10385
|
+
if (device?.synthetic !== true
|
|
10386
|
+
&& device?.connected
|
|
10387
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10388
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10389
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10390
|
+
}
|
|
10391
|
+
if (device?.liveCapturePause?.token) {
|
|
9534
10392
|
return {
|
|
9535
10393
|
ok: false,
|
|
9536
10394
|
error: 'LIVE_CAPTURE_PAUSED',
|
|
@@ -9662,6 +10520,13 @@ export function createRemoteHub(options = {}) {
|
|
|
9662
10520
|
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
9663
10521
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
9664
10522
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10523
|
+
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
10524
|
+
return {
|
|
10525
|
+
ok: false,
|
|
10526
|
+
error: 'device-frame-mode-unavailable',
|
|
10527
|
+
frameMode: transfer.frameMode
|
|
10528
|
+
};
|
|
10529
|
+
}
|
|
9665
10530
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
9666
10531
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
9667
10532
|
return {
|
|
@@ -9676,13 +10541,24 @@ export function createRemoteHub(options = {}) {
|
|
|
9676
10541
|
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
9677
10542
|
if (pendingDescriptor?.commandId) {
|
|
9678
10543
|
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
9679
|
-
|
|
10544
|
+
const pendingMatchesSharedOwner = options.forceRestart !== true
|
|
10545
|
+
&& options.reuseExisting === true
|
|
10546
|
+
&& options.reuseSharedExisting === true
|
|
10547
|
+
&& liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
|
|
10548
|
+
if ((pendingMatchesRequest || pendingMatchesSharedOwner)
|
|
10549
|
+
&& liveStreamReplacementStillPending(activeLiveStream)) {
|
|
9680
10550
|
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
9681
10551
|
streamId,
|
|
9682
10552
|
commandId: pendingDescriptor.commandId,
|
|
9683
10553
|
activeCommandId: activeLiveStream.commandId,
|
|
9684
10554
|
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
9685
10555
|
});
|
|
10556
|
+
if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
|
|
10557
|
+
return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
|
|
10558
|
+
ready: false,
|
|
10559
|
+
pending: true
|
|
10560
|
+
});
|
|
10561
|
+
}
|
|
9686
10562
|
return {
|
|
9687
10563
|
ok: true,
|
|
9688
10564
|
commandId: pendingDescriptor.commandId,
|
|
@@ -9756,6 +10632,25 @@ export function createRemoteHub(options = {}) {
|
|
|
9756
10632
|
reused: true
|
|
9757
10633
|
};
|
|
9758
10634
|
}
|
|
10635
|
+
if (activeLiveStream
|
|
10636
|
+
&& options.forceRestart !== true
|
|
10637
|
+
&& options.reuseExisting === true
|
|
10638
|
+
&& options.reuseSharedExisting === true
|
|
10639
|
+
&& liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
|
|
10640
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
10641
|
+
emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
|
|
10642
|
+
streamId,
|
|
10643
|
+
commandId: activeLiveStream.commandId,
|
|
10644
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10645
|
+
requestedFps: normalized.fps,
|
|
10646
|
+
effectiveFps: Number(activeLiveStream.fps || normalized.fps),
|
|
10647
|
+
requestedMaxWidth: normalized.maxWidth,
|
|
10648
|
+
requestedMaxHeight: normalized.maxHeight,
|
|
10649
|
+
effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
10650
|
+
effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
|
|
10651
|
+
});
|
|
10652
|
+
return sharedLiveProfileResult(device, activeLiveStream, normalized);
|
|
10653
|
+
}
|
|
9759
10654
|
if (activeLiveStream
|
|
9760
10655
|
&& options.forceRestart !== true
|
|
9761
10656
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
@@ -9924,9 +10819,15 @@ export function createRemoteHub(options = {}) {
|
|
|
9924
10819
|
};
|
|
9925
10820
|
}
|
|
9926
10821
|
|
|
9927
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
9928
|
-
const device = devices.get(String(deviceId || ''));
|
|
9929
|
-
|
|
10822
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
10823
|
+
const device = devices.get(String(deviceId || ''));
|
|
10824
|
+
if (device?.synthetic !== true
|
|
10825
|
+
&& device?.connected
|
|
10826
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10827
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10828
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10829
|
+
}
|
|
10830
|
+
const stopAll = options.stopAll === true;
|
|
9930
10831
|
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24).toLowerCase();
|
|
9931
10832
|
const requestedStreamId = safeString(options.streamId, 128);
|
|
9932
10833
|
const purposeStreams = !stopAll && !requestedStreamId && streamPurpose && device
|
|
@@ -10527,12 +11428,18 @@ export function createRemoteHub(options = {}) {
|
|
|
10527
11428
|
};
|
|
10528
11429
|
}
|
|
10529
11430
|
|
|
10530
|
-
function startAudioStream(deviceId, options = {}) {
|
|
10531
|
-
const device = devices.get(String(deviceId || ''));
|
|
10532
|
-
if (!device) {
|
|
10533
|
-
return { ok: false, error: 'device-not-found' };
|
|
10534
|
-
}
|
|
10535
|
-
if (
|
|
11431
|
+
function startAudioStream(deviceId, options = {}) {
|
|
11432
|
+
const device = devices.get(String(deviceId || ''));
|
|
11433
|
+
if (!device) {
|
|
11434
|
+
return { ok: false, error: 'device-not-found' };
|
|
11435
|
+
}
|
|
11436
|
+
if (device.synthetic !== true
|
|
11437
|
+
&& device.connected
|
|
11438
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11439
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11440
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11441
|
+
}
|
|
11442
|
+
if (!device.connected) {
|
|
10536
11443
|
return { ok: false, error: 'device-not-connected' };
|
|
10537
11444
|
}
|
|
10538
11445
|
const denied = policyError(device, 'allowRemoteAudio');
|
|
@@ -10609,12 +11516,18 @@ export function createRemoteHub(options = {}) {
|
|
|
10609
11516
|
return { ok: true, commandId, streamId };
|
|
10610
11517
|
}
|
|
10611
11518
|
|
|
10612
|
-
function stopAudioStream(deviceId, options = {}) {
|
|
10613
|
-
const device = devices.get(String(deviceId || ''));
|
|
10614
|
-
if (!device) {
|
|
10615
|
-
return { ok: false, error: 'device-not-found' };
|
|
10616
|
-
}
|
|
10617
|
-
if (
|
|
11519
|
+
function stopAudioStream(deviceId, options = {}) {
|
|
11520
|
+
const device = devices.get(String(deviceId || ''));
|
|
11521
|
+
if (!device) {
|
|
11522
|
+
return { ok: false, error: 'device-not-found' };
|
|
11523
|
+
}
|
|
11524
|
+
if (device.synthetic !== true
|
|
11525
|
+
&& device.connected
|
|
11526
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11527
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11528
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11529
|
+
}
|
|
11530
|
+
if (!device.connected) {
|
|
10618
11531
|
return { ok: false, error: 'device-not-connected' };
|
|
10619
11532
|
}
|
|
10620
11533
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
@@ -10827,10 +11740,14 @@ export function createRemoteHub(options = {}) {
|
|
|
10827
11740
|
replacementTimerCount: device.liveStreamReplacementTimers instanceof Map
|
|
10828
11741
|
? device.liveStreamReplacementTimers.size
|
|
10829
11742
|
: 0,
|
|
10830
|
-
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
10831
|
-
.filter(waiter => waiter.deviceId === device.deviceId
|
|
10832
|
-
&& waiter.sessionId === device.sessionId)
|
|
10833
|
-
.length,
|
|
11743
|
+
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11744
|
+
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11745
|
+
&& waiter.sessionId === device.sessionId)
|
|
11746
|
+
.length,
|
|
11747
|
+
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11748
|
+
.filter(operation => operation.deviceId === device.deviceId
|
|
11749
|
+
&& operation.sessionId === device.sessionId)
|
|
11750
|
+
.length,
|
|
10834
11751
|
retainedPayloadCount: retainedPayloads.size,
|
|
10835
11752
|
retainedPayloadBytes: [...retainedPayloads].reduce(
|
|
10836
11753
|
(total, payload) => total + payload.length,
|
|
@@ -10840,8 +11757,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10840
11757
|
&& snapshot.stopPendingCount === 0
|
|
10841
11758
|
&& snapshot.pendingStartCount === 0
|
|
10842
11759
|
&& snapshot.pendingStopPromiseCount === 0
|
|
10843
|
-
&& snapshot.replacementTimerCount === 0
|
|
10844
|
-
&& snapshot.commandResultWaiterCount === 0
|
|
11760
|
+
&& snapshot.replacementTimerCount === 0
|
|
11761
|
+
&& snapshot.commandResultWaiterCount === 0
|
|
11762
|
+
&& snapshot.clipboardOperationCount === 0
|
|
10845
11763
|
&& snapshot.retainedPayloadCount === 0
|
|
10846
11764
|
&& snapshot.retainedPayloadBytes === 0;
|
|
10847
11765
|
return snapshot;
|
|
@@ -10853,8 +11771,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10853
11771
|
'stopPendingCount',
|
|
10854
11772
|
'pendingStartCount',
|
|
10855
11773
|
'pendingStopPromiseCount',
|
|
10856
|
-
'replacementTimerCount',
|
|
10857
|
-
'commandResultWaiterCount',
|
|
11774
|
+
'replacementTimerCount',
|
|
11775
|
+
'commandResultWaiterCount',
|
|
11776
|
+
'clipboardOperationCount',
|
|
10858
11777
|
'retainedPayloadCount',
|
|
10859
11778
|
'retainedPayloadBytes'
|
|
10860
11779
|
]) {
|
|
@@ -10867,9 +11786,10 @@ export function createRemoteHub(options = {}) {
|
|
|
10867
11786
|
stopPendingCount: 0,
|
|
10868
11787
|
pendingStartCount: 0,
|
|
10869
11788
|
pendingStopPromiseCount: 0,
|
|
10870
|
-
replacementTimerCount: 0,
|
|
10871
|
-
commandResultWaiterCount: 0,
|
|
10872
|
-
|
|
11789
|
+
replacementTimerCount: 0,
|
|
11790
|
+
commandResultWaiterCount: 0,
|
|
11791
|
+
clipboardOperationCount: 0,
|
|
11792
|
+
retainedPayloadCount: 0,
|
|
10873
11793
|
retainedPayloadBytes: 0
|
|
10874
11794
|
});
|
|
10875
11795
|
return {
|
|
@@ -10909,8 +11829,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10909
11829
|
listDeviceFrames,
|
|
10910
11830
|
disconnectDevice,
|
|
10911
11831
|
assignDeviceSlot,
|
|
10912
|
-
sendCommand,
|
|
10913
|
-
|
|
11832
|
+
sendCommand,
|
|
11833
|
+
sendClipboardCommand,
|
|
11834
|
+
refreshDevicePolicies,
|
|
10914
11835
|
sendLegacyClientUpdate,
|
|
10915
11836
|
sendInputControl,
|
|
10916
11837
|
releaseInputOwner,
|