livedesk 0.1.498 → 0.1.500

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/bin/livedesk.js CHANGED
@@ -2640,9 +2640,9 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2640
2640
  );
2641
2641
  const packageTarget = `livedesk@${requestedVersion}`;
2642
2642
  const nextArgs = [
2643
- '-y',
2644
- '--prefer-online',
2645
- '--prefix',
2643
+ '-y',
2644
+ '--prefer-offline',
2645
+ '--prefix',
2646
2646
  neutralCwd,
2647
2647
  '--workspaces=false',
2648
2648
  packageTarget,
@@ -1019,7 +1019,7 @@ export function prefetchExactNpxRestore(config, env = process.env, options = {})
1019
1019
  }
1020
1020
  const neutralCwd = prepareNeutralCwd(config?.neutralCwd);
1021
1021
  const invocation = resolveNpxInvocation(
1022
- buildExactNpxArgs(neutralCwd, '--prefer-online', version, ['--version']),
1022
+ buildExactNpxArgs(neutralCwd, '--prefer-offline', version, ['--version']),
1023
1023
  env
1024
1024
  );
1025
1025
  const result = (options.spawnSyncImpl || spawnSync)(
@@ -1691,7 +1691,7 @@ export async function runLegacyClientUpdateSupervisor(options = {}) {
1691
1691
  config.targetProductVersion,
1692
1692
  'target',
1693
1693
  attempt,
1694
- '--prefer-online',
1694
+ '--prefer-offline',
1695
1695
  config.targetVersion
1696
1696
  );
1697
1697
  ownershipCheck();
@@ -31,23 +31,30 @@ function writePrivateJson(path, value) {
31
31
  }
32
32
  }
33
33
 
34
- function originAllowed(req, host, port) {
35
- const origin = clean(req.headers.origin, 300);
34
+ function originAllowed(req, host, port) {
35
+ const origin = clean(req.headers.origin, 300);
36
36
  const requestHost = clean(req.headers.host, 300).split(':')[0].toLowerCase();
37
37
  const allowedHost = host.toLowerCase() === 'localhost' || host === '127.0.0.1' ? host.toLowerCase() : '127.0.0.1';
38
38
  if (requestHost && requestHost !== allowedHost && requestHost !== 'localhost' && requestHost !== '127.0.0.1') return false;
39
- // Electron can load the local runtime from a shell whose serialized origin
40
- // is `null`. The request is still restricted to a loopback Host above, so
41
- // allow this local-shell origin for the bootstrap API and role selection.
42
- if (!origin || origin === 'null') return true;
39
+ if (!origin) {
40
+ const fetchSite = clean(req.headers['sec-fetch-site'], 40).toLowerCase();
41
+ return !fetchSite
42
+ || fetchSite === 'same-origin'
43
+ || fetchSite === 'same-site'
44
+ || fetchSite === 'none';
45
+ }
46
+ // Electron loads the bootstrap UI from the loopback HTTP origin. An opaque
47
+ // origin cannot distinguish that UI from an attacker-controlled local file
48
+ // or sandbox and therefore receives no bootstrap API privilege.
49
+ if (origin === 'null') return false;
43
50
  return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
44
51
  }
45
52
 
46
53
  function sendJson(req, res, status, payload, port) {
47
54
  const origin = clean(req.headers.origin, 300);
48
- const allowedOrigin = origin === 'null'
49
- ? 'null'
50
- : origin === `http://localhost:${port}` ? origin : `http://127.0.0.1:${port}`;
55
+ const allowedOrigin = origin === `http://localhost:${port}`
56
+ ? origin
57
+ : `http://127.0.0.1:${port}`;
51
58
  res.writeHead(status, {
52
59
  'Content-Type': 'application/json; charset=utf-8',
53
60
  'Cache-Control': 'no-store',
@@ -251,7 +251,7 @@ async function waitForClientUpdateShutdownSignal(
251
251
  return { ready: false, error };
252
252
  }
253
253
 
254
- function markClientUpdateConnected() {
254
+ function markClientUpdateConnected() {
255
255
  const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
256
256
  if (!operationId) return;
257
257
  void writeClientUpdateState('connected', {
@@ -262,8 +262,59 @@ function markClientUpdateConnected() {
262
262
  error: ''
263
263
  }).catch(error => {
264
264
  console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
265
- });
266
- }
265
+ });
266
+ }
267
+
268
+ async function publishClientUpdateSupervisorProof(socket) {
269
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
270
+ const attemptId = String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
271
+ const expectedLauncherPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0);
272
+ if (!operationId || !attemptId || !Number.isInteger(expectedLauncherPid) || expectedLauncherPid <= 1) return;
273
+ const deadline = Date.now() + 60_000;
274
+ while (Date.now() < deadline && !socket.destroyed) {
275
+ try {
276
+ const state = JSON.parse(await fs.readFile(getClientUpdateStatePath(), 'utf8'));
277
+ if (String(state?.operationId || '') !== operationId
278
+ || String(state?.attemptId || '') !== attemptId) {
279
+ await wait(100);
280
+ continue;
281
+ }
282
+ if (['failed', 'restored', 'cancelled'].includes(String(state.stage || ''))) return;
283
+ const launcherPid = Number(state.launcherPid || 0);
284
+ const agentPid = Number(state.agentPid || 0);
285
+ const supervisorPid = Number(state.supervisorPid || 0);
286
+ const completedAt = String(state.completedAt || '').trim();
287
+ const exactSupervisorCompletion = state.stage === 'connected'
288
+ && state.restartVerified === true
289
+ && Number.isFinite(Date.parse(completedAt))
290
+ && launcherPid === expectedLauncherPid
291
+ && agentPid === process.pid
292
+ && Number.isInteger(supervisorPid)
293
+ && supervisorPid > 1
294
+ && String(state.productVersion || '') === PRODUCT_VERSION
295
+ && String(state.agentVersion || '') === AGENT_VERSION
296
+ && String(state.targetProductVersion || '') === PRODUCT_VERSION
297
+ && String(state.targetAgentVersion || state.targetVersion || '') === AGENT_VERSION;
298
+ if (exactSupervisorCompletion) {
299
+ writeJsonLine(socket, {
300
+ type: 'client.update.verified',
301
+ operationId,
302
+ attemptId,
303
+ launcherPid,
304
+ agentPid,
305
+ supervisorPid,
306
+ productVersion: PRODUCT_VERSION,
307
+ agentVersion: AGENT_VERSION,
308
+ completedAt
309
+ });
310
+ return;
311
+ }
312
+ } catch {
313
+ // The exact package supervisor may be replacing the atomic state file.
314
+ }
315
+ await wait(100);
316
+ }
317
+ }
267
318
 
268
319
  function printHelp() {
269
320
  console.log(`
@@ -1972,9 +2023,10 @@ function connectOnce(options, deviceId) {
1972
2023
  remoteFiles: true,
1973
2024
  fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
1974
2025
  computerAgent: options.taskEnabled,
1975
- taskDispatch: options.taskEnabled,
1976
- clientUpdate: true,
1977
- productVersion: PRODUCT_VERSION,
2026
+ taskDispatch: options.taskEnabled,
2027
+ clientUpdate: true,
2028
+ clientUpdateVerifiedProof: true,
2029
+ productVersion: PRODUCT_VERSION,
1978
2030
  agentApproval: options.taskEnabled,
1979
2031
  agentAudit: options.taskEnabled,
1980
2032
  agentTools: [...NODE_AGENT_OPERATIONS],
@@ -2000,8 +2052,13 @@ function connectOnce(options, deviceId) {
2000
2052
  options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
2001
2053
  ? message.effectivePolicy
2002
2054
  : null;
2003
- console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2004
- markClientUpdateConnected();
2055
+ console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2056
+ markClientUpdateConnected();
2057
+ void publishClientUpdateSupervisorProof(socket).catch(error => {
2058
+ if (!socket.destroyed) {
2059
+ console.error(`LiveDesk client update verification proof failed: ${error?.message || error}`);
2060
+ }
2061
+ });
2005
2062
  writeJsonLine(socket, {
2006
2063
  type: 'status',
2007
2064
  status: getStatus(options)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.238",
3
+ "version": "0.1.239",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.436",
46
- "@livedesk/fast-osx-arm64": "0.1.436",
47
- "@livedesk/fast-osx-x64": "0.1.436",
48
- "@livedesk/fast-win-x64": "0.1.436"
45
+ "@livedesk/fast-linux-x64": "0.1.437",
46
+ "@livedesk/fast-osx-arm64": "0.1.437",
47
+ "@livedesk/fast-osx-x64": "0.1.437",
48
+ "@livedesk/fast-win-x64": "0.1.437"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -1022,10 +1022,9 @@ function isLoopbackRequest(req) {
1022
1022
  return address === '127.0.0.1' || address === '::1' || address === 'localhost' || !address;
1023
1023
  }
1024
1024
 
1025
- function isTrustedApiOrigin(value, trustedWebOrigins) {
1026
- const rawOrigin = normalizeString(value, 300);
1027
- if (rawOrigin === 'null') return true;
1028
- const origin = normalizeOrigin(rawOrigin);
1025
+ function isTrustedApiOrigin(value, trustedWebOrigins) {
1026
+ const rawOrigin = normalizeString(value, 300);
1027
+ const origin = normalizeOrigin(rawOrigin);
1029
1028
  if (!origin) return false;
1030
1029
  if (trustedWebOrigins.has(origin)) return true;
1031
1030
  try {
@@ -1049,11 +1048,10 @@ function isTrustedLocalRequest(req, host, port, options = {}) {
1049
1048
  const requestHost = normalizeString(req.headers.host, 300).split(':')[0].toLowerCase();
1050
1049
  if (requestHost && requestHost !== host.toLowerCase() && requestHost !== 'localhost' && requestHost !== '127.0.0.1') return false;
1051
1050
 
1052
- // Chromium can mark modulepreload/stylesheet requests as cross-site, or
1053
- // send Origin: null when the page was opened from a local shell. Static
1054
- // assets are not privileged, and the local shell still needs to read the
1055
- // loopback runtime API to determine the active role. Mutating API routes
1056
- // remain protected by the CSRF token check below.
1051
+ // Chromium can mark modulepreload/stylesheet requests as cross-site. Static
1052
+ // assets are not privileged. An opaque Origin (`null`) is never accepted for
1053
+ // an API route because it cannot distinguish the LiveDesk shell from an
1054
+ // attacker-controlled file, sandbox, or data page.
1057
1055
  if (options.allowStaticCrossSite === true) {
1058
1056
  return true;
1059
1057
  }
@@ -1073,10 +1071,9 @@ function isTrustedLocalRequest(req, host, port, options = {}) {
1073
1071
  || fetchSite === 'none';
1074
1072
  }
1075
1073
 
1076
- function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
1077
- const origin = normalizeString(requestOrigin, 300);
1078
- if (origin === 'null') return 'null';
1079
- return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
1074
+ function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
1075
+ const origin = normalizeString(requestOrigin, 300);
1076
+ return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
1080
1077
  ? normalizeOrigin(origin)
1081
1078
  : `http://127.0.0.1:${port}`;
1082
1079
  }
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -8,10 +8,11 @@ export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
8
8
  // same device returned, so the Hub-owned compatibility bridge upgrades them.
9
9
  export const LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION = '0.1.172';
10
10
  export const LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE = 5;
11
- export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 480_000;
12
- export const LIVE_DESK_UPDATE_TIMEOUT_MS = 90 * 60_000;
13
- export const LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS = 60_000;
14
- export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
11
+ export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 480_000;
12
+ export const LIVE_DESK_UPDATE_TIMEOUT_MS = 90 * 60_000;
13
+ export const LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS = 60_000;
14
+ export const LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS = 10_000;
15
+ export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
15
16
  export const LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH = 3900;
16
17
 
17
18
  function cleanVersion(value) {
@@ -194,9 +195,10 @@ function statusForRun(run) {
194
195
  activeCount,
195
196
  queuedCount,
196
197
  pendingOfflineCount,
197
- failedCount,
198
- batchSize: run.batchSize,
199
- clientRestartStabilityMs: run.clientRestartStabilityMs,
198
+ failedCount,
199
+ batchSize: run.batchSize,
200
+ clientRestartStabilityMs: run.clientRestartStabilityMs,
201
+ verifiedClientRestartStabilityMs: run.verifiedClientRestartStabilityMs,
200
202
  latestManagerVersion: run.latestManagerVersion,
201
203
  latestClientVersion: run.latestClientVersion,
202
204
  error: run.error || '',
@@ -226,10 +228,14 @@ function resetReconnectCandidate(target) {
226
228
  target.candidatePid = 0;
227
229
  target.candidateConnectedAt = '';
228
230
  target.candidateProductVersion = '';
229
- target.candidateAgentVersion = '';
230
- target.candidateSince = '';
231
- target.stabilityDeadlineAt = '';
232
- }
231
+ target.candidateAgentVersion = '';
232
+ target.candidateSupervisorProofId = '';
233
+ target.verificationSource = '';
234
+ target.requiredStabilityMs = 0;
235
+ target.supervisorProofAt = '';
236
+ target.candidateSince = '';
237
+ target.stabilityDeadlineAt = '';
238
+ }
233
239
 
234
240
  export function createLiveDeskUpdateManager({
235
241
  remoteHub,
@@ -240,10 +246,11 @@ export function createLiveDeskUpdateManager({
240
246
  fetchImpl = globalThis.fetch,
241
247
  now = () => Date.now(),
242
248
  clientBatchSize = LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE,
243
- targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
244
- operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
245
- clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
246
- }) {
249
+ targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
250
+ operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
251
+ clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS,
252
+ verifiedClientRestartStabilityMs = LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
253
+ }) {
247
254
  let latestRelease = null;
248
255
  let checkError = '';
249
256
  let checkPromise = null;
@@ -262,12 +269,18 @@ export function createLiveDeskUpdateManager({
262
269
  effectiveTargetTimeoutMs,
263
270
  Number(operationTimeoutMs) || LIVE_DESK_UPDATE_TIMEOUT_MS
264
271
  );
265
- const effectiveClientRestartStabilityMs = Math.max(
272
+ const effectiveClientRestartStabilityMs = Math.max(
266
273
  0,
267
274
  Number.isFinite(Number(clientRestartStabilityMs))
268
275
  ? Number(clientRestartStabilityMs)
269
- : LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
270
- );
276
+ : LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
277
+ );
278
+ const effectiveVerifiedClientRestartStabilityMs = Math.max(
279
+ 5_000,
280
+ Number.isFinite(Number(verifiedClientRestartStabilityMs))
281
+ ? Number(verifiedClientRestartStabilityMs)
282
+ : LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
283
+ );
271
284
 
272
285
  const touch = () => {
273
286
  if (run) run.updatedAt = new Date(now()).toISOString();
@@ -341,9 +354,10 @@ export function createLiveDeskUpdateManager({
341
354
  queuedCount: 0,
342
355
  pendingOfflineCount: 0,
343
356
  failedCount: 0,
344
- batchSize: effectiveClientBatchSize,
345
- clientRestartStabilityMs: effectiveClientRestartStabilityMs,
346
- targets: []
357
+ batchSize: effectiveClientBatchSize,
358
+ clientRestartStabilityMs: effectiveClientRestartStabilityMs,
359
+ verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
360
+ targets: []
347
361
  })
348
362
  };
349
363
  };
@@ -535,9 +549,30 @@ export function createLiveDeskUpdateManager({
535
549
  const connectedAtEpochMs = Date.parse(connectedAt);
536
550
  const dispatchedAtEpochMs = Date.parse(target.dispatchedAt || '');
537
551
  const candidatePid = Math.max(0, Math.floor(Number(device?.pid) || 0));
538
- const candidateProductVersion = String(device?.productVersion || '');
539
- const candidateAgentVersion = String(device?.agentVersion || '');
540
- const qualifiesForStability = device?.connected === true
552
+ const candidateProductVersion = String(device?.productVersion || '');
553
+ const candidateAgentVersion = String(device?.agentVersion || '');
554
+ const supervisorProof = device?.clientUpdateProof;
555
+ const supervisorProofId = [
556
+ String(supervisorProof?.operationId || ''),
557
+ String(supervisorProof?.attemptId || ''),
558
+ String(supervisorProof?.sessionId || ''),
559
+ String(supervisorProof?.agentPid || ''),
560
+ String(supervisorProof?.launcherPid || ''),
561
+ String(supervisorProof?.completedAt || '')
562
+ ].join(':');
563
+ const hasSupervisorProof = !!supervisorProof
564
+ && String(supervisorProof.operationId || '') === run.operationId
565
+ && !!String(supervisorProof.attemptId || '')
566
+ && String(supervisorProof.sessionId || '') === sessionId
567
+ && Number(supervisorProof.agentPid || 0) === candidatePid
568
+ && Number(supervisorProof.launcherPid || 0) > 1
569
+ && cleanVersion(supervisorProof.productVersion) === cleanVersion(candidateProductVersion)
570
+ && cleanVersion(supervisorProof.agentVersion) === cleanVersion(candidateAgentVersion)
571
+ && Date.parse(String(supervisorProof.receivedAt || '')) >= connectedAtEpochMs;
572
+ const requiredStabilityMs = hasSupervisorProof
573
+ ? run.verifiedClientRestartStabilityMs
574
+ : run.clientRestartStabilityMs;
575
+ const qualifiesForStability = device?.connected === true
541
576
  && !!sessionId
542
577
  && candidatePid > 1
543
578
  && sessionId !== target.dispatchedSessionId
@@ -546,9 +581,10 @@ export function createLiveDeskUpdateManager({
546
581
  const sameCandidate = qualifiesForStability
547
582
  && target.candidateSessionId === sessionId
548
583
  && target.candidatePid === candidatePid
549
- && target.candidateConnectedAt === connectedAt
550
- && target.candidateProductVersion === candidateProductVersion
551
- && target.candidateAgentVersion === candidateAgentVersion;
584
+ && target.candidateConnectedAt === connectedAt
585
+ && target.candidateProductVersion === candidateProductVersion
586
+ && target.candidateAgentVersion === candidateAgentVersion
587
+ && target.candidateSupervisorProofId === (hasSupervisorProof ? supervisorProofId : '');
552
588
 
553
589
  if (!qualifiesForStability) {
554
590
  resetReconnectCandidate(target);
@@ -556,12 +592,18 @@ export function createLiveDeskUpdateManager({
556
592
  target.candidateSessionId = sessionId;
557
593
  target.candidatePid = candidatePid;
558
594
  target.candidateConnectedAt = connectedAt;
559
- target.candidateProductVersion = candidateProductVersion;
560
- target.candidateAgentVersion = candidateAgentVersion;
561
- target.candidateSince = new Date(currentTime).toISOString();
562
- target.stabilityDeadlineAt = new Date(
563
- currentTime + run.clientRestartStabilityMs
564
- ).toISOString();
595
+ target.candidateProductVersion = candidateProductVersion;
596
+ target.candidateAgentVersion = candidateAgentVersion;
597
+ target.candidateSupervisorProofId = hasSupervisorProof ? supervisorProofId : '';
598
+ target.verificationSource = hasSupervisorProof ? 'supervisor-proof' : 'hub-session';
599
+ target.requiredStabilityMs = requiredStabilityMs;
600
+ target.supervisorProofAt = hasSupervisorProof
601
+ ? String(supervisorProof.receivedAt || supervisorProof.completedAt || '')
602
+ : '';
603
+ target.candidateSince = new Date(currentTime).toISOString();
604
+ target.stabilityDeadlineAt = new Date(
605
+ currentTime + requiredStabilityMs
606
+ ).toISOString();
565
607
  }
566
608
 
567
609
  if (qualifiesForStability
@@ -646,8 +688,9 @@ export function createLiveDeskUpdateManager({
646
688
  latestManagerVersion: release.latestManagerVersion,
647
689
  latestClientVersion: release.latestClientVersion,
648
690
  needsHubRestart,
649
- batchSize: effectiveClientBatchSize,
650
- clientRestartStabilityMs: effectiveClientRestartStabilityMs,
691
+ batchSize: effectiveClientBatchSize,
692
+ clientRestartStabilityMs: effectiveClientRestartStabilityMs,
693
+ verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
651
694
  error: '',
652
695
  targets: targets.map(device => ({
653
696
  deviceId: String(device.deviceId || ''),
@@ -666,9 +709,13 @@ export function createLiveDeskUpdateManager({
666
709
  candidateSessionId: '',
667
710
  candidatePid: 0,
668
711
  candidateConnectedAt: '',
669
- candidateProductVersion: '',
670
- candidateAgentVersion: '',
671
- candidateSince: '',
712
+ candidateProductVersion: '',
713
+ candidateAgentVersion: '',
714
+ candidateSupervisorProofId: '',
715
+ verificationSource: '',
716
+ requiredStabilityMs: 0,
717
+ supervisorProofAt: '',
718
+ candidateSince: '',
672
719
  stabilityDeadlineAt: '',
673
720
  completedAt: '',
674
721
  error: ''
@@ -698,7 +745,9 @@ export function createLiveDeskUpdateManager({
698
745
 
699
746
  const handleRemoteEvent = (type, event) => {
700
747
  if (!run || run.state !== 'waiting-for-clients') return;
701
- if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
748
+ if (type === 'RemoteDeviceConnected'
749
+ || type === 'RemoteDeviceDisconnected'
750
+ || type === 'RemoteClientUpdateVerified') {
702
751
  verifyTargets();
703
752
  return;
704
753
  }
@@ -286,7 +286,7 @@ function normalizePort(value) {
286
286
  return clampNumber(value, 0, 65535, DEFAULT_REMOTE_HUB_PORT);
287
287
  }
288
288
 
289
- function safeString(value, maxLength = 200) {
289
+ function safeString(value, maxLength = 200) {
290
290
  return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
291
291
  }
292
292
 
@@ -1123,6 +1123,42 @@ function safeText(value, maxLength = 1000) {
1123
1123
  return String(value ?? '').replace(/\0/g, '').trim().slice(0, maxLength);
1124
1124
  }
1125
1125
 
1126
+ function normalizeClientUpdateVerifiedProof(device, message, receivedAt) {
1127
+ const operationId = safeString(message?.operationId, 180);
1128
+ const attemptId = safeString(message?.attemptId, 180);
1129
+ const productVersion = safeString(message?.productVersion, 40);
1130
+ const agentVersion = safeString(message?.agentVersion, 40);
1131
+ const completedAt = safeString(message?.completedAt, 64);
1132
+ const launcherPid = Math.max(0, Math.floor(Number(message?.launcherPid) || 0));
1133
+ const agentPid = Math.max(0, Math.floor(Number(message?.agentPid) || 0));
1134
+ const supervisorPid = Math.max(0, Math.floor(Number(message?.supervisorPid) || 0));
1135
+ if (!operationId
1136
+ || !attemptId
1137
+ || !productVersion
1138
+ || !agentVersion
1139
+ || !Number.isFinite(Date.parse(completedAt))
1140
+ || launcherPid <= 1
1141
+ || agentPid <= 1
1142
+ || supervisorPid <= 1
1143
+ || agentPid !== Number(device?.pid || 0)
1144
+ || productVersion !== String(device?.productVersion || '')
1145
+ || agentVersion !== String(device?.agentVersion || '')) {
1146
+ return null;
1147
+ }
1148
+ return {
1149
+ operationId,
1150
+ attemptId,
1151
+ sessionId: String(device.sessionId || ''),
1152
+ launcherPid,
1153
+ agentPid,
1154
+ supervisorPid,
1155
+ productVersion,
1156
+ agentVersion,
1157
+ completedAt,
1158
+ receivedAt
1159
+ };
1160
+ }
1161
+
1126
1162
  function normalizeRemoteKeyboardKey(value) {
1127
1163
  const raw = String(value ?? '').replace(/\0/g, '');
1128
1164
  return raw === ' ' ? ' ' : safeString(raw, 80);
@@ -1626,8 +1662,9 @@ function serializeDevice(device, options = {}) {
1626
1662
  byteLength: device.latestAudioFrame.byteLength
1627
1663
  }
1628
1664
  : null,
1629
- latestAudioStatus: device.latestAudioStatus ? { ...device.latestAudioStatus } : null,
1630
- udp: device.udp ? { ...device.udp } : { enabled: false, state: 'tcp-only', ready: false },
1665
+ latestAudioStatus: device.latestAudioStatus ? { ...device.latestAudioStatus } : null,
1666
+ clientUpdateProof: device.clientUpdateProof ? { ...device.clientUpdateProof } : null,
1667
+ udp: device.udp ? { ...device.udp } : { enabled: false, state: 'tcp-only', ready: false },
1631
1668
  latestTask: device.latestTask ? { ...device.latestTask } : null,
1632
1669
  synthetic: device.synthetic === true,
1633
1670
  recentTasks: Array.isArray(device.recentTasks)
@@ -4427,8 +4464,9 @@ export function createRemoteHub(options = {}) {
4427
4464
  liveCapturePause: existing?.liveCapturePause || null,
4428
4465
  activeAudioStream: null,
4429
4466
  latestAudioFrame: null,
4430
- latestAudioStatus: null,
4431
- udp: {
4467
+ latestAudioStatus: null,
4468
+ clientUpdateProof: null,
4469
+ udp: {
4432
4470
  enabled: capabilities.udpP2p === true,
4433
4471
  state: 'tcp-only',
4434
4472
  ready: false,
@@ -7441,7 +7479,7 @@ export function createRemoteHub(options = {}) {
7441
7479
  device.counters.statusReceived += 1;
7442
7480
  emitRemoteEvent('RemoteDeviceStatus', device);
7443
7481
  break;
7444
- case 'command.result':
7482
+ case 'command.result':
7445
7483
  case 'command.error':
7446
7484
  device.counters.commandResultsReceived += 1;
7447
7485
  {
@@ -7469,9 +7507,23 @@ export function createRemoteHub(options = {}) {
7469
7507
  commandId: safeString(message.commandId, 128),
7470
7508
  result: message.result ?? null,
7471
7509
  error: safeString(message.error, 500)
7472
- });
7473
- break;
7474
- case 'input.applied':
7510
+ });
7511
+ break;
7512
+ case 'client.update.verified': {
7513
+ const receivedAt = new Date().toISOString();
7514
+ const proof = normalizeClientUpdateVerifiedProof(device, message, receivedAt);
7515
+ if (!proof) {
7516
+ logWarn(
7517
+ 'remote',
7518
+ `ignored invalid Client update supervisor proof from ${device.deviceName} (${device.deviceId})`
7519
+ );
7520
+ break;
7521
+ }
7522
+ device.clientUpdateProof = proof;
7523
+ emitRemoteEvent('RemoteClientUpdateVerified', device, { proof });
7524
+ break;
7525
+ }
7526
+ case 'input.applied':
7475
7527
  case 'input.error':
7476
7528
  handleRemoteInputOutcome(device, message, 'main');
7477
7529
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.498",
4
- "livedeskClientVersion": "0.1.238",
3
+ "version": "0.1.500",
4
+ "livedeskClientVersion": "0.1.239",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -52,10 +52,10 @@
52
52
  "ws": "^8.18.3"
53
53
  },
54
54
  "optionalDependencies": {
55
- "@livedesk/fast-linux-x64": "0.1.436",
56
- "@livedesk/fast-osx-arm64": "0.1.436",
57
- "@livedesk/fast-osx-x64": "0.1.436",
58
- "@livedesk/fast-win-x64": "0.1.436"
55
+ "@livedesk/fast-linux-x64": "0.1.437",
56
+ "@livedesk/fast-osx-arm64": "0.1.437",
57
+ "@livedesk/fast-osx-x64": "0.1.437",
58
+ "@livedesk/fast-win-x64": "0.1.437"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"