@livedesk/hub 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/agents/agent-manager.js +2 -2
- package/src/agents/agent-permissions.js +2 -2
- package/src/agents/agent-tool-registry.js +30 -4
- package/src/agents/codex-agent-runtime.js +16 -16
- package/src/agents/codex-mcp-server.js +5 -5
- package/src/filesystem/roots.js +20 -20
- package/src/filesystem/shared-folders.js +2 -2
- package/src/live-desk-update.js +9 -9
- package/src/remote-clipboard-contract.mjs +482 -482
- package/src/remote-hub.js +1022 -1020
- package/src/server.js +143 -141
- package/src/settings/effective-device-policy.js +31 -31
- package/src/settings/settings-schema.js +2 -2
package/src/server.js
CHANGED
|
@@ -8,12 +8,12 @@ import { dirname, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
|
-
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
-
import {
|
|
13
|
-
REMOTE_CLIPBOARD_INPUT_ACTIONS,
|
|
14
|
-
RemoteClipboardContractError,
|
|
15
|
-
normalizeRemoteClipboardRequest
|
|
16
|
-
} from './remote-clipboard-contract.mjs';
|
|
11
|
+
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
+
import {
|
|
13
|
+
REMOTE_CLIPBOARD_INPUT_ACTIONS,
|
|
14
|
+
RemoteClipboardContractError,
|
|
15
|
+
normalizeRemoteClipboardRequest
|
|
16
|
+
} from './remote-clipboard-contract.mjs';
|
|
17
17
|
import { createHubConsoleRelay } from './console-relay.js';
|
|
18
18
|
import {
|
|
19
19
|
buildImmutableRemoteFramePacket,
|
|
@@ -151,7 +151,7 @@ const atlasPool = new Mode4AtlasPool({
|
|
|
151
151
|
onStatus: status => {
|
|
152
152
|
if (status?.type !== 'log') return;
|
|
153
153
|
const logger = status.level === 'warn' ? console.warn : console.log;
|
|
154
|
-
logger(`[
|
|
154
|
+
logger(`[VuvoDesk Hub] ${status.message}`);
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
const inputClients = new Set();
|
|
@@ -266,7 +266,7 @@ function handleRemoteHubEvent(type, event) {
|
|
|
266
266
|
hubTransferJobs?.handleRemoteEvent(type, event);
|
|
267
267
|
if (traceRemoteTestEventsEnabled
|
|
268
268
|
&& (type === 'RemoteFrameDropped' || type === 'RemoteFrameTransportProofAccepted')) {
|
|
269
|
-
console.warn(`[
|
|
269
|
+
console.warn(`[VuvoDesk Hub Test Event] ${type} ${JSON.stringify({
|
|
270
270
|
reason: event?.reason || '',
|
|
271
271
|
deviceId: event?.deviceId || event?.device?.deviceId || '',
|
|
272
272
|
proofOnly: event?.proofOnly === true,
|
|
@@ -572,7 +572,7 @@ async function getRuntimeAccessToken() {
|
|
|
572
572
|
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
573
573
|
});
|
|
574
574
|
} catch (error) {
|
|
575
|
-
console.warn(`[
|
|
575
|
+
console.warn(`[VuvoDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
576
576
|
}
|
|
577
577
|
return runtimeAccessToken;
|
|
578
578
|
}
|
|
@@ -658,7 +658,7 @@ async function watchAuthoritativeRuntimeRole() {
|
|
|
658
658
|
try {
|
|
659
659
|
const role = await queryAuthoritativeRuntimeRole();
|
|
660
660
|
if (role === 'client') {
|
|
661
|
-
console.warn('[
|
|
661
|
+
console.warn('[VuvoDesk Hub] Supabase selected another Sync Server. Transitioning this runtime to Client.');
|
|
662
662
|
void clearHubHostTarget('role-demoted').finally(() => {
|
|
663
663
|
setTimeout(() => process.exit(ROLE_TRANSITION_EXIT_CODE), 150);
|
|
664
664
|
});
|
|
@@ -692,16 +692,16 @@ const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
|
692
692
|
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
693
693
|
const captureStore = new CaptureStore({ dataDir: agentDataDir });
|
|
694
694
|
void captureStore.initialize().catch(error => {
|
|
695
|
-
console.warn(`[
|
|
695
|
+
console.warn(`[VuvoDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
696
696
|
});
|
|
697
697
|
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
698
|
-
console.warn(`[
|
|
698
|
+
console.warn(`[VuvoDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
699
699
|
});
|
|
700
700
|
|
|
701
701
|
const udpTransport = createHubUdpTransport({
|
|
702
702
|
env: process.env,
|
|
703
|
-
logEvent: (_scope, message) => console.log(`[
|
|
704
|
-
logWarn: (_scope, message) => console.warn(`[
|
|
703
|
+
logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
|
|
704
|
+
logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`)
|
|
705
705
|
});
|
|
706
706
|
|
|
707
707
|
const remoteHub = createRemoteHub({
|
|
@@ -713,8 +713,8 @@ const remoteHub = createRemoteHub({
|
|
|
713
713
|
MINDEXEC_MANAGER_VERSION: packageInfo.version
|
|
714
714
|
},
|
|
715
715
|
udpTransport,
|
|
716
|
-
logEvent: (_scope, message) => console.log(`[
|
|
717
|
-
logWarn: (_scope, message) => console.warn(`[
|
|
716
|
+
logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
|
|
717
|
+
logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`),
|
|
718
718
|
emitEvent: handleRemoteHubEvent,
|
|
719
719
|
emitFrame: broadcastRemoteBinaryFrame,
|
|
720
720
|
emitAudio: broadcastRemoteBinaryAudio,
|
|
@@ -870,7 +870,7 @@ if (process.env.LIVEDESK_UPDATE_CONTINUE === '1') {
|
|
|
870
870
|
clearInterval(continueUpdateTimer);
|
|
871
871
|
}
|
|
872
872
|
} catch (error) {
|
|
873
|
-
console.warn(`[
|
|
873
|
+
console.warn(`[VuvoDesk Hub] automatic client update continuation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
874
874
|
clearInterval(continueUpdateTimer);
|
|
875
875
|
} finally {
|
|
876
876
|
continueUpdateInFlight = false;
|
|
@@ -1169,6 +1169,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1169
1169
|
'livedesk.write_file': 'file.write',
|
|
1170
1170
|
'livedesk.delete_file': 'file.delete',
|
|
1171
1171
|
'livedesk.list_directory': 'file.list',
|
|
1172
|
+
'livedesk.search_files': 'file.search',
|
|
1173
|
+
'livedesk.create_directory': 'directory.create',
|
|
1172
1174
|
'livedesk.run_command': 'command.run',
|
|
1173
1175
|
'livedesk.run_script': 'script.run',
|
|
1174
1176
|
'livedesk.install_software': 'software.install',
|
|
@@ -1253,8 +1255,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1253
1255
|
? String(args.serviceName || '').replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
|
|
1254
1256
|
: '';
|
|
1255
1257
|
const result = remoteHub.requestAgentTaskBatch(targetIds, {
|
|
1256
|
-
instruction: `${tool.readOnly ? `
|
|
1257
|
-
title: `
|
|
1258
|
+
instruction: `${tool.readOnly ? `VuvoDesk read-only ${operation}` : `VuvoDesk ${name}`}${targetQuery ? ` for ${targetQuery}` : ''}. Return only the collected result.`,
|
|
1259
|
+
title: `VuvoDesk ${operation}`,
|
|
1258
1260
|
operation,
|
|
1259
1261
|
targetQuery,
|
|
1260
1262
|
approvalLevel: tool.readOnly ? 'read-only' : 'task-only',
|
|
@@ -1328,7 +1330,7 @@ const codexRuntime = createCodexAgentRuntime({
|
|
|
1328
1330
|
const detail = ['failed', 'recovering'].includes(progress.stage) && progress.message
|
|
1329
1331
|
? ` detail=${JSON.stringify(String(progress.message).slice(0, 800))}`
|
|
1330
1332
|
: '';
|
|
1331
|
-
console.log(`[
|
|
1333
|
+
console.log(`[VuvoDesk Hub] Agent ${progress.stage} run=${progress.runId}${tool} clients=${delivery.sent}/${delivery.total}${detail}`);
|
|
1332
1334
|
}
|
|
1333
1335
|
});
|
|
1334
1336
|
const agentManager = createAgentManager({
|
|
@@ -1759,40 +1761,40 @@ function normalizeLiveOptions(payload = {}) {
|
|
|
1759
1761
|
};
|
|
1760
1762
|
}
|
|
1761
1763
|
|
|
1762
|
-
function sendJson(ws, payload) {
|
|
1764
|
+
function sendJson(ws, payload) {
|
|
1763
1765
|
if (!ws || ws.readyState !== 1) {
|
|
1764
1766
|
return false;
|
|
1765
1767
|
}
|
|
1766
1768
|
return safeWebSocketSend(ws, JSON.stringify(payload));
|
|
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
|
-
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function findLiveRemoteInputClient(hubConnectionId) {
|
|
1772
|
+
const requestedId = String(hubConnectionId || '').trim();
|
|
1773
|
+
if (!requestedId) {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
for (const ws of inputClients) {
|
|
1777
|
+
if (ws.readyState === 1 && ws.liveDeskInputClientId === requestedId) {
|
|
1778
|
+
return ws;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function clipboardHttpStatus(error) {
|
|
1785
|
+
const code = String(error || '');
|
|
1786
|
+
if (code.includes('blocked-by-settings') || code === 'remote-access-blocked-by-settings') return 403;
|
|
1787
|
+
if (code.includes('too-large')) return 413;
|
|
1788
|
+
if (code.includes('unavailable') || code === 'device-not-connected') return 503;
|
|
1789
|
+
if (code.startsWith('STALE_')
|
|
1790
|
+
|| code.includes('owner')
|
|
1791
|
+
|| code.includes('not-ready')
|
|
1792
|
+
|| code.includes('not-found')
|
|
1793
|
+
|| code.includes('already-exists')
|
|
1794
|
+
|| code.includes('busy')
|
|
1795
|
+
|| code.includes('capacity')) return 409;
|
|
1796
|
+
return 400;
|
|
1797
|
+
}
|
|
1796
1798
|
|
|
1797
1799
|
function forgetWebSocketClient(ws) {
|
|
1798
1800
|
unregisterFrameClient(ws);
|
|
@@ -1929,12 +1931,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
|
|
|
1929
1931
|
if (result?.stopPromise) {
|
|
1930
1932
|
void result.stopPromise.then(confirmation => {
|
|
1931
1933
|
if (confirmation?.captureStopConfirmed === true) {
|
|
1932
|
-
console.log(`[
|
|
1934
|
+
console.log(`[VuvoDesk Hub] confirmed ${streamPurpose} capture stopped after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1933
1935
|
return;
|
|
1934
1936
|
}
|
|
1935
1937
|
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1936
1938
|
console.warn(
|
|
1937
|
-
`[
|
|
1939
|
+
`[VuvoDesk Hub] ${streamPurpose} capture stop was not confirmed device=${deviceId} `
|
|
1938
1940
|
+ `stream=${streamId}: ${confirmation?.error || 'capture-stop-unconfirmed'}. `
|
|
1939
1941
|
+ 'Disconnecting the Agent so its final capture drain runs before reconnect.'
|
|
1940
1942
|
);
|
|
@@ -1942,12 +1944,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
|
|
|
1942
1944
|
}
|
|
1943
1945
|
}).catch(error => {
|
|
1944
1946
|
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1945
|
-
console.warn(`[
|
|
1947
|
+
console.warn(`[VuvoDesk Hub] ${streamPurpose} capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1946
1948
|
remoteHub.disconnectDevice(deviceId, 'frame-capture-stop-confirmation-failed');
|
|
1947
1949
|
}
|
|
1948
1950
|
});
|
|
1949
1951
|
} else if (result?.ok && !result.alreadyStopped) {
|
|
1950
|
-
console.log(`[
|
|
1952
|
+
console.log(`[VuvoDesk Hub] queued ${streamPurpose} capture stop after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1951
1953
|
}
|
|
1952
1954
|
}, frameStreamStopGraceMs);
|
|
1953
1955
|
pendingFrameStreamStops.set(key, { timer });
|
|
@@ -3746,11 +3748,11 @@ function stopUnsubscribedAudioOwners(owners, reason) {
|
|
|
3746
3748
|
disconnect: (targetDeviceId, disconnectReason) => (
|
|
3747
3749
|
remoteHub.disconnectDevice(targetDeviceId, disconnectReason)
|
|
3748
3750
|
),
|
|
3749
|
-
warn: message => console.warn(`[
|
|
3751
|
+
warn: message => console.warn(`[VuvoDesk Hub] ${message}. Disconnecting the Agent for a final drain.`)
|
|
3750
3752
|
});
|
|
3751
3753
|
if (result?.ok === false
|
|
3752
3754
|
&& !['device-not-found', 'device-not-connected', 'device-audio-unavailable'].includes(String(result.error || ''))) {
|
|
3753
|
-
console.warn(`[
|
|
3755
|
+
console.warn(`[VuvoDesk Hub] Could not stop unused remote audio capture device=${deviceId} reason=${reason}: ${result.error || 'unknown-error'}`);
|
|
3754
3756
|
}
|
|
3755
3757
|
}
|
|
3756
3758
|
}
|
|
@@ -3858,7 +3860,7 @@ function buildHubHealthPayload({
|
|
|
3858
3860
|
};
|
|
3859
3861
|
return {
|
|
3860
3862
|
ok: true,
|
|
3861
|
-
product: 'LiveDesk',
|
|
3863
|
+
product: 'LiveDesk',
|
|
3862
3864
|
timestamp: new Date().toISOString(),
|
|
3863
3865
|
persistentSessionGc,
|
|
3864
3866
|
memoryBeforeGc,
|
|
@@ -4321,7 +4323,7 @@ app.get('/api/remote/status', (_req, res) => {
|
|
|
4321
4323
|
res.json({
|
|
4322
4324
|
...remoteHub.getStatus({ includeSecrets: false }),
|
|
4323
4325
|
pairingPin: secretStatus.pairingPin,
|
|
4324
|
-
product: 'LiveDesk',
|
|
4326
|
+
product: 'LiveDesk',
|
|
4325
4327
|
runtimeRole,
|
|
4326
4328
|
deviceId: runtimeDeviceId,
|
|
4327
4329
|
deviceName: runtimeDeviceName,
|
|
@@ -4651,7 +4653,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
|
|
|
4651
4653
|
lastEventId: eventId,
|
|
4652
4654
|
nextAttemptAt: ''
|
|
4653
4655
|
};
|
|
4654
|
-
console.log(`[
|
|
4656
|
+
console.log(`[VuvoDesk Hub] Hub-online wake signal sent event=${eventId.slice(0, 8)} delivered=${Number(data?.delivered || 0)}.`);
|
|
4655
4657
|
return { ok: true, eventId, delivered: Number(data?.delivered || 0) };
|
|
4656
4658
|
} catch (error) {
|
|
4657
4659
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4662,7 +4664,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
|
|
|
4662
4664
|
lastError: message,
|
|
4663
4665
|
nextAttemptAt: new Date(hubWakeNextAttemptAtMs).toISOString()
|
|
4664
4666
|
};
|
|
4665
|
-
console.warn(`[
|
|
4667
|
+
console.warn(`[VuvoDesk Hub] Hub-online wake signal failed; Supabase target remains authoritative: ${message}`);
|
|
4666
4668
|
return { ok: false, error: message, eventId };
|
|
4667
4669
|
}
|
|
4668
4670
|
}
|
|
@@ -4712,7 +4714,7 @@ async function promoteCurrentRuntimeToHub() {
|
|
|
4712
4714
|
p_assigned_hub_id: null,
|
|
4713
4715
|
p_expected_role_version: null
|
|
4714
4716
|
}, accessToken);
|
|
4715
|
-
console.log(`[
|
|
4717
|
+
console.log(`[VuvoDesk Hub] Sync Server role confirmed for ${runtimeDeviceId}.`);
|
|
4716
4718
|
return result;
|
|
4717
4719
|
}
|
|
4718
4720
|
|
|
@@ -4782,12 +4784,12 @@ async function publishHubHostTarget({ takeover = false, reason = 'renewal' } = {
|
|
|
4782
4784
|
lastPublishedEndpoint: target.endpoint || remoteStatus.agentEndpoint || '',
|
|
4783
4785
|
lastExpiresAt: target.expiresAt || ''
|
|
4784
4786
|
});
|
|
4785
|
-
console.log(`[
|
|
4787
|
+
console.log(`[VuvoDesk Hub] Host target ${reason} published endpoint=${target.endpoint || remoteStatus.agentEndpoint} expires=${target.expiresAt || ''}.`);
|
|
4786
4788
|
return { ok: true, active: true, hostTarget: target, lease: getHubHostTargetLeaseStatus() };
|
|
4787
4789
|
} catch (error) {
|
|
4788
4790
|
const message = error instanceof Error ? error.message : String(error);
|
|
4789
4791
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message });
|
|
4790
|
-
console.error(`[
|
|
4792
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} publish failed: ${message}`);
|
|
4791
4793
|
return { ok: false, active: false, error: message, lease: getHubHostTargetLeaseStatus() };
|
|
4792
4794
|
} finally {
|
|
4793
4795
|
hubHostTargetRenewInFlight = false;
|
|
@@ -4831,7 +4833,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
|
|
|
4831
4833
|
}
|
|
4832
4834
|
} catch (cause) {
|
|
4833
4835
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
4834
|
-
console.error(`[
|
|
4836
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} clear failed: ${error}`);
|
|
4835
4837
|
}
|
|
4836
4838
|
try {
|
|
4837
4839
|
await remoteHub.setHostTarget({ enabled: false, nodeId: targetNodeId });
|
|
@@ -4846,7 +4848,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
|
|
|
4846
4848
|
lastExpiresAt: ''
|
|
4847
4849
|
});
|
|
4848
4850
|
if (!error) {
|
|
4849
|
-
console.log(`[
|
|
4851
|
+
console.log(`[VuvoDesk Hub] Host target cleared (${reason}).`);
|
|
4850
4852
|
}
|
|
4851
4853
|
return { ok: !error, error, lease: getHubHostTargetLeaseStatus() };
|
|
4852
4854
|
}
|
|
@@ -4867,7 +4869,7 @@ function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-receive
|
|
|
4867
4869
|
void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
|
|
4868
4870
|
const message = error instanceof Error ? error.message : String(error);
|
|
4869
4871
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message });
|
|
4870
|
-
console.error(`[
|
|
4872
|
+
console.error(`[VuvoDesk Hub] Host target ${reason} background publish failed: ${message}`);
|
|
4871
4873
|
});
|
|
4872
4874
|
});
|
|
4873
4875
|
}
|
|
@@ -4921,7 +4923,7 @@ app.post('/api/hub/sync', async (req, res) => {
|
|
|
4921
4923
|
} catch (error) {
|
|
4922
4924
|
const message = error instanceof Error ? error.message : String(error);
|
|
4923
4925
|
updateHubHostTargetLeaseState({ state: 'error', lastError: message, lastReason: 'sync-server' });
|
|
4924
|
-
console.error(`[
|
|
4926
|
+
console.error(`[VuvoDesk Hub] Sync Server failed: ${message}`);
|
|
4925
4927
|
res.status(409).json({ ok: false, error: message, lease: getHubHostTargetLeaseStatus() });
|
|
4926
4928
|
}
|
|
4927
4929
|
});
|
|
@@ -5182,64 +5184,64 @@ app.post('/api/remote/devices/:deviceId/status-diagnostic', (req, res) => {
|
|
|
5182
5184
|
}));
|
|
5183
5185
|
});
|
|
5184
5186
|
|
|
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) => {
|
|
5187
|
+
app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
|
|
5188
|
+
noStore(res);
|
|
5189
|
+
res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
|
|
5190
|
+
});
|
|
5191
|
+
|
|
5192
|
+
app.post('/api/remote/devices/:deviceId/clipboard', requireHubFeatureAccess, async (req, res) => {
|
|
5193
|
+
noStore(res);
|
|
5194
|
+
try {
|
|
5195
|
+
const request = normalizeRemoteClipboardRequest(req.body || {});
|
|
5196
|
+
const inputClient = findLiveRemoteInputClient(request.hubConnectionId);
|
|
5197
|
+
if (!inputClient) {
|
|
5198
|
+
res.status(409).json({ ok: false, error: 'clipboard-input-owner-not-live' });
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
const deviceId = String(req.params.deviceId || '').trim();
|
|
5202
|
+
inputClient.liveDeskInputDeviceIds?.add?.(deviceId);
|
|
5203
|
+
|
|
5204
|
+
if (REMOTE_CLIPBOARD_INPUT_ACTIONS.has(request.action)) {
|
|
5205
|
+
const result = remoteHub.sendInputControl(deviceId, {
|
|
5206
|
+
type: request.action,
|
|
5207
|
+
clipboardOperationId: request.operationId,
|
|
5208
|
+
operationId: request.operationId,
|
|
5209
|
+
clipboardDirection: request.clipboardDirection,
|
|
5210
|
+
manifest: request.payload.manifest || null,
|
|
5211
|
+
contentKind: request.payload.contentKind || request.payload.manifest?.contentKind || '',
|
|
5212
|
+
controlSessionId: request.binding.controlSessionId,
|
|
5213
|
+
controlCommandId: request.binding.controlCommandId,
|
|
5214
|
+
captureGeneration: request.binding.captureGeneration,
|
|
5215
|
+
monitorIndex: request.binding.monitorIndex,
|
|
5216
|
+
hubConnectionId: request.hubConnectionId,
|
|
5217
|
+
requestAck: true
|
|
5218
|
+
});
|
|
5219
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json({
|
|
5220
|
+
ok: result.ok === true,
|
|
5221
|
+
action: request.action,
|
|
5222
|
+
operationId: request.operationId,
|
|
5223
|
+
input: result,
|
|
5224
|
+
...(result.ok ? {} : { error: result.error || 'clipboard-input-failed' })
|
|
5225
|
+
});
|
|
5226
|
+
return;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
const result = await remoteHub.sendClipboardCommand(deviceId, request);
|
|
5230
|
+
res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json(result);
|
|
5231
|
+
} catch (error) {
|
|
5232
|
+
const status = error instanceof RemoteClipboardContractError
|
|
5233
|
+
? error.status
|
|
5234
|
+
: 500;
|
|
5235
|
+
res.status(status).json({
|
|
5236
|
+
ok: false,
|
|
5237
|
+
error: error instanceof RemoteClipboardContractError
|
|
5238
|
+
? error.code
|
|
5239
|
+
: 'clipboard-request-failed'
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
});
|
|
5243
|
+
|
|
5244
|
+
app.get('/api/remote/filesystem/roots', requireHubFeatureAccess, async (req, res) => {
|
|
5243
5245
|
noStore(res);
|
|
5244
5246
|
try {
|
|
5245
5247
|
res.json(await hubFilesystem.getRoots({ refresh: /^(1|true|yes|on)$/i.test(String(req.query?.refresh || '')) }));
|
|
@@ -5953,14 +5955,14 @@ await remoteHub.start();
|
|
|
5953
5955
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
5954
5956
|
hubSharedFolders.startAutoSync(
|
|
5955
5957
|
() => remoteHub.listDevices({ includeDataUrl: false }).filter(device => device.connected).map(device => device.deviceId),
|
|
5956
|
-
() => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/
|
|
5958
|
+
() => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/VuvoDeskFiles'
|
|
5957
5959
|
);
|
|
5958
5960
|
httpServer.listen(httpPort, httpHost, () => {
|
|
5959
5961
|
const status = remoteHub.getStatus({ includeSecrets: true });
|
|
5960
5962
|
const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
|
|
5961
|
-
console.log(`[
|
|
5962
|
-
console.log(`[
|
|
5963
|
-
console.log(`[
|
|
5963
|
+
console.log(`[VuvoDesk Hub] Version ${managerVersion}`);
|
|
5964
|
+
console.log(`[VuvoDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
5965
|
+
console.log(`[VuvoDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
5964
5966
|
hubConsoleRelay?.start();
|
|
5965
5967
|
});
|
|
5966
5968
|
|
|
@@ -5986,9 +5988,9 @@ async function boundedShutdownStep(label, action, timeoutMs) {
|
|
|
5986
5988
|
timer.unref?.();
|
|
5987
5989
|
})
|
|
5988
5990
|
]);
|
|
5989
|
-
console.log(`[
|
|
5991
|
+
console.log(`[VuvoDesk Hub] Shutdown stage completed: ${label}.`);
|
|
5990
5992
|
} catch (error) {
|
|
5991
|
-
console.warn(`[
|
|
5993
|
+
console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
5992
5994
|
} finally {
|
|
5993
5995
|
if (timer) clearTimeout(timer);
|
|
5994
5996
|
}
|
|
@@ -5998,7 +6000,7 @@ function runSynchronousShutdownStep(label, action) {
|
|
|
5998
6000
|
try {
|
|
5999
6001
|
action();
|
|
6000
6002
|
} catch (error) {
|
|
6001
|
-
console.warn(`[
|
|
6003
|
+
console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6002
6004
|
}
|
|
6003
6005
|
}
|
|
6004
6006
|
|
|
@@ -6008,13 +6010,13 @@ function closeBrowserWebSockets() {
|
|
|
6008
6010
|
for (const ws of wss.clients) {
|
|
6009
6011
|
closeRequested += 1;
|
|
6010
6012
|
try {
|
|
6011
|
-
ws.close(1012, '
|
|
6013
|
+
ws.close(1012, 'VuvoDesk Hub restarting');
|
|
6012
6014
|
} catch {
|
|
6013
6015
|
try { ws.terminate(); } catch { /* socket already closed */ }
|
|
6014
6016
|
}
|
|
6015
6017
|
}
|
|
6016
6018
|
}
|
|
6017
|
-
console.log(`[
|
|
6019
|
+
console.log(`[VuvoDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
|
|
6018
6020
|
}
|
|
6019
6021
|
|
|
6020
6022
|
function terminateOpenConnections() {
|
|
@@ -6033,7 +6035,7 @@ function terminateOpenConnections() {
|
|
|
6033
6035
|
try { socket.destroy(); } catch { /* socket already closed */ }
|
|
6034
6036
|
}
|
|
6035
6037
|
try { httpServer.closeAllConnections?.(); } catch { /* older Node runtime */ }
|
|
6036
|
-
console.log(`[
|
|
6038
|
+
console.log(`[VuvoDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
|
|
6037
6039
|
}
|
|
6038
6040
|
|
|
6039
6041
|
let hubShutdownPromise = null;
|
|
@@ -6041,7 +6043,7 @@ function shutdownHub(signal) {
|
|
|
6041
6043
|
if (hubShutdownPromise) return hubShutdownPromise;
|
|
6042
6044
|
hubShutdownPromise = (async () => {
|
|
6043
6045
|
const startedAt = Date.now();
|
|
6044
|
-
console.log(`[
|
|
6046
|
+
console.log(`[VuvoDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
|
|
6045
6047
|
if (roleWatchTimer) clearInterval(roleWatchTimer);
|
|
6046
6048
|
clearInterval(browserWebSocketHeartbeatTimer);
|
|
6047
6049
|
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
@@ -6053,15 +6055,15 @@ function shutdownHub(signal) {
|
|
|
6053
6055
|
try {
|
|
6054
6056
|
httpServer.close(error => {
|
|
6055
6057
|
if (error) {
|
|
6056
|
-
console.warn(`[
|
|
6058
|
+
console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error.message}`);
|
|
6057
6059
|
} else {
|
|
6058
|
-
console.log('[
|
|
6060
|
+
console.log('[VuvoDesk Hub] Shutdown stage completed: HTTP listener closed.');
|
|
6059
6061
|
}
|
|
6060
6062
|
resolveClose();
|
|
6061
6063
|
});
|
|
6062
6064
|
httpServer.closeIdleConnections?.();
|
|
6063
6065
|
} catch (error) {
|
|
6064
|
-
console.warn(`[
|
|
6066
|
+
console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
6065
6067
|
resolveClose();
|
|
6066
6068
|
}
|
|
6067
6069
|
});
|
|
@@ -6081,7 +6083,7 @@ function shutdownHub(signal) {
|
|
|
6081
6083
|
await Promise.race([httpClosed, waitForShutdownDelay(remainingMs)]);
|
|
6082
6084
|
clearTimeout(forceCloseTimer);
|
|
6083
6085
|
terminateOpenConnections();
|
|
6084
|
-
console.log(`[
|
|
6086
|
+
console.log(`[VuvoDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
|
|
6085
6087
|
})();
|
|
6086
6088
|
return hubShutdownPromise;
|
|
6087
6089
|
}
|
|
@@ -6091,7 +6093,7 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
|
6091
6093
|
void shutdownHub(signal)
|
|
6092
6094
|
.then(() => process.exit(0))
|
|
6093
6095
|
.catch(error => {
|
|
6094
|
-
console.error(`[
|
|
6096
|
+
console.error(`[VuvoDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6095
6097
|
terminateOpenConnections();
|
|
6096
6098
|
process.exit(1);
|
|
6097
6099
|
});
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
-
import { REMOTE_CLIPBOARD_LIMITS } from '../remote-clipboard-contract.mjs';
|
|
1
|
+
import { effectiveDevicePolicy } from './settings-schema.js';
|
|
2
|
+
import { REMOTE_CLIPBOARD_LIMITS } from '../remote-clipboard-contract.mjs';
|
|
3
3
|
|
|
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 {
|
|
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 {
|
|
18
18
|
...policy,
|
|
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,
|
|
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,
|
|
34
34
|
remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
|
|
35
35
|
agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
|
|
36
36
|
}
|
|
@@ -65,7 +65,7 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
|
65
65
|
openReceivedFolder: false,
|
|
66
66
|
notifyTransferComplete: true,
|
|
67
67
|
allowOverwrite: false,
|
|
68
|
-
defaultReceiveFolder: 'Desktop/
|
|
68
|
+
defaultReceiveFolder: 'Desktop/VuvoDeskFiles',
|
|
69
69
|
maxFileSizeBytes: 1024 * 1024 * 1024,
|
|
70
70
|
allowRemoteAudio: true,
|
|
71
71
|
startAudioMuted: false,
|
|
@@ -76,7 +76,7 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
|
|
|
76
76
|
recordingQuality: 'standard',
|
|
77
77
|
timelapseIntervalSeconds: 10,
|
|
78
78
|
includeRemoteCursor: true,
|
|
79
|
-
captureSaveLocation: '
|
|
79
|
+
captureSaveLocation: 'VuvoDesk Captures',
|
|
80
80
|
captureAutoDelete: 'never'
|
|
81
81
|
},
|
|
82
82
|
agent: {
|