@livedesk/hub 0.1.50 → 0.1.52

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.52",
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
@@ -4288,22 +4288,25 @@ export function createRemoteHub(options = {}) {
4288
4288
  }
4289
4289
  }
4290
4290
 
4291
- function closeInputSocket(device, reason = 'input-socket-closed') {
4291
+ function closeInputSocket(device, reason = 'input-socket-closed') {
4292
4292
  const socket = device?.inputSocket;
4293
4293
  if (!socket) {
4294
4294
  return;
4295
4295
  }
4296
4296
 
4297
- const writeOwner = device.inputWriteOwner;
4297
+ const writeOwner = device.inputWriteOwner;
4298
+ clearPendingDedicatedInputAcks(device);
4298
4299
  clearClipboardOperationsForDevice(device, reason);
4299
4300
  device.inputSocket = null;
4300
4301
  device.inputWriteOwner = null;
4301
4302
  device.inputSocketConnectionId = '';
4302
4303
  device.inputOwnerConnectionId = '';
4303
- device.inputOwnerBindingKey = '';
4304
- inputSockets.delete(socket);
4305
- writeOwner?.close?.(reason);
4306
- try {
4304
+ device.inputOwnerBindingKey = '';
4305
+ inputSockets.delete(socket);
4306
+ writeOwner?.close?.(reason);
4307
+ device.inputLastSeenAt = new Date().toISOString();
4308
+ emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
4309
+ try {
4307
4310
  writeJsonLine(socket, { type: 'disconnect', reason });
4308
4311
  } catch {
4309
4312
  // Best-effort notice before closing the side channel.
@@ -4315,7 +4318,7 @@ export function createRemoteHub(options = {}) {
4315
4318
  }
4316
4319
  }
4317
4320
 
4318
- function detachInputSocket(socket, reason = 'input-socket-closed') {
4321
+ function detachInputSocket(socket, reason = 'input-socket-closed') {
4319
4322
  const deviceId = inputSockets.get(socket);
4320
4323
  inputSockets.delete(socket);
4321
4324
  if (!deviceId) {
@@ -4327,7 +4330,8 @@ export function createRemoteHub(options = {}) {
4327
4330
  return;
4328
4331
  }
4329
4332
 
4330
- const writeOwner = device.inputWriteOwner;
4333
+ const writeOwner = device.inputWriteOwner;
4334
+ clearPendingDedicatedInputAcks(device);
4331
4335
  clearClipboardOperationsForDevice(device, reason);
4332
4336
  device.inputSocket = null;
4333
4337
  device.inputWriteOwner = null;
@@ -4946,7 +4950,8 @@ export function createRemoteHub(options = {}) {
4946
4950
  return null;
4947
4951
  }
4948
4952
 
4949
- closeInputSocket(device, 'replaced-by-new-input-socket');
4953
+ closeInputSocket(device, 'replaced-by-new-input-socket');
4954
+ clearPendingDedicatedInputAcks(device);
4950
4955
 
4951
4956
  const now = new Date().toISOString();
4952
4957
  const inputSocketConnectionId = crypto.randomUUID();
@@ -5390,7 +5395,7 @@ export function createRemoteHub(options = {}) {
5390
5395
  return failed;
5391
5396
  }
5392
5397
 
5393
- function ensurePendingInputFallbacks(device) {
5398
+ function ensurePendingInputFallbacks(device) {
5394
5399
  if (!(device?.pendingInputFallbacks instanceof Map)) {
5395
5400
  Object.defineProperty(device, 'pendingInputFallbacks', {
5396
5401
  value: new Map(),
@@ -5407,8 +5412,151 @@ export function createRemoteHub(options = {}) {
5407
5412
  writable: true
5408
5413
  });
5409
5414
  }
5410
- return device.pendingInputFallbacks;
5411
- }
5415
+ return device.pendingInputFallbacks;
5416
+ }
5417
+
5418
+ function ensurePendingDedicatedInputAckState(device) {
5419
+ if (!device?.pendingDedicatedInputAckState) {
5420
+ Object.defineProperty(device, 'pendingDedicatedInputAckState', {
5421
+ value: { current: null, next: null, timeoutTimer: null },
5422
+ enumerable: false,
5423
+ configurable: true,
5424
+ writable: true
5425
+ });
5426
+ }
5427
+ return device.pendingDedicatedInputAckState;
5428
+ }
5429
+
5430
+ function clearPendingDedicatedInputAcks(device) {
5431
+ if (!device?.pendingDedicatedInputAckState) {
5432
+ return;
5433
+ }
5434
+ const state = device.pendingDedicatedInputAckState;
5435
+ if (state.timeoutTimer) {
5436
+ clearTimeout(state.timeoutTimer);
5437
+ }
5438
+ state.timeoutTimer = null;
5439
+ state.current = null;
5440
+ state.next = null;
5441
+ }
5442
+
5443
+ function dedicatedInputAckMatches(pending, message = {}) {
5444
+ if (!pending) {
5445
+ return false;
5446
+ }
5447
+ const result = message?.result && typeof message.result === 'object'
5448
+ ? message.result
5449
+ : {};
5450
+ const inputEventId = safeString(
5451
+ message.inputEventId
5452
+ || message.InputEventId
5453
+ || result.inputEventId
5454
+ || result.InputEventId,
5455
+ 128);
5456
+ const hubConnectionId = safeString(
5457
+ message.hubConnectionId
5458
+ || message.HubConnectionId
5459
+ || result.hubConnectionId
5460
+ || result.HubConnectionId,
5461
+ 128);
5462
+ const inputSeq = Number(
5463
+ message.inputSeq
5464
+ ?? message.InputSeq
5465
+ ?? result.inputSeq
5466
+ ?? result.InputSeq
5467
+ ?? 0) || 0;
5468
+ if (pending.inputEventId && inputEventId) {
5469
+ return pending.inputEventId === inputEventId
5470
+ && pending.hubConnectionId === hubConnectionId;
5471
+ }
5472
+ return pending.inputSeq > 0
5473
+ && pending.inputSeq === inputSeq
5474
+ && pending.hubConnectionId === hubConnectionId;
5475
+ }
5476
+
5477
+ function armPendingDedicatedInputAck(device) {
5478
+ const state = ensurePendingDedicatedInputAckState(device);
5479
+ if (state.timeoutTimer) {
5480
+ clearTimeout(state.timeoutTimer);
5481
+ state.timeoutTimer = null;
5482
+ }
5483
+ const pending = state.current;
5484
+ if (!pending) {
5485
+ return;
5486
+ }
5487
+ const remainingMs = Math.max(0, pending.deadlineAtEpochMs - Date.now());
5488
+ state.timeoutTimer = setTimeout(() => {
5489
+ state.timeoutTimer = null;
5490
+ if (state.current !== pending) {
5491
+ return;
5492
+ }
5493
+ state.current = null;
5494
+ state.next = null;
5495
+ if (device.inputSocketConnectionId !== pending.inputSocketConnectionId) {
5496
+ return;
5497
+ }
5498
+ emitRemoteInputError(
5499
+ device,
5500
+ { error: 'remote-input-timeout' },
5501
+ pending,
5502
+ 'remote-input-timeout');
5503
+ closeInputSocket(device, 'input-ack-timeout');
5504
+ }, remainingMs);
5505
+ state.timeoutTimer.unref?.();
5506
+ }
5507
+
5508
+ function schedulePendingDedicatedInputAck(device, pending) {
5509
+ if (pending?.requestAck !== true
5510
+ || !Number.isSafeInteger(pending.inputSeq)
5511
+ || pending.inputSeq <= 0) {
5512
+ return;
5513
+ }
5514
+ const state = ensurePendingDedicatedInputAckState(device);
5515
+ const record = {
5516
+ ...pending,
5517
+ deadlineAtEpochMs: Date.now() + inputAckTimeoutMs
5518
+ };
5519
+ if (!state.current) {
5520
+ state.current = record;
5521
+ state.next = null;
5522
+ armPendingDedicatedInputAck(device);
5523
+ return;
5524
+ }
5525
+ if (state.current.inputSocketConnectionId !== record.inputSocketConnectionId
5526
+ || state.current.bindingKey !== record.bindingKey) {
5527
+ clearPendingDedicatedInputAcks(device);
5528
+ state.current = record;
5529
+ armPendingDedicatedInputAck(device);
5530
+ return;
5531
+ }
5532
+ // Keep only one later checkpoint. If the current acknowledgement
5533
+ // arrives, this latest sampled input becomes the next exact deadline.
5534
+ state.next = record;
5535
+ }
5536
+
5537
+ function acknowledgePendingDedicatedInput(device, message = {}) {
5538
+ const state = device?.pendingDedicatedInputAckState;
5539
+ if (!state?.current) {
5540
+ return false;
5541
+ }
5542
+ if (dedicatedInputAckMatches(state.current, message)) {
5543
+ if (state.timeoutTimer) {
5544
+ clearTimeout(state.timeoutTimer);
5545
+ state.timeoutTimer = null;
5546
+ }
5547
+ state.current = state.next;
5548
+ state.next = null;
5549
+ armPendingDedicatedInputAck(device);
5550
+ return true;
5551
+ }
5552
+ if (dedicatedInputAckMatches(state.next, message)) {
5553
+ // A later ordered acknowledgement proves that the input reader is
5554
+ // still draining, even if an earlier diagnostic response vanished.
5555
+ clearPendingDedicatedInputAcks(device);
5556
+ return true;
5557
+ }
5558
+ return false;
5559
+ }
5412
5560
 
5413
5561
  function pruneCompletedInputFallbacks(device, now = Date.now()) {
5414
5562
  ensurePendingInputFallbacks(device);
@@ -7864,10 +8012,14 @@ export function createRemoteHub(options = {}) {
7864
8012
  return;
7865
8013
  }
7866
8014
 
7867
- if (state.inputOnly) {
7868
- device.inputLastSeenAt = new Date().toISOString();
7869
- if (message.type === 'input.applied' || message.type === 'input.error') {
7870
- handleRemoteInputOutcome(device, {
8015
+ if (state.inputOnly) {
8016
+ if (device.inputSocket !== socket) {
8017
+ return;
8018
+ }
8019
+ device.inputLastSeenAt = new Date().toISOString();
8020
+ if (message.type === 'input.applied' || message.type === 'input.error') {
8021
+ acknowledgePendingDedicatedInput(device, message);
8022
+ handleRemoteInputOutcome(device, {
7871
8023
  ...message,
7872
8024
  agentApplyMs: Number(message.agentApplyMs || 0) || 0
7873
8025
  }, 'input');
@@ -9026,15 +9178,40 @@ export function createRemoteHub(options = {}) {
9026
9178
  };
9027
9179
  }
9028
9180
 
9029
- function getCurrentInputWriteOwner(device) {
9181
+ function getCurrentInputWriteOwner(device) {
9030
9182
  if (!device?.inputSocket
9031
9183
  || device.inputSocket.destroyed
9032
9184
  || !device.inputWriteOwner
9033
9185
  || !safeString(device.inputSocketConnectionId, 128)) {
9034
9186
  return null;
9035
9187
  }
9036
- return device.inputWriteOwner;
9037
- }
9188
+ return device.inputWriteOwner;
9189
+ }
9190
+
9191
+ function getInputRouteState(deviceId) {
9192
+ const device = devices.get(String(deviceId || ''));
9193
+ if (!device || !device.connected || !device.socket || device.socket.destroyed) {
9194
+ return {
9195
+ deviceId: safeString(deviceId, 160),
9196
+ ready: false,
9197
+ dedicatedInputChannel: false,
9198
+ reason: 'device-not-connected'
9199
+ };
9200
+ }
9201
+
9202
+ const dedicatedInputChannel = readCapabilityFlag(
9203
+ device.capabilities,
9204
+ 'dedicatedInputChannel');
9205
+ const ready = dedicatedInputChannel
9206
+ ? !!getCurrentInputWriteOwner(device)
9207
+ : true;
9208
+ return {
9209
+ deviceId: device.deviceId,
9210
+ ready,
9211
+ dedicatedInputChannel,
9212
+ reason: ready ? 'ready' : 'input-channel-not-connected'
9213
+ };
9214
+ }
9038
9215
 
9039
9216
  function sendInputControl(deviceId, input = {}) {
9040
9217
  const device = devices.get(String(deviceId || ''));
@@ -9057,9 +9234,9 @@ export function createRemoteHub(options = {}) {
9057
9234
  return { ok: false, error: 'missing-input-type' };
9058
9235
  }
9059
9236
 
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.
9237
+ // A reset releases native mouse-button and keyboard state. The
9238
+ // current browser input owner must be able to send it even when its
9239
+ // former Control binding has just become stale during a monitor/capture-generation switch.
9063
9240
  const currentInputOwner = safeString(device.inputOwnerConnectionId, 128);
9064
9241
  const isCurrentOwnerKeyboardReset = normalized.type.toLowerCase() === 'keyboard.reset'
9065
9242
  && normalized.hubConnectionId
@@ -9238,9 +9415,10 @@ export function createRemoteHub(options = {}) {
9238
9415
  && normalized.hubConnectionId !== previousOwnerConnectionId;
9239
9416
  const bindingChanged = previousInputBindingKey
9240
9417
  && previousInputBindingKey !== activeInputBindingKey;
9241
- const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
9242
- if (ownerChanged || bindingChanged || writeBindingChanged) {
9243
- if (ownerChanged) {
9418
+ const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
9419
+ if (ownerChanged || bindingChanged || writeBindingChanged) {
9420
+ clearPendingDedicatedInputAcks(device);
9421
+ if (ownerChanged) {
9244
9422
  clearClipboardOperationsForDevice(
9245
9423
  device,
9246
9424
  'browser-input-owner-changed',
@@ -9265,11 +9443,12 @@ export function createRemoteHub(options = {}) {
9265
9443
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
9266
9444
  }
9267
9445
  }
9268
- const sent = inputWriteOwner.enqueue({
9269
- type: 'input.control',
9270
- payload: {
9271
- ...normalized,
9272
- hubForwardedAtEpochMs: Date.now(),
9446
+ const hubForwardedAtEpochMs = Date.now();
9447
+ const sent = inputWriteOwner.enqueue({
9448
+ type: 'input.control',
9449
+ payload: {
9450
+ ...normalized,
9451
+ hubForwardedAtEpochMs,
9273
9452
  issuedAt: normalized.issuedAt || new Date().toISOString()
9274
9453
  }
9275
9454
  }, activeInputBindingKey);
@@ -9277,9 +9456,15 @@ export function createRemoteHub(options = {}) {
9277
9456
  if (normalized.hubConnectionId) {
9278
9457
  device.inputOwnerConnectionId = normalized.hubConnectionId;
9279
9458
  }
9280
- device.inputOwnerBindingKey = activeInputBindingKey;
9281
- device.counters.commandsSent += 1;
9282
- device.inputLastSeenAt = new Date().toISOString();
9459
+ device.inputOwnerBindingKey = activeInputBindingKey;
9460
+ device.counters.commandsSent += 1;
9461
+ device.inputLastSeenAt = new Date().toISOString();
9462
+ schedulePendingDedicatedInputAck(device, {
9463
+ ...normalized,
9464
+ inputSocketConnectionId: device.inputSocketConnectionId,
9465
+ bindingKey: activeInputBindingKey,
9466
+ hubForwardedAtEpochMs
9467
+ });
9283
9468
  if (clipboardOperationKey) {
9284
9469
  const operation = clipboardOperations.get(clipboardOperationKey);
9285
9470
  if (operation) {
@@ -9370,7 +9555,8 @@ export function createRemoteHub(options = {}) {
9370
9555
  return { ok: false, error: 'input-owner-not-current' };
9371
9556
  }
9372
9557
 
9373
- clearClipboardOperationsForDevice(device, reason, owner);
9558
+ clearClipboardOperationsForDevice(device, reason, owner);
9559
+ clearPendingDedicatedInputAcks(device);
9374
9560
  device.inputOwnerConnectionId = '';
9375
9561
  device.inputOwnerBindingKey = '';
9376
9562
  const inputSocket = device.inputSocket;
@@ -11835,8 +12021,9 @@ export function createRemoteHub(options = {}) {
11835
12021
  sendClipboardCommand,
11836
12022
  refreshDevicePolicies,
11837
12023
  sendLegacyClientUpdate,
11838
- sendInputControl,
11839
- releaseInputOwner,
12024
+ sendInputControl,
12025
+ getInputRouteState,
12026
+ releaseInputOwner,
11840
12027
  notifyAgentProgress,
11841
12028
  requestAgentTask,
11842
12029
  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