@livedesk/hub 0.1.50 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
19
- "@livedesk/runtime-core": "0.1.6",
19
+ "@livedesk/runtime-core": "0.1.6",
20
20
  "@openai/codex-sdk": "0.145.0",
21
21
  "cors": "^2.8.5",
22
22
  "express": "^4.21.2",
package/src/remote-hub.js CHANGED
@@ -4300,10 +4300,12 @@ export function createRemoteHub(options = {}) {
4300
4300
  device.inputWriteOwner = null;
4301
4301
  device.inputSocketConnectionId = '';
4302
4302
  device.inputOwnerConnectionId = '';
4303
- device.inputOwnerBindingKey = '';
4304
- inputSockets.delete(socket);
4305
- writeOwner?.close?.(reason);
4306
- try {
4303
+ device.inputOwnerBindingKey = '';
4304
+ inputSockets.delete(socket);
4305
+ writeOwner?.close?.(reason);
4306
+ device.inputLastSeenAt = new Date().toISOString();
4307
+ emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
4308
+ try {
4307
4309
  writeJsonLine(socket, { type: 'disconnect', reason });
4308
4310
  } catch {
4309
4311
  // Best-effort notice before closing the side channel.
@@ -9026,15 +9028,40 @@ export function createRemoteHub(options = {}) {
9026
9028
  };
9027
9029
  }
9028
9030
 
9029
- function getCurrentInputWriteOwner(device) {
9031
+ function getCurrentInputWriteOwner(device) {
9030
9032
  if (!device?.inputSocket
9031
9033
  || device.inputSocket.destroyed
9032
9034
  || !device.inputWriteOwner
9033
9035
  || !safeString(device.inputSocketConnectionId, 128)) {
9034
9036
  return null;
9035
9037
  }
9036
- return device.inputWriteOwner;
9037
- }
9038
+ return device.inputWriteOwner;
9039
+ }
9040
+
9041
+ function getInputRouteState(deviceId) {
9042
+ const device = devices.get(String(deviceId || ''));
9043
+ if (!device || !device.connected || !device.socket || device.socket.destroyed) {
9044
+ return {
9045
+ deviceId: safeString(deviceId, 160),
9046
+ ready: false,
9047
+ dedicatedInputChannel: false,
9048
+ reason: 'device-not-connected'
9049
+ };
9050
+ }
9051
+
9052
+ const dedicatedInputChannel = readCapabilityFlag(
9053
+ device.capabilities,
9054
+ 'dedicatedInputChannel');
9055
+ const ready = dedicatedInputChannel
9056
+ ? !!getCurrentInputWriteOwner(device)
9057
+ : true;
9058
+ return {
9059
+ deviceId: device.deviceId,
9060
+ ready,
9061
+ dedicatedInputChannel,
9062
+ reason: ready ? 'ready' : 'input-channel-not-connected'
9063
+ };
9064
+ }
9038
9065
 
9039
9066
  function sendInputControl(deviceId, input = {}) {
9040
9067
  const device = devices.get(String(deviceId || ''));
@@ -9057,9 +9084,9 @@ export function createRemoteHub(options = {}) {
9057
9084
  return { ok: false, error: 'missing-input-type' };
9058
9085
  }
9059
9086
 
9060
- // A reset only releases native key state. The current browser input
9061
- // owner must be able to send it even when its former Control binding
9062
- // has just become stale during a monitor/capture-generation switch.
9087
+ // A reset releases native mouse-button and keyboard state. The
9088
+ // current browser input owner must be able to send it even when its
9089
+ // former Control binding has just become stale during a monitor/capture-generation switch.
9063
9090
  const currentInputOwner = safeString(device.inputOwnerConnectionId, 128);
9064
9091
  const isCurrentOwnerKeyboardReset = normalized.type.toLowerCase() === 'keyboard.reset'
9065
9092
  && normalized.hubConnectionId
@@ -11835,8 +11862,9 @@ export function createRemoteHub(options = {}) {
11835
11862
  sendClipboardCommand,
11836
11863
  refreshDevicePolicies,
11837
11864
  sendLegacyClientUpdate,
11838
- sendInputControl,
11839
- releaseInputOwner,
11865
+ sendInputControl,
11866
+ getInputRouteState,
11867
+ releaseInputOwner,
11840
11868
  notifyAgentProgress,
11841
11869
  requestAgentTask,
11842
11870
  requestAgentTaskBatch,
package/src/server.js CHANGED
@@ -253,15 +253,35 @@ 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
@@ -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
  }
@@ -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;
@@ -3860,7 +3889,7 @@ function buildHubHealthPayload({
3860
3889
  };
3861
3890
  return {
3862
3891
  ok: true,
3863
- product: 'LiveDesk',
3892
+ product: 'LiveDesk',
3864
3893
  timestamp: new Date().toISOString(),
3865
3894
  persistentSessionGc,
3866
3895
  memoryBeforeGc,
@@ -4323,7 +4352,7 @@ app.get('/api/remote/status', (_req, res) => {
4323
4352
  res.json({
4324
4353
  ...remoteHub.getStatus({ includeSecrets: false }),
4325
4354
  pairingPin: secretStatus.pairingPin,
4326
- product: 'LiveDesk',
4355
+ product: 'LiveDesk',
4327
4356
  runtimeRole,
4328
4357
  deviceId: runtimeDeviceId,
4329
4358
  deviceName: runtimeDeviceName,
@@ -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
  }
@@ -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