@livedesk/hub 0.1.49 → 0.1.51

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/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(`[LiveDesk Hub] ${status.message}`);
154
+ logger(`[VuvoDesk Hub] ${status.message}`);
155
155
  }
156
156
  });
157
157
  const inputClients = new Set();
@@ -253,20 +253,40 @@ function readPositiveIntegerEnv(name, fallback) {
253
253
  return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
254
254
  }
255
255
 
256
- function secureExactTestTokenMatches(actual, expected) {
256
+ function secureExactTestTokenMatches(actual, expected) {
257
257
  const actualBytes = Buffer.from(String(actual || ''), 'utf8');
258
258
  const expectedBytes = Buffer.from(String(expected || ''), 'utf8');
259
259
  return actualBytes.length > 0
260
260
  && actualBytes.length === expectedBytes.length
261
- && crypto.timingSafeEqual(actualBytes, expectedBytes);
262
- }
263
-
264
- function handleRemoteHubEvent(type, event) {
261
+ && crypto.timingSafeEqual(actualBytes, expectedBytes);
262
+ }
263
+
264
+ function sendRemoteInputRouteState(ws, deviceId, reason = '') {
265
+ const route = remoteHub.getInputRouteState(deviceId);
266
+ sendJson(ws, {
267
+ type: 'RemoteInputRouteState',
268
+ ...route,
269
+ reason: String(reason || route.reason || ''),
270
+ timestamp: new Date().toISOString()
271
+ });
272
+ }
273
+
274
+ function broadcastRemoteInputRouteState(deviceId, reason = '') {
275
+ const normalizedDeviceId = String(deviceId || '').trim();
276
+ if (!normalizedDeviceId) return;
277
+ for (const ws of inputClients) {
278
+ if (ws.readyState === 1 && ws.liveDeskInputDeviceIds?.has(normalizedDeviceId)) {
279
+ sendRemoteInputRouteState(ws, normalizedDeviceId, reason);
280
+ }
281
+ }
282
+ }
283
+
284
+ function handleRemoteHubEvent(type, event) {
265
285
  liveDeskUpdateManager?.handleRemoteEvent(type, event);
266
286
  hubTransferJobs?.handleRemoteEvent(type, event);
267
287
  if (traceRemoteTestEventsEnabled
268
288
  && (type === 'RemoteFrameDropped' || type === 'RemoteFrameTransportProofAccepted')) {
269
- console.warn(`[LiveDesk Hub Test Event] ${type} ${JSON.stringify({
289
+ console.warn(`[VuvoDesk Hub Test Event] ${type} ${JSON.stringify({
270
290
  reason: event?.reason || '',
271
291
  deviceId: event?.deviceId || event?.device?.deviceId || '',
272
292
  proofOnly: event?.proofOnly === true,
@@ -297,7 +317,7 @@ function handleRemoteHubEvent(type, event) {
297
317
  }
298
318
  return;
299
319
  }
300
- if (type === 'RemoteInputError') {
320
+ if (type === 'RemoteInputError') {
301
321
  const targetConnectionId = String(event?.hubConnectionId || '').trim();
302
322
  for (const ws of inputClients) {
303
323
  if (targetConnectionId && ws.liveDeskInputClientId !== targetConnectionId) {
@@ -308,11 +328,18 @@ function handleRemoteHubEvent(type, event) {
308
328
  ...event
309
329
  });
310
330
  }
311
- return;
312
- }
313
- if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
314
- connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
315
- }
331
+ return;
332
+ }
333
+ if (type === 'RemoteInputSocketConnected' || type === 'RemoteInputSocketDisconnected') {
334
+ const deviceId = String(event?.deviceId || event?.device?.deviceId || '').trim();
335
+ broadcastRemoteInputRouteState(deviceId, event?.reason || type);
336
+ return;
337
+ }
338
+ if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
339
+ connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
340
+ const deviceId = String(event?.deviceId || event?.device?.deviceId || '').trim();
341
+ broadcastRemoteInputRouteState(deviceId, event?.reason || type);
342
+ }
316
343
  if (type !== 'RemoteDeviceConnected') {
317
344
  return;
318
345
  }
@@ -572,7 +599,7 @@ async function getRuntimeAccessToken() {
572
599
  expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
573
600
  });
574
601
  } catch (error) {
575
- console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
602
+ console.warn(`[VuvoDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
576
603
  }
577
604
  return runtimeAccessToken;
578
605
  }
@@ -658,7 +685,7 @@ async function watchAuthoritativeRuntimeRole() {
658
685
  try {
659
686
  const role = await queryAuthoritativeRuntimeRole();
660
687
  if (role === 'client') {
661
- console.warn('[LiveDesk Hub] Supabase selected another Sync Server. Transitioning this runtime to Client.');
688
+ console.warn('[VuvoDesk Hub] Supabase selected another Sync Server. Transitioning this runtime to Client.');
662
689
  void clearHubHostTarget('role-demoted').finally(() => {
663
690
  setTimeout(() => process.exit(ROLE_TRANSITION_EXIT_CODE), 150);
664
691
  });
@@ -692,16 +719,16 @@ const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
692
719
  const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
693
720
  const captureStore = new CaptureStore({ dataDir: agentDataDir });
694
721
  void captureStore.initialize().catch(error => {
695
- console.warn(`[LiveDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
722
+ console.warn(`[VuvoDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
696
723
  });
697
724
  void liveDeskSettingsStore.getRecord().catch(error => {
698
- console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
725
+ console.warn(`[VuvoDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
699
726
  });
700
727
 
701
728
  const udpTransport = createHubUdpTransport({
702
729
  env: process.env,
703
- logEvent: (_scope, message) => console.log(`[LiveDesk Hub] ${message}`),
704
- logWarn: (_scope, message) => console.warn(`[LiveDesk Hub] ${message}`)
730
+ logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
731
+ logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`)
705
732
  });
706
733
 
707
734
  const remoteHub = createRemoteHub({
@@ -713,8 +740,8 @@ const remoteHub = createRemoteHub({
713
740
  MINDEXEC_MANAGER_VERSION: packageInfo.version
714
741
  },
715
742
  udpTransport,
716
- logEvent: (_scope, message) => console.log(`[LiveDesk Hub] ${message}`),
717
- logWarn: (_scope, message) => console.warn(`[LiveDesk Hub] ${message}`),
743
+ logEvent: (_scope, message) => console.log(`[VuvoDesk Hub] ${message}`),
744
+ logWarn: (_scope, message) => console.warn(`[VuvoDesk Hub] ${message}`),
718
745
  emitEvent: handleRemoteHubEvent,
719
746
  emitFrame: broadcastRemoteBinaryFrame,
720
747
  emitAudio: broadcastRemoteBinaryAudio,
@@ -817,16 +844,18 @@ function getLiveDeskUpdateStatus() {
817
844
  };
818
845
  }
819
846
 
820
- if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
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(),
827
- requestHubRestart
828
- });
829
- }
847
+ const electronDesktopOwnsSelfUpdate = process.env.LIVEDESK_DESKTOP_HOST === '1';
848
+ liveDeskUpdateManager = createLiveDeskUpdateManager({
849
+ remoteHub,
850
+ currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
851
+ currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
852
+ excludedDeviceIds: [runtimeDeviceId],
853
+ // electron-updater owns replacement of the installed desktop application.
854
+ // The same Hub still needs the fleet manager for connected outdated Clients.
855
+ restartSupported: !electronDesktopOwnsSelfUpdate
856
+ && !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
857
+ requestHubRestart: electronDesktopOwnsSelfUpdate ? undefined : requestHubRestart
858
+ });
830
859
 
831
860
  if (process.env.LIVEDESK_UPDATE_CONTINUE === '1') {
832
861
  let continueUpdateInFlight = false;
@@ -870,7 +899,7 @@ if (process.env.LIVEDESK_UPDATE_CONTINUE === '1') {
870
899
  clearInterval(continueUpdateTimer);
871
900
  }
872
901
  } catch (error) {
873
- console.warn(`[LiveDesk Hub] automatic client update continuation failed: ${error instanceof Error ? error.message : String(error)}`);
902
+ console.warn(`[VuvoDesk Hub] automatic client update continuation failed: ${error instanceof Error ? error.message : String(error)}`);
874
903
  clearInterval(continueUpdateTimer);
875
904
  } finally {
876
905
  continueUpdateInFlight = false;
@@ -1167,11 +1196,11 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
1167
1196
  'livedesk.close_application': 'application.close',
1168
1197
  'livedesk.read_file': 'file.read',
1169
1198
  'livedesk.write_file': 'file.write',
1170
- 'livedesk.delete_file': 'file.delete',
1171
- 'livedesk.list_directory': 'file.list',
1172
- 'livedesk.search_files': 'file.search',
1173
- 'livedesk.create_directory': 'directory.create',
1174
- 'livedesk.run_command': 'command.run',
1199
+ 'livedesk.delete_file': 'file.delete',
1200
+ 'livedesk.list_directory': 'file.list',
1201
+ 'livedesk.search_files': 'file.search',
1202
+ 'livedesk.create_directory': 'directory.create',
1203
+ 'livedesk.run_command': 'command.run',
1175
1204
  'livedesk.run_script': 'script.run',
1176
1205
  'livedesk.install_software': 'software.install',
1177
1206
  'livedesk.get_network_status': 'network.status',
@@ -1255,8 +1284,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
1255
1284
  ? String(args.serviceName || '').replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
1256
1285
  : '';
1257
1286
  const result = remoteHub.requestAgentTaskBatch(targetIds, {
1258
- instruction: `${tool.readOnly ? `LiveDesk read-only ${operation}` : `LiveDesk ${name}`}${targetQuery ? ` for ${targetQuery}` : ''}. Return only the collected result.`,
1259
- title: `LiveDesk ${operation}`,
1287
+ instruction: `${tool.readOnly ? `VuvoDesk read-only ${operation}` : `VuvoDesk ${name}`}${targetQuery ? ` for ${targetQuery}` : ''}. Return only the collected result.`,
1288
+ title: `VuvoDesk ${operation}`,
1260
1289
  operation,
1261
1290
  targetQuery,
1262
1291
  approvalLevel: tool.readOnly ? 'read-only' : 'task-only',
@@ -1330,7 +1359,7 @@ const codexRuntime = createCodexAgentRuntime({
1330
1359
  const detail = ['failed', 'recovering'].includes(progress.stage) && progress.message
1331
1360
  ? ` detail=${JSON.stringify(String(progress.message).slice(0, 800))}`
1332
1361
  : '';
1333
- console.log(`[LiveDesk Hub] Agent ${progress.stage} run=${progress.runId}${tool} clients=${delivery.sent}/${delivery.total}${detail}`);
1362
+ console.log(`[VuvoDesk Hub] Agent ${progress.stage} run=${progress.runId}${tool} clients=${delivery.sent}/${delivery.total}${detail}`);
1334
1363
  }
1335
1364
  });
1336
1365
  const agentManager = createAgentManager({
@@ -1761,40 +1790,40 @@ function normalizeLiveOptions(payload = {}) {
1761
1790
  };
1762
1791
  }
1763
1792
 
1764
- function sendJson(ws, payload) {
1793
+ function sendJson(ws, payload) {
1765
1794
  if (!ws || ws.readyState !== 1) {
1766
1795
  return false;
1767
1796
  }
1768
1797
  return safeWebSocketSend(ws, JSON.stringify(payload));
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
- }
1798
+ }
1799
+
1800
+ function findLiveRemoteInputClient(hubConnectionId) {
1801
+ const requestedId = String(hubConnectionId || '').trim();
1802
+ if (!requestedId) {
1803
+ return null;
1804
+ }
1805
+ for (const ws of inputClients) {
1806
+ if (ws.readyState === 1 && ws.liveDeskInputClientId === requestedId) {
1807
+ return ws;
1808
+ }
1809
+ }
1810
+ return null;
1811
+ }
1812
+
1813
+ function clipboardHttpStatus(error) {
1814
+ const code = String(error || '');
1815
+ if (code.includes('blocked-by-settings') || code === 'remote-access-blocked-by-settings') return 403;
1816
+ if (code.includes('too-large')) return 413;
1817
+ if (code.includes('unavailable') || code === 'device-not-connected') return 503;
1818
+ if (code.startsWith('STALE_')
1819
+ || code.includes('owner')
1820
+ || code.includes('not-ready')
1821
+ || code.includes('not-found')
1822
+ || code.includes('already-exists')
1823
+ || code.includes('busy')
1824
+ || code.includes('capacity')) return 409;
1825
+ return 400;
1826
+ }
1798
1827
 
1799
1828
  function forgetWebSocketClient(ws) {
1800
1829
  unregisterFrameClient(ws);
@@ -1931,12 +1960,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
1931
1960
  if (result?.stopPromise) {
1932
1961
  void result.stopPromise.then(confirmation => {
1933
1962
  if (confirmation?.captureStopConfirmed === true) {
1934
- console.log(`[LiveDesk Hub] confirmed ${streamPurpose} capture stopped after frame client closed device=${deviceId} stream=${streamId}`);
1963
+ console.log(`[VuvoDesk Hub] confirmed ${streamPurpose} capture stopped after frame client closed device=${deviceId} stream=${streamId}`);
1935
1964
  return;
1936
1965
  }
1937
1966
  if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
1938
1967
  console.warn(
1939
- `[LiveDesk Hub] ${streamPurpose} capture stop was not confirmed device=${deviceId} `
1968
+ `[VuvoDesk Hub] ${streamPurpose} capture stop was not confirmed device=${deviceId} `
1940
1969
  + `stream=${streamId}: ${confirmation?.error || 'capture-stop-unconfirmed'}. `
1941
1970
  + 'Disconnecting the Agent so its final capture drain runs before reconnect.'
1942
1971
  );
@@ -1944,12 +1973,12 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
1944
1973
  }
1945
1974
  }).catch(error => {
1946
1975
  if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
1947
- console.warn(`[LiveDesk Hub] ${streamPurpose} capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
1976
+ console.warn(`[VuvoDesk Hub] ${streamPurpose} capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
1948
1977
  remoteHub.disconnectDevice(deviceId, 'frame-capture-stop-confirmation-failed');
1949
1978
  }
1950
1979
  });
1951
1980
  } else if (result?.ok && !result.alreadyStopped) {
1952
- console.log(`[LiveDesk Hub] queued ${streamPurpose} capture stop after frame client closed device=${deviceId} stream=${streamId}`);
1981
+ console.log(`[VuvoDesk Hub] queued ${streamPurpose} capture stop after frame client closed device=${deviceId} stream=${streamId}`);
1953
1982
  }
1954
1983
  }, frameStreamStopGraceMs);
1955
1984
  pendingFrameStreamStops.set(key, { timer });
@@ -3748,11 +3777,11 @@ function stopUnsubscribedAudioOwners(owners, reason) {
3748
3777
  disconnect: (targetDeviceId, disconnectReason) => (
3749
3778
  remoteHub.disconnectDevice(targetDeviceId, disconnectReason)
3750
3779
  ),
3751
- warn: message => console.warn(`[LiveDesk Hub] ${message}. Disconnecting the Agent for a final drain.`)
3780
+ warn: message => console.warn(`[VuvoDesk Hub] ${message}. Disconnecting the Agent for a final drain.`)
3752
3781
  });
3753
3782
  if (result?.ok === false
3754
3783
  && !['device-not-found', 'device-not-connected', 'device-audio-unavailable'].includes(String(result.error || ''))) {
3755
- console.warn(`[LiveDesk Hub] Could not stop unused remote audio capture device=${deviceId} reason=${reason}: ${result.error || 'unknown-error'}`);
3784
+ console.warn(`[VuvoDesk Hub] Could not stop unused remote audio capture device=${deviceId} reason=${reason}: ${result.error || 'unknown-error'}`);
3756
3785
  }
3757
3786
  }
3758
3787
  }
@@ -4653,7 +4682,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
4653
4682
  lastEventId: eventId,
4654
4683
  nextAttemptAt: ''
4655
4684
  };
4656
- console.log(`[LiveDesk Hub] Hub-online wake signal sent event=${eventId.slice(0, 8)} delivered=${Number(data?.delivered || 0)}.`);
4685
+ console.log(`[VuvoDesk Hub] Hub-online wake signal sent event=${eventId.slice(0, 8)} delivered=${Number(data?.delivered || 0)}.`);
4657
4686
  return { ok: true, eventId, delivered: Number(data?.delivered || 0) };
4658
4687
  } catch (error) {
4659
4688
  const message = error instanceof Error ? error.message : String(error);
@@ -4664,7 +4693,7 @@ async function notifyHubOnline({ accessToken, target, remoteStatus, endpointCand
4664
4693
  lastError: message,
4665
4694
  nextAttemptAt: new Date(hubWakeNextAttemptAtMs).toISOString()
4666
4695
  };
4667
- console.warn(`[LiveDesk Hub] Hub-online wake signal failed; Supabase target remains authoritative: ${message}`);
4696
+ console.warn(`[VuvoDesk Hub] Hub-online wake signal failed; Supabase target remains authoritative: ${message}`);
4668
4697
  return { ok: false, error: message, eventId };
4669
4698
  }
4670
4699
  }
@@ -4714,7 +4743,7 @@ async function promoteCurrentRuntimeToHub() {
4714
4743
  p_assigned_hub_id: null,
4715
4744
  p_expected_role_version: null
4716
4745
  }, accessToken);
4717
- console.log(`[LiveDesk Hub] Sync Server role confirmed for ${runtimeDeviceId}.`);
4746
+ console.log(`[VuvoDesk Hub] Sync Server role confirmed for ${runtimeDeviceId}.`);
4718
4747
  return result;
4719
4748
  }
4720
4749
 
@@ -4784,12 +4813,12 @@ async function publishHubHostTarget({ takeover = false, reason = 'renewal' } = {
4784
4813
  lastPublishedEndpoint: target.endpoint || remoteStatus.agentEndpoint || '',
4785
4814
  lastExpiresAt: target.expiresAt || ''
4786
4815
  });
4787
- console.log(`[LiveDesk Hub] Host target ${reason} published endpoint=${target.endpoint || remoteStatus.agentEndpoint} expires=${target.expiresAt || ''}.`);
4816
+ console.log(`[VuvoDesk Hub] Host target ${reason} published endpoint=${target.endpoint || remoteStatus.agentEndpoint} expires=${target.expiresAt || ''}.`);
4788
4817
  return { ok: true, active: true, hostTarget: target, lease: getHubHostTargetLeaseStatus() };
4789
4818
  } catch (error) {
4790
4819
  const message = error instanceof Error ? error.message : String(error);
4791
4820
  updateHubHostTargetLeaseState({ state: 'error', lastError: message });
4792
- console.error(`[LiveDesk Hub] Host target ${reason} publish failed: ${message}`);
4821
+ console.error(`[VuvoDesk Hub] Host target ${reason} publish failed: ${message}`);
4793
4822
  return { ok: false, active: false, error: message, lease: getHubHostTargetLeaseStatus() };
4794
4823
  } finally {
4795
4824
  hubHostTargetRenewInFlight = false;
@@ -4833,7 +4862,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
4833
4862
  }
4834
4863
  } catch (cause) {
4835
4864
  error = cause instanceof Error ? cause.message : String(cause);
4836
- console.error(`[LiveDesk Hub] Host target ${reason} clear failed: ${error}`);
4865
+ console.error(`[VuvoDesk Hub] Host target ${reason} clear failed: ${error}`);
4837
4866
  }
4838
4867
  try {
4839
4868
  await remoteHub.setHostTarget({ enabled: false, nodeId: targetNodeId });
@@ -4848,7 +4877,7 @@ async function clearHubHostTarget(reason = 'shutdown') {
4848
4877
  lastExpiresAt: ''
4849
4878
  });
4850
4879
  if (!error) {
4851
- console.log(`[LiveDesk Hub] Host target cleared (${reason}).`);
4880
+ console.log(`[VuvoDesk Hub] Host target cleared (${reason}).`);
4852
4881
  }
4853
4882
  return { ok: !error, error, lease: getHubHostTargetLeaseStatus() };
4854
4883
  }
@@ -4869,7 +4898,7 @@ function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-receive
4869
4898
  void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
4870
4899
  const message = error instanceof Error ? error.message : String(error);
4871
4900
  updateHubHostTargetLeaseState({ state: 'error', lastError: message });
4872
- console.error(`[LiveDesk Hub] Host target ${reason} background publish failed: ${message}`);
4901
+ console.error(`[VuvoDesk Hub] Host target ${reason} background publish failed: ${message}`);
4873
4902
  });
4874
4903
  });
4875
4904
  }
@@ -4923,7 +4952,7 @@ app.post('/api/hub/sync', async (req, res) => {
4923
4952
  } catch (error) {
4924
4953
  const message = error instanceof Error ? error.message : String(error);
4925
4954
  updateHubHostTargetLeaseState({ state: 'error', lastError: message, lastReason: 'sync-server' });
4926
- console.error(`[LiveDesk Hub] Sync Server failed: ${message}`);
4955
+ console.error(`[VuvoDesk Hub] Sync Server failed: ${message}`);
4927
4956
  res.status(409).json({ ok: false, error: message, lease: getHubHostTargetLeaseStatus() });
4928
4957
  }
4929
4958
  });
@@ -5010,31 +5039,30 @@ app.post('/api/runtime/role', async (req, res) => {
5010
5039
  }
5011
5040
  });
5012
5041
 
5013
- app.get('/api/update/status', (_req, res) => {
5014
- noStore(res);
5015
- if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
5016
- res.json({
5017
- updateAvailable: false,
5018
- state: 'electron-managed',
5019
- canApply: false,
5020
- disabled: true,
5021
- restartResult: readHubRestartResult()
5022
- });
5023
- return;
5024
- }
5025
- res.json(getLiveDeskUpdateStatus());
5026
- });
5027
-
5028
- app.post('/api/update/apply', async (_req, res) => {
5029
- noStore(res);
5030
- if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
5031
- res.status(409).json({ ok: false, error: 'electron-updater-managed', disabled: true });
5032
- return;
5033
- }
5034
- try {
5035
- liveDeskUpdateManager?.reconcileHubRestartResult(readHubRestartResult());
5036
- const result = await liveDeskUpdateManager?.startUpdate();
5037
- res.status(result?.ok === false ? 409 : 200).json(result || { ok: false, error: 'update-manager-unavailable' });
5042
+ app.get('/api/update/status', (_req, res) => {
5043
+ noStore(res);
5044
+ res.json({
5045
+ ...getLiveDeskUpdateStatus(),
5046
+ selfUpdateOwner: electronDesktopOwnsSelfUpdate ? 'electron-updater' : 'npm-launcher'
5047
+ });
5048
+ });
5049
+
5050
+ app.post('/api/update/apply', async (_req, res) => {
5051
+ noStore(res);
5052
+ try {
5053
+ liveDeskUpdateManager?.reconcileHubRestartResult(readHubRestartResult());
5054
+ const updateStatus = liveDeskUpdateManager?.getStatus();
5055
+ if (electronDesktopOwnsSelfUpdate
5056
+ && (updateStatus?.managerUpdateAvailable || updateStatus?.clientPackageUpdateAvailable)) {
5057
+ res.status(409).json({
5058
+ ok: false,
5059
+ error: 'electron-updater-managed',
5060
+ selfUpdateOwner: 'electron-updater'
5061
+ });
5062
+ return;
5063
+ }
5064
+ const result = await liveDeskUpdateManager?.startUpdate();
5065
+ res.status(result?.ok === false ? 409 : 200).json(result || { ok: false, error: 'update-manager-unavailable' });
5038
5066
  } catch (error) {
5039
5067
  res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
5040
5068
  }
@@ -5184,64 +5212,64 @@ app.post('/api/remote/devices/:deviceId/status-diagnostic', (req, res) => {
5184
5212
  }));
5185
5213
  });
5186
5214
 
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) => {
5215
+ app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
5216
+ noStore(res);
5217
+ res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
5218
+ });
5219
+
5220
+ app.post('/api/remote/devices/:deviceId/clipboard', requireHubFeatureAccess, async (req, res) => {
5221
+ noStore(res);
5222
+ try {
5223
+ const request = normalizeRemoteClipboardRequest(req.body || {});
5224
+ const inputClient = findLiveRemoteInputClient(request.hubConnectionId);
5225
+ if (!inputClient) {
5226
+ res.status(409).json({ ok: false, error: 'clipboard-input-owner-not-live' });
5227
+ return;
5228
+ }
5229
+ const deviceId = String(req.params.deviceId || '').trim();
5230
+ inputClient.liveDeskInputDeviceIds?.add?.(deviceId);
5231
+
5232
+ if (REMOTE_CLIPBOARD_INPUT_ACTIONS.has(request.action)) {
5233
+ const result = remoteHub.sendInputControl(deviceId, {
5234
+ type: request.action,
5235
+ clipboardOperationId: request.operationId,
5236
+ operationId: request.operationId,
5237
+ clipboardDirection: request.clipboardDirection,
5238
+ manifest: request.payload.manifest || null,
5239
+ contentKind: request.payload.contentKind || request.payload.manifest?.contentKind || '',
5240
+ controlSessionId: request.binding.controlSessionId,
5241
+ controlCommandId: request.binding.controlCommandId,
5242
+ captureGeneration: request.binding.captureGeneration,
5243
+ monitorIndex: request.binding.monitorIndex,
5244
+ hubConnectionId: request.hubConnectionId,
5245
+ requestAck: true
5246
+ });
5247
+ res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json({
5248
+ ok: result.ok === true,
5249
+ action: request.action,
5250
+ operationId: request.operationId,
5251
+ input: result,
5252
+ ...(result.ok ? {} : { error: result.error || 'clipboard-input-failed' })
5253
+ });
5254
+ return;
5255
+ }
5256
+
5257
+ const result = await remoteHub.sendClipboardCommand(deviceId, request);
5258
+ res.status(result.ok ? 200 : clipboardHttpStatus(result.error)).json(result);
5259
+ } catch (error) {
5260
+ const status = error instanceof RemoteClipboardContractError
5261
+ ? error.status
5262
+ : 500;
5263
+ res.status(status).json({
5264
+ ok: false,
5265
+ error: error instanceof RemoteClipboardContractError
5266
+ ? error.code
5267
+ : 'clipboard-request-failed'
5268
+ });
5269
+ }
5270
+ });
5271
+
5272
+ app.get('/api/remote/filesystem/roots', requireHubFeatureAccess, async (req, res) => {
5245
5273
  noStore(res);
5246
5274
  try {
5247
5275
  res.json(await hubFilesystem.getRoots({ refresh: /^(1|true|yes|on)$/i.test(String(req.query?.refresh || '')) }));
@@ -5793,7 +5821,7 @@ frameWss.on('connection', (ws, req) => {
5793
5821
  } catch {
5794
5822
  updateFrameSubscription(ws, {});
5795
5823
  }
5796
- ws.on('message', data => {
5824
+ ws.on('message', data => {
5797
5825
  try {
5798
5826
  const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
5799
5827
  if (payload?.type === 'subscribe') {
@@ -5899,11 +5927,29 @@ inputWss.on('connection', ws => {
5899
5927
  try {
5900
5928
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
5901
5929
  } catch {
5902
- sendJson(ws, { type: 'RemoteInputError', error: 'invalid-json' });
5903
- return;
5904
- }
5905
- const deviceId = String(payload?.deviceId || '').trim();
5906
- const inputEventId = String(
5930
+ sendJson(ws, { type: 'RemoteInputError', error: 'invalid-json' });
5931
+ return;
5932
+ }
5933
+ const deviceId = String(payload?.deviceId || '').trim();
5934
+ if (payload?.type === 'watch') {
5935
+ if (!deviceId) {
5936
+ sendJson(ws, { type: 'RemoteInputError', error: 'device-id-required' });
5937
+ return;
5938
+ }
5939
+ for (const previousDeviceId of ws.liveDeskInputDeviceIds) {
5940
+ if (previousDeviceId !== deviceId) {
5941
+ remoteHub.releaseInputOwner(
5942
+ previousDeviceId,
5943
+ ws.liveDeskInputClientId,
5944
+ 'browser-input-target-changed');
5945
+ }
5946
+ }
5947
+ ws.liveDeskInputDeviceIds.clear();
5948
+ ws.liveDeskInputDeviceIds.add(deviceId);
5949
+ sendRemoteInputRouteState(ws, deviceId, 'browser-input-watch');
5950
+ return;
5951
+ }
5952
+ const inputEventId = String(
5907
5953
  payload?.input?.inputEventId
5908
5954
  || payload?.inputEventId
5909
5955
  || payload?.requestId
@@ -5955,14 +6001,14 @@ await remoteHub.start();
5955
6001
  connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
5956
6002
  hubSharedFolders.startAutoSync(
5957
6003
  () => remoteHub.listDevices({ includeDataUrl: false }).filter(device => device.connected).map(device => device.deviceId),
5958
- () => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/LiveDeskFiles'
6004
+ () => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/VuvoDeskFiles'
5959
6005
  );
5960
6006
  httpServer.listen(httpPort, httpHost, () => {
5961
6007
  const status = remoteHub.getStatus({ includeSecrets: true });
5962
6008
  const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
5963
- console.log(`[LiveDesk Hub] Version ${managerVersion}`);
5964
- console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
5965
- console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
6009
+ console.log(`[VuvoDesk Hub] Version ${managerVersion}`);
6010
+ console.log(`[VuvoDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
6011
+ console.log(`[VuvoDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
5966
6012
  hubConsoleRelay?.start();
5967
6013
  });
5968
6014
 
@@ -5988,9 +6034,9 @@ async function boundedShutdownStep(label, action, timeoutMs) {
5988
6034
  timer.unref?.();
5989
6035
  })
5990
6036
  ]);
5991
- console.log(`[LiveDesk Hub] Shutdown stage completed: ${label}.`);
6037
+ console.log(`[VuvoDesk Hub] Shutdown stage completed: ${label}.`);
5992
6038
  } catch (error) {
5993
- console.warn(`[LiveDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
6039
+ console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
5994
6040
  } finally {
5995
6041
  if (timer) clearTimeout(timer);
5996
6042
  }
@@ -6000,7 +6046,7 @@ function runSynchronousShutdownStep(label, action) {
6000
6046
  try {
6001
6047
  action();
6002
6048
  } catch (error) {
6003
- console.warn(`[LiveDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
6049
+ console.warn(`[VuvoDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
6004
6050
  }
6005
6051
  }
6006
6052
 
@@ -6010,13 +6056,13 @@ function closeBrowserWebSockets() {
6010
6056
  for (const ws of wss.clients) {
6011
6057
  closeRequested += 1;
6012
6058
  try {
6013
- ws.close(1012, 'LiveDesk Hub restarting');
6059
+ ws.close(1012, 'VuvoDesk Hub restarting');
6014
6060
  } catch {
6015
6061
  try { ws.terminate(); } catch { /* socket already closed */ }
6016
6062
  }
6017
6063
  }
6018
6064
  }
6019
- console.log(`[LiveDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
6065
+ console.log(`[VuvoDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
6020
6066
  }
6021
6067
 
6022
6068
  function terminateOpenConnections() {
@@ -6035,7 +6081,7 @@ function terminateOpenConnections() {
6035
6081
  try { socket.destroy(); } catch { /* socket already closed */ }
6036
6082
  }
6037
6083
  try { httpServer.closeAllConnections?.(); } catch { /* older Node runtime */ }
6038
- console.log(`[LiveDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
6084
+ console.log(`[VuvoDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
6039
6085
  }
6040
6086
 
6041
6087
  let hubShutdownPromise = null;
@@ -6043,7 +6089,7 @@ function shutdownHub(signal) {
6043
6089
  if (hubShutdownPromise) return hubShutdownPromise;
6044
6090
  hubShutdownPromise = (async () => {
6045
6091
  const startedAt = Date.now();
6046
- console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
6092
+ console.log(`[VuvoDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
6047
6093
  if (roleWatchTimer) clearInterval(roleWatchTimer);
6048
6094
  clearInterval(browserWebSocketHeartbeatTimer);
6049
6095
  runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
@@ -6055,15 +6101,15 @@ function shutdownHub(signal) {
6055
6101
  try {
6056
6102
  httpServer.close(error => {
6057
6103
  if (error) {
6058
- console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error.message}`);
6104
+ console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error.message}`);
6059
6105
  } else {
6060
- console.log('[LiveDesk Hub] Shutdown stage completed: HTTP listener closed.');
6106
+ console.log('[VuvoDesk Hub] Shutdown stage completed: HTTP listener closed.');
6061
6107
  }
6062
6108
  resolveClose();
6063
6109
  });
6064
6110
  httpServer.closeIdleConnections?.();
6065
6111
  } catch (error) {
6066
- console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
6112
+ console.warn(`[VuvoDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
6067
6113
  resolveClose();
6068
6114
  }
6069
6115
  });
@@ -6083,7 +6129,7 @@ function shutdownHub(signal) {
6083
6129
  await Promise.race([httpClosed, waitForShutdownDelay(remainingMs)]);
6084
6130
  clearTimeout(forceCloseTimer);
6085
6131
  terminateOpenConnections();
6086
- console.log(`[LiveDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
6132
+ console.log(`[VuvoDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
6087
6133
  })();
6088
6134
  return hubShutdownPromise;
6089
6135
  }
@@ -6093,7 +6139,7 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
6093
6139
  void shutdownHub(signal)
6094
6140
  .then(() => process.exit(0))
6095
6141
  .catch(error => {
6096
- console.error(`[LiveDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
6142
+ console.error(`[VuvoDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
6097
6143
  terminateOpenConnections();
6098
6144
  process.exit(1);
6099
6145
  });