@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/server.js
CHANGED
|
@@ -7,9 +7,14 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, wri
|
|
|
7
7
|
import { dirname, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import os from 'node:os';
|
|
10
|
-
import { WebSocketServer } from 'ws';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
11
|
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
REMOTE_CLIPBOARD_INPUT_ACTIONS,
|
|
14
|
+
RemoteClipboardContractError,
|
|
15
|
+
normalizeRemoteClipboardRequest
|
|
16
|
+
} from './remote-clipboard-contract.mjs';
|
|
17
|
+
import { createHubConsoleRelay } from './console-relay.js';
|
|
13
18
|
import {
|
|
14
19
|
buildImmutableRemoteFramePacket,
|
|
15
20
|
createRemoteFramePacketMetrics,
|
|
@@ -40,9 +45,9 @@ import { createAgentDeviceScope, resolveAgentTargetIds } from './agents/agent-de
|
|
|
40
45
|
import { AgentRuntimeError } from './agents/agent-runtime-error.js';
|
|
41
46
|
import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
|
|
42
47
|
import { createAgentPermissionStore } from './agents/agent-permission-store.js';
|
|
43
|
-
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
44
|
-
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
45
|
-
import { enrichAgentTaskResults } from './agents/agent-result-enrichment.js';
|
|
48
|
+
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
49
|
+
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
50
|
+
import { enrichAgentTaskResults } from './agents/agent-result-enrichment.js';
|
|
46
51
|
import { LiveDeskSettingsStore, SettingsConflictError } from './settings/settings-store.js';
|
|
47
52
|
import { effectiveDevicePolicy } from './settings/settings-schema.js';
|
|
48
53
|
import { buildEffectiveDevicePolicy } from './settings/effective-device-policy.js';
|
|
@@ -204,22 +209,22 @@ let verifiedLicense = {
|
|
|
204
209
|
let frameClientSeq = 0;
|
|
205
210
|
let inputClientSeq = 0;
|
|
206
211
|
let audioClientSeq = 0;
|
|
207
|
-
let liveDeskUpdateManager = null;
|
|
208
|
-
let hubTransferJobs = null;
|
|
209
|
-
let hubConsoleRelay = null;
|
|
212
|
+
let liveDeskUpdateManager = null;
|
|
213
|
+
let hubTransferJobs = null;
|
|
214
|
+
let hubConsoleRelay = null;
|
|
210
215
|
const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
|
|
211
216
|
const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
|
|
212
217
|
readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
|
|
213
218
|
Math.max(1000, HUB_HOST_TARGET_LEASE_MS - 1000)
|
|
214
219
|
));
|
|
215
|
-
const HUB_ACCESS_TOKEN_REFRESH_SKEW_MS = 90_000;
|
|
216
|
-
const hubWakeBaseUrl = String(process.env.LIVEDESK_WAKE_URL || 'https://livedesk-wake.lovecrdm.workers.dev').trim();
|
|
217
|
-
const hubConsoleRelayBaseUrl = String(
|
|
218
|
-
process.env.LIVEDESK_CONSOLE_RELAY_URL
|
|
219
|
-
|| (process.env.LIVEDESK_TEST_MODE === '1' || process.env.LIVEDESK_AUTH_TEST_MODE === '1'
|
|
220
|
-
? 'off'
|
|
221
|
-
: 'https://livedesk-wake.lovecrdm.workers.dev')
|
|
222
|
-
).trim();
|
|
220
|
+
const HUB_ACCESS_TOKEN_REFRESH_SKEW_MS = 90_000;
|
|
221
|
+
const hubWakeBaseUrl = String(process.env.LIVEDESK_WAKE_URL || 'https://livedesk-wake.lovecrdm.workers.dev').trim();
|
|
222
|
+
const hubConsoleRelayBaseUrl = String(
|
|
223
|
+
process.env.LIVEDESK_CONSOLE_RELAY_URL
|
|
224
|
+
|| (process.env.LIVEDESK_TEST_MODE === '1' || process.env.LIVEDESK_AUTH_TEST_MODE === '1'
|
|
225
|
+
? 'off'
|
|
226
|
+
: 'https://livedesk-wake.lovecrdm.workers.dev')
|
|
227
|
+
).trim();
|
|
223
228
|
const HUB_WAKE_NOTIFY_RETRY_MS = 60_000;
|
|
224
229
|
let hubHostTargetRenewTimer = null;
|
|
225
230
|
let hubHostTargetRenewInFlight = false;
|
|
@@ -513,19 +518,19 @@ async function getRuntimeAccessToken() {
|
|
|
513
518
|
const accessToken = String(runtimeAccessToken || '').trim();
|
|
514
519
|
const expiresSoon = runtimeAccessTokenExpiresAt > 0
|
|
515
520
|
&& runtimeAccessTokenExpiresAt <= Date.now() + HUB_ACCESS_TOKEN_REFRESH_SKEW_MS;
|
|
516
|
-
if (accessToken && !expiresSoon) {
|
|
517
|
-
return accessToken;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// The packaged Electron main process owns the encrypted refresh token and
|
|
521
|
-
// proactively sends each rotation to this child runtime. A second refresh
|
|
522
|
-
// owner here could consume the one-time token first and make the desktop
|
|
523
|
-
// shell appear signed out after sleep.
|
|
524
|
-
if (desktopMainAuthConfig) {
|
|
525
|
-
return accessToken;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const refreshToken = String(runtimeRefreshToken || '').trim();
|
|
521
|
+
if (accessToken && !expiresSoon) {
|
|
522
|
+
return accessToken;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// The packaged Electron main process owns the encrypted refresh token and
|
|
526
|
+
// proactively sends each rotation to this child runtime. A second refresh
|
|
527
|
+
// owner here could consume the one-time token first and make the desktop
|
|
528
|
+
// shell appear signed out after sleep.
|
|
529
|
+
if (desktopMainAuthConfig) {
|
|
530
|
+
return accessToken;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const refreshToken = String(runtimeRefreshToken || '').trim();
|
|
529
534
|
if (!refreshToken) {
|
|
530
535
|
return accessToken;
|
|
531
536
|
}
|
|
@@ -629,9 +634,9 @@ async function queryAuthoritativeRuntimeRole() {
|
|
|
629
634
|
},
|
|
630
635
|
SUPABASE_AUTH_TIMEOUT_MS
|
|
631
636
|
);
|
|
632
|
-
if (response.status === 401 || response.status === 403) {
|
|
633
|
-
if (!desktopMainAuthConfig) clearRuntimeSession();
|
|
634
|
-
throw new Error(`hub-role-query-not-authenticated:${response.status}`);
|
|
637
|
+
if (response.status === 401 || response.status === 403) {
|
|
638
|
+
if (!desktopMainAuthConfig) clearRuntimeSession();
|
|
639
|
+
throw new Error(`hub-role-query-not-authenticated:${response.status}`);
|
|
635
640
|
}
|
|
636
641
|
if (!response.ok) {
|
|
637
642
|
throw new Error(`hub-role-query-failed:${response.status}`);
|
|
@@ -813,12 +818,12 @@ function getLiveDeskUpdateStatus() {
|
|
|
813
818
|
}
|
|
814
819
|
|
|
815
820
|
if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
|
|
816
|
-
liveDeskUpdateManager = createLiveDeskUpdateManager({
|
|
817
|
-
remoteHub,
|
|
818
|
-
currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
|
|
819
|
-
currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
|
|
820
|
-
excludedDeviceIds: [runtimeDeviceId],
|
|
821
|
-
restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
|
|
821
|
+
liveDeskUpdateManager = createLiveDeskUpdateManager({
|
|
822
|
+
remoteHub,
|
|
823
|
+
currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
|
|
824
|
+
currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
|
|
825
|
+
excludedDeviceIds: [runtimeDeviceId],
|
|
826
|
+
restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
|
|
822
827
|
requestHubRestart
|
|
823
828
|
});
|
|
824
829
|
}
|
|
@@ -1278,16 +1283,16 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1278
1283
|
remoteHub.cancelTaskBatch(result.batchId);
|
|
1279
1284
|
return { ok: false, error: 'cancelled-by-user', batchId: result.batchId };
|
|
1280
1285
|
}
|
|
1281
|
-
const batch = remoteHub.getTaskBatch(result.batchId);
|
|
1282
|
-
if (!batch) return { ok: false, error: 'task-not-found', batchId: result.batchId };
|
|
1283
|
-
if (!['queued', 'running'].includes(batch.status)) {
|
|
1284
|
-
const taskResults = enrichAgentTaskResults({
|
|
1285
|
-
operation,
|
|
1286
|
-
results: batch.results,
|
|
1287
|
-
devices: remoteHub.listDevices({ includeDataUrl: false })
|
|
1288
|
-
.filter(device => targetIds.includes(device.deviceId))
|
|
1289
|
-
});
|
|
1290
|
-
return {
|
|
1286
|
+
const batch = remoteHub.getTaskBatch(result.batchId);
|
|
1287
|
+
if (!batch) return { ok: false, error: 'task-not-found', batchId: result.batchId };
|
|
1288
|
+
if (!['queued', 'running'].includes(batch.status)) {
|
|
1289
|
+
const taskResults = enrichAgentTaskResults({
|
|
1290
|
+
operation,
|
|
1291
|
+
results: batch.results,
|
|
1292
|
+
devices: remoteHub.listDevices({ includeDataUrl: false })
|
|
1293
|
+
.filter(device => targetIds.includes(device.deviceId))
|
|
1294
|
+
});
|
|
1295
|
+
return {
|
|
1291
1296
|
ok: true,
|
|
1292
1297
|
batchId: result.batchId,
|
|
1293
1298
|
operation,
|
|
@@ -1295,8 +1300,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1295
1300
|
total: batch.total,
|
|
1296
1301
|
completed: batch.completed,
|
|
1297
1302
|
failed: batch.failed,
|
|
1298
|
-
results: taskResults.map(item => ({ deviceId: item.deviceId, deviceName: item.deviceName, status: item.status, stage: item.stage, result: String(item.result || '').slice(0, 3000), data: item.data, error: String(item.error || '').slice(0, 500) }))
|
|
1299
|
-
};
|
|
1303
|
+
results: taskResults.map(item => ({ deviceId: item.deviceId, deviceName: item.deviceName, status: item.status, stage: item.stage, result: String(item.result || '').slice(0, 3000), data: item.data, error: String(item.error || '').slice(0, 500) }))
|
|
1304
|
+
};
|
|
1300
1305
|
}
|
|
1301
1306
|
await delayAgentMcp(200);
|
|
1302
1307
|
}
|
|
@@ -1356,16 +1361,16 @@ const hubSharedFolders = createHubSharedFolders({
|
|
|
1356
1361
|
dataDir: agentDataDir
|
|
1357
1362
|
});
|
|
1358
1363
|
|
|
1359
|
-
const app = express();
|
|
1360
|
-
const httpServer = createServer(app);
|
|
1361
|
-
hubConsoleRelay = createHubConsoleRelay({
|
|
1362
|
-
url: runtimeRole === 'hub' ? hubConsoleRelayBaseUrl : 'off',
|
|
1363
|
-
deviceId: runtimeDeviceId,
|
|
1364
|
-
httpBaseUrl: `http://127.0.0.1:${httpPort}`,
|
|
1365
|
-
getAccessToken: () => getRuntimeAccessToken(),
|
|
1366
|
-
logger: console
|
|
1367
|
-
});
|
|
1368
|
-
const httpConnections = new Set();
|
|
1364
|
+
const app = express();
|
|
1365
|
+
const httpServer = createServer(app);
|
|
1366
|
+
hubConsoleRelay = createHubConsoleRelay({
|
|
1367
|
+
url: runtimeRole === 'hub' ? hubConsoleRelayBaseUrl : 'off',
|
|
1368
|
+
deviceId: runtimeDeviceId,
|
|
1369
|
+
httpBaseUrl: `http://127.0.0.1:${httpPort}`,
|
|
1370
|
+
getAccessToken: () => getRuntimeAccessToken(),
|
|
1371
|
+
logger: console
|
|
1372
|
+
});
|
|
1373
|
+
const httpConnections = new Set();
|
|
1369
1374
|
const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1370
1375
|
const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1371
1376
|
const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
@@ -1754,12 +1759,40 @@ function normalizeLiveOptions(payload = {}) {
|
|
|
1754
1759
|
};
|
|
1755
1760
|
}
|
|
1756
1761
|
|
|
1757
|
-
function sendJson(ws, payload) {
|
|
1762
|
+
function sendJson(ws, payload) {
|
|
1758
1763
|
if (!ws || ws.readyState !== 1) {
|
|
1759
1764
|
return false;
|
|
1760
1765
|
}
|
|
1761
1766
|
return safeWebSocketSend(ws, JSON.stringify(payload));
|
|
1762
|
-
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function findLiveRemoteInputClient(hubConnectionId) {
|
|
1770
|
+
const requestedId = String(hubConnectionId || '').trim();
|
|
1771
|
+
if (!requestedId) {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
for (const ws of inputClients) {
|
|
1775
|
+
if (ws.readyState === 1 && ws.liveDeskInputClientId === requestedId) {
|
|
1776
|
+
return ws;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function clipboardHttpStatus(error) {
|
|
1783
|
+
const code = String(error || '');
|
|
1784
|
+
if (code.includes('blocked-by-settings') || code === 'remote-access-blocked-by-settings') return 403;
|
|
1785
|
+
if (code.includes('too-large')) return 413;
|
|
1786
|
+
if (code.includes('unavailable') || code === 'device-not-connected') return 503;
|
|
1787
|
+
if (code.startsWith('STALE_')
|
|
1788
|
+
|| code.includes('owner')
|
|
1789
|
+
|| code.includes('not-ready')
|
|
1790
|
+
|| code.includes('not-found')
|
|
1791
|
+
|| code.includes('already-exists')
|
|
1792
|
+
|| code.includes('busy')
|
|
1793
|
+
|| code.includes('capacity')) return 409;
|
|
1794
|
+
return 400;
|
|
1795
|
+
}
|
|
1763
1796
|
|
|
1764
1797
|
function forgetWebSocketClient(ws) {
|
|
1765
1798
|
unregisterFrameClient(ws);
|
|
@@ -1846,6 +1879,26 @@ function hasOtherFrameStreamOwner(ws, deviceId, streamId, streamPurpose = '') {
|
|
|
1846
1879
|
return false;
|
|
1847
1880
|
}
|
|
1848
1881
|
|
|
1882
|
+
function hasOtherFrameStreamDemand(ws, deviceId, streamPurpose = '') {
|
|
1883
|
+
const normalizedPurpose = String(streamPurpose || '').trim().toLowerCase();
|
|
1884
|
+
if (!deviceId || !normalizedPurpose) {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
for (const candidate of frameClients) {
|
|
1888
|
+
if (candidate === ws || candidate.readyState !== candidate.OPEN) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (candidate.liveDeskAutoStart === true
|
|
1892
|
+
&& candidate.liveDeskDeviceIds instanceof Set
|
|
1893
|
+
&& candidate.liveDeskDeviceIds.has(deviceId)
|
|
1894
|
+
&& String(candidate.liveDeskLiveOptions?.streamPurpose || '').trim().toLowerCase()
|
|
1895
|
+
=== normalizedPurpose) {
|
|
1896
|
+
return true;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1849
1902
|
function frameStreamStopKey(deviceId, streamId, streamPurpose) {
|
|
1850
1903
|
return `${deviceId}\u0000${streamId}\u0000${streamPurpose}`;
|
|
1851
1904
|
}
|
|
@@ -2147,6 +2200,15 @@ function snapshotFrameLaneResourceHealth() {
|
|
|
2147
2200
|
.map(binding => String(binding?.streamPurpose || '').trim().toLowerCase())
|
|
2148
2201
|
.filter(Boolean))]
|
|
2149
2202
|
.slice(0, 4),
|
|
2203
|
+
requestedProfile: ws.liveDeskLiveOptions ? {
|
|
2204
|
+
frameMode: String(ws.liveDeskLiveOptions.frameMode || ws.liveDeskLiveOptions.mode || ''),
|
|
2205
|
+
streamPurpose: String(ws.liveDeskLiveOptions.streamPurpose || ''),
|
|
2206
|
+
fps: Math.max(0, Number(ws.liveDeskLiveOptions.fps || 0)),
|
|
2207
|
+
maxWidth: Math.max(0, Number(ws.liveDeskLiveOptions.maxWidth || 0)),
|
|
2208
|
+
maxHeight: Math.max(0, Number(ws.liveDeskLiveOptions.maxHeight || 0)),
|
|
2209
|
+
quality: Math.max(0, Number(ws.liveDeskLiveOptions.quality || 0))
|
|
2210
|
+
} : null,
|
|
2211
|
+
sharedProfileReuseCount: Math.max(0, Number(ws.liveDeskSharedProfileReuseCount || 0)),
|
|
2150
2212
|
queueDepth: Math.max(0, Number(lane?.queue?.length || 0)),
|
|
2151
2213
|
queueLimit: frameClientQueueLimit(ws),
|
|
2152
2214
|
queueHighWaterPackets: Math.max(0, Number(lane?.queuedPacketsHighWater || 0)),
|
|
@@ -2939,9 +3001,23 @@ function startFrameSubscriptionLive(
|
|
|
2939
3001
|
monitorIndex,
|
|
2940
3002
|
reuseExisting: liveOptions.forceRestart !== true
|
|
2941
3003
|
&& (liveOptions.reuseExisting === true || reason === 'subscribe' || reason === 'watchdog'),
|
|
3004
|
+
// Wall capture is one shared native encoder per device. Two open browser
|
|
3005
|
+
// views may ask for different soft profiles (for example mobile 5 fps at
|
|
3006
|
+
// 1080p and desktop 20 fps at 540p). Once a healthy shared owner exists,
|
|
3007
|
+
// those preferences must bind to that owner instead of replacing it back
|
|
3008
|
+
// and forth every time either view refreshes its subscription.
|
|
3009
|
+
reuseSharedExisting: liveOptions.forceRestart !== true
|
|
3010
|
+
&& String(liveOptions.streamPurpose || '').trim().toLowerCase() === 'wall'
|
|
3011
|
+
&& hasOtherFrameStreamDemand(ws, deviceId, 'wall'),
|
|
2942
3012
|
silentReuse: reason === 'watchdog' && liveOptions.forceRestart !== true
|
|
2943
3013
|
});
|
|
2944
3014
|
if (result?.ok) {
|
|
3015
|
+
if (result.sharedProfileReused === true) {
|
|
3016
|
+
ws.liveDeskSharedProfileReuseCount = Math.max(
|
|
3017
|
+
0,
|
|
3018
|
+
Number(ws.liveDeskSharedProfileReuseCount || 0)
|
|
3019
|
+
) + 1;
|
|
3020
|
+
}
|
|
2945
3021
|
frameCaptureTransitionRetries.complete(ws, deviceId, intentGeneration, 'capture-started');
|
|
2946
3022
|
const expectedBinding = {
|
|
2947
3023
|
deviceId,
|
|
@@ -4249,10 +4325,10 @@ app.get('/api/remote/status', (_req, res) => {
|
|
|
4249
4325
|
runtimeRole,
|
|
4250
4326
|
deviceId: runtimeDeviceId,
|
|
4251
4327
|
deviceName: runtimeDeviceName,
|
|
4252
|
-
roleSource: runtimeRoleSource,
|
|
4253
|
-
agentPackage: '@livedesk/client',
|
|
4254
|
-
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
|
|
4255
|
-
frameLanes: snapshotFrameLaneResourceHealth(),
|
|
4328
|
+
roleSource: runtimeRoleSource,
|
|
4329
|
+
agentPackage: '@livedesk/client',
|
|
4330
|
+
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
|
|
4331
|
+
frameLanes: snapshotFrameLaneResourceHealth(),
|
|
4256
4332
|
update: getLiveDeskUpdateStatus()
|
|
4257
4333
|
});
|
|
4258
4334
|
});
|
|
@@ -4484,11 +4560,11 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
4484
4560
|
csrfToken: uiSession.csrfToken,
|
|
4485
4561
|
expiresAt: uiSession.expiresAt
|
|
4486
4562
|
}
|
|
4487
|
-
});
|
|
4488
|
-
if (runtimeRole === 'hub') {
|
|
4489
|
-
hubConsoleRelay?.refresh();
|
|
4490
|
-
scheduleAuthenticatedHubHostTargetPublication('session-received');
|
|
4491
|
-
}
|
|
4563
|
+
});
|
|
4564
|
+
if (runtimeRole === 'hub') {
|
|
4565
|
+
hubConsoleRelay?.refresh();
|
|
4566
|
+
scheduleAuthenticatedHubHostTargetPublication('session-received');
|
|
4567
|
+
}
|
|
4492
4568
|
} catch (error) {
|
|
4493
4569
|
const message = error instanceof Error ? error.message : String(error);
|
|
4494
4570
|
const status = authVerificationHttpStatus(message);
|
|
@@ -4508,11 +4584,11 @@ function getHubHostTargetLeaseStatus() {
|
|
|
4508
4584
|
...hubHostTargetLeaseState,
|
|
4509
4585
|
renewing: hubHostTargetRenewInFlight,
|
|
4510
4586
|
renewalIntervalMs: HUB_HOST_TARGET_RENEW_MS,
|
|
4511
|
-
leaseDurationMs: HUB_HOST_TARGET_LEASE_MS,
|
|
4512
|
-
authenticated: Boolean(runtimeAccessToken),
|
|
4513
|
-
timerActive: Boolean(hubHostTargetRenewTimer),
|
|
4514
|
-
wake: { ...hubWakeNotificationState },
|
|
4515
|
-
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' }
|
|
4587
|
+
leaseDurationMs: HUB_HOST_TARGET_LEASE_MS,
|
|
4588
|
+
authenticated: Boolean(runtimeAccessToken),
|
|
4589
|
+
timerActive: Boolean(hubHostTargetRenewTimer),
|
|
4590
|
+
wake: { ...hubWakeNotificationState },
|
|
4591
|
+
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' }
|
|
4516
4592
|
};
|
|
4517
4593
|
}
|
|
4518
4594
|
|
|
@@ -4609,9 +4685,9 @@ async function callHubSupabaseRpc(name, payload, accessToken) {
|
|
|
4609
4685
|
);
|
|
4610
4686
|
const data = await response.json().catch(() => null);
|
|
4611
4687
|
const result = Array.isArray(data) ? data[0] : data;
|
|
4612
|
-
if (response.status === 401 || response.status === 403) {
|
|
4613
|
-
if (!desktopMainAuthConfig) clearRuntimeSession();
|
|
4614
|
-
throw new Error(`${name}-not-authenticated:${response.status}`);
|
|
4688
|
+
if (response.status === 401 || response.status === 403) {
|
|
4689
|
+
if (!desktopMainAuthConfig) clearRuntimeSession();
|
|
4690
|
+
throw new Error(`${name}-not-authenticated:${response.status}`);
|
|
4615
4691
|
}
|
|
4616
4692
|
if (!response.ok) {
|
|
4617
4693
|
throw new Error(`${name}-failed:${response.status}`);
|
|
@@ -4816,11 +4892,11 @@ app.get('/api/hub/status', (_req, res) => {
|
|
|
4816
4892
|
...remoteHub.getStatus({ includeSecrets: false }),
|
|
4817
4893
|
role: 'hub',
|
|
4818
4894
|
deviceId: runtimeDeviceId,
|
|
4819
|
-
deviceName: runtimeDeviceName,
|
|
4820
|
-
roleSource: runtimeRoleSource,
|
|
4821
|
-
runtimeStarted: true,
|
|
4822
|
-
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
|
|
4823
|
-
hostTargetLease: getHubHostTargetLeaseStatus(),
|
|
4895
|
+
deviceName: runtimeDeviceName,
|
|
4896
|
+
roleSource: runtimeRoleSource,
|
|
4897
|
+
runtimeStarted: true,
|
|
4898
|
+
consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
|
|
4899
|
+
hostTargetLease: getHubHostTargetLeaseStatus(),
|
|
4824
4900
|
update: getLiveDeskUpdateStatus()
|
|
4825
4901
|
});
|
|
4826
4902
|
});
|
|
@@ -5106,12 +5182,64 @@ app.post('/api/remote/devices/:deviceId/status-diagnostic', (req, res) => {
|
|
|
5106
5182
|
}));
|
|
5107
5183
|
});
|
|
5108
5184
|
|
|
5109
|
-
app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
|
|
5110
|
-
noStore(res);
|
|
5111
|
-
res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
|
|
5112
|
-
});
|
|
5113
|
-
|
|
5114
|
-
app.
|
|
5185
|
+
app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
|
|
5186
|
+
noStore(res);
|
|
5187
|
+
res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
|
|
5188
|
+
});
|
|
5189
|
+
|
|
5190
|
+
app.post('/api/remote/devices/:deviceId/clipboard', requireHubFeatureAccess, async (req, res) => {
|
|
5191
|
+
noStore(res);
|
|
5192
|
+
try {
|
|
5193
|
+
const request = normalizeRemoteClipboardRequest(req.body || {});
|
|
5194
|
+
const inputClient = findLiveRemoteInputClient(request.hubConnectionId);
|
|
5195
|
+
if (!inputClient) {
|
|
5196
|
+
res.status(409).json({ ok: false, error: 'clipboard-input-owner-not-live' });
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
const deviceId = String(req.params.deviceId || '').trim();
|
|
5200
|
+
inputClient.liveDeskInputDeviceIds?.add?.(deviceId);
|
|
5201
|
+
|
|
5202
|
+
if (REMOTE_CLIPBOARD_INPUT_ACTIONS.has(request.action)) {
|
|
5203
|
+
const result = remoteHub.sendInputControl(deviceId, {
|
|
5204
|
+
type: request.action,
|
|
5205
|
+
clipboardOperationId: request.operationId,
|
|
5206
|
+
operationId: request.operationId,
|
|
5207
|
+
clipboardDirection: request.clipboardDirection,
|
|
5208
|
+
manifest: request.payload.manifest || null,
|
|
5209
|
+
contentKind: request.payload.contentKind || request.payload.manifest?.contentKind || '',
|
|
5210
|
+
controlSessionId: request.binding.controlSessionId,
|
|
5211
|
+
controlCommandId: request.binding.controlCommandId,
|
|
5212
|
+
captureGeneration: request.binding.captureGeneration,
|
|
5213
|
+
monitorIndex: request.binding.monitorIndex,
|
|
5214
|
+
hubConnectionId: request.hubConnectionId,
|
|
5215
|
+
requestAck: true
|
|
5216
|
+
});
|
|
5217
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json({
|
|
5218
|
+
ok: result.ok === true,
|
|
5219
|
+
action: request.action,
|
|
5220
|
+
operationId: request.operationId,
|
|
5221
|
+
input: result,
|
|
5222
|
+
...(result.ok ? {} : { error: result.error || 'clipboard-input-failed' })
|
|
5223
|
+
});
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
const result = await remoteHub.sendClipboardCommand(deviceId, request);
|
|
5228
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json(result);
|
|
5229
|
+
} catch (error) {
|
|
5230
|
+
const status = error instanceof RemoteClipboardContractError
|
|
5231
|
+
? error.status
|
|
5232
|
+
: 500;
|
|
5233
|
+
res.status(status).json({
|
|
5234
|
+
ok: false,
|
|
5235
|
+
error: error instanceof RemoteClipboardContractError
|
|
5236
|
+
? error.code
|
|
5237
|
+
: 'clipboard-request-failed'
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
});
|
|
5241
|
+
|
|
5242
|
+
app.get('/api/remote/filesystem/roots', requireHubFeatureAccess, async (req, res) => {
|
|
5115
5243
|
noStore(res);
|
|
5116
5244
|
try {
|
|
5117
5245
|
res.json(await hubFilesystem.getRoots({ refresh: /^(1|true|yes|on)$/i.test(String(req.query?.refresh || '')) }));
|
|
@@ -5827,14 +5955,14 @@ hubSharedFolders.startAutoSync(
|
|
|
5827
5955
|
() => remoteHub.listDevices({ includeDataUrl: false }).filter(device => device.connected).map(device => device.deviceId),
|
|
5828
5956
|
() => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/LiveDeskFiles'
|
|
5829
5957
|
);
|
|
5830
|
-
httpServer.listen(httpPort, httpHost, () => {
|
|
5958
|
+
httpServer.listen(httpPort, httpHost, () => {
|
|
5831
5959
|
const status = remoteHub.getStatus({ includeSecrets: true });
|
|
5832
5960
|
const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
|
|
5833
5961
|
console.log(`[LiveDesk Hub] Version ${managerVersion}`);
|
|
5834
|
-
console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
5835
|
-
console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
5836
|
-
hubConsoleRelay?.start();
|
|
5837
|
-
});
|
|
5962
|
+
console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
5963
|
+
console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
5964
|
+
hubConsoleRelay?.start();
|
|
5965
|
+
});
|
|
5838
5966
|
|
|
5839
5967
|
const roleWatchTimer = runtimeRole === 'hub'
|
|
5840
5968
|
? setInterval(() => { void watchAuthoritativeRuntimeRole(); }, 5000)
|
|
@@ -5915,10 +6043,10 @@ function shutdownHub(signal) {
|
|
|
5915
6043
|
const startedAt = Date.now();
|
|
5916
6044
|
console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
|
|
5917
6045
|
if (roleWatchTimer) clearInterval(roleWatchTimer);
|
|
5918
|
-
clearInterval(browserWebSocketHeartbeatTimer);
|
|
5919
|
-
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
5920
|
-
runSynchronousShutdownStep('mobile console relay close', () => hubConsoleRelay?.close());
|
|
5921
|
-
atlasClients.clear();
|
|
6046
|
+
clearInterval(browserWebSocketHeartbeatTimer);
|
|
6047
|
+
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
6048
|
+
runSynchronousShutdownStep('mobile console relay close', () => hubConsoleRelay?.close());
|
|
6049
|
+
atlasClients.clear();
|
|
5922
6050
|
runSynchronousShutdownStep('shared folder close', () => hubSharedFolders.close());
|
|
5923
6051
|
|
|
5924
6052
|
const httpClosed = new Promise(resolveClose => {
|
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
1
|
+
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
+
import { REMOTE_CLIPBOARD_LIMITS } from '../remote-clipboard-contract.mjs';
|
|
2
3
|
|
|
3
|
-
export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
|
|
4
|
-
const policy = effectiveDevicePolicy(settings);
|
|
5
|
-
|
|
4
|
+
export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
|
|
5
|
+
const policy = effectiveDevicePolicy(settings);
|
|
6
|
+
const clipboardV1 = capabilities.clipboardProtocol === 'livedesk.clipboard.v1';
|
|
7
|
+
const clipboardCommonLimits = clipboardV1
|
|
8
|
+
&& capabilities.sessionBoundSideChannels === true
|
|
9
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTextBytes))
|
|
10
|
+
&& Number(capabilities.clipboardMaxTextBytes) > 0
|
|
11
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxTotalBytes))
|
|
12
|
+
&& Number(capabilities.clipboardMaxTotalBytes) > 0
|
|
13
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxChunkBytes))
|
|
14
|
+
&& Number(capabilities.clipboardMaxChunkBytes) >= REMOTE_CLIPBOARD_LIMITS.maxChunkBytes;
|
|
15
|
+
const clipboardMaxImageBytes = Number(
|
|
16
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes);
|
|
17
|
+
return {
|
|
6
18
|
...policy,
|
|
7
|
-
deviceId: String(deviceId || ''),
|
|
8
|
-
supported: {
|
|
9
|
-
control: capabilities.control !== false,
|
|
10
|
-
clipboardText: capabilities.clipboardText
|
|
11
|
-
|
|
19
|
+
deviceId: String(deviceId || ''),
|
|
20
|
+
supported: {
|
|
21
|
+
control: capabilities.control !== false,
|
|
22
|
+
clipboardText: clipboardCommonLimits && capabilities.clipboardText === true,
|
|
23
|
+
clipboardImage: clipboardCommonLimits
|
|
24
|
+
&& capabilities.clipboardImage === true
|
|
25
|
+
&& Number.isSafeInteger(clipboardMaxImageBytes)
|
|
26
|
+
&& clipboardMaxImageBytes > 0,
|
|
27
|
+
clipboardFiles: clipboardCommonLimits
|
|
28
|
+
&& capabilities.clipboardFiles === true
|
|
29
|
+
&& Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
30
|
+
&& Number(capabilities.clipboardMaxFiles) > 0,
|
|
31
|
+
clipboardCopy: clipboardCommonLimits && capabilities.clipboardCopy === true,
|
|
32
|
+
clipboardPaste: clipboardCommonLimits && capabilities.clipboardPaste === true,
|
|
33
|
+
fileTransfer: capabilities.fileTransfer !== false,
|
|
12
34
|
remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
|
|
13
35
|
agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
|
|
14
36
|
}
|
|
@@ -13,7 +13,8 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
|
13
13
|
approveNewDevices: true,
|
|
14
14
|
startWithComputer: true,
|
|
15
15
|
keepRunningInTray: true,
|
|
16
|
-
showConnectionNotifications: true
|
|
16
|
+
showConnectionNotifications: true,
|
|
17
|
+
showThisComputer: false
|
|
17
18
|
},
|
|
18
19
|
security: {
|
|
19
20
|
accessMode: 'trusted-only',
|
|
@@ -157,7 +158,7 @@ const numbers = (entries) => Object.fromEntries(entries.map(([key, min, max]) =>
|
|
|
157
158
|
|
|
158
159
|
const RULES = {
|
|
159
160
|
connection: {
|
|
160
|
-
...bools(['usePinToAddDevices', 'rotatePinAfterPairing', 'allowNewDevices', 'approveNewDevices', 'startWithComputer', 'keepRunningInTray', 'showConnectionNotifications']),
|
|
161
|
+
...bools(['usePinToAddDevices', 'rotatePinAfterPairing', 'allowNewDevices', 'approveNewDevices', 'startWithComputer', 'keepRunningInTray', 'showConnectionNotifications', 'showThisComputer']),
|
|
161
162
|
...numbers([['pinValidityMinutes', 10, 1440]])
|
|
162
163
|
},
|
|
163
164
|
security: {
|