livedesk 0.1.446 → 0.1.447

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.
@@ -0,0 +1,414 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import {
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ statSync,
7
+ unlinkSync,
8
+ writeFileSync
9
+ } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import os from 'node:os';
12
+
13
+ export const RUNTIME_SHUTDOWN_PROTOCOL_VERSION = 1;
14
+ export const DEFAULT_RUNTIME_SHUTDOWN_POLL_MS = 100;
15
+ export const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 8_000;
16
+ export const DEFAULT_RUNTIME_SHUTDOWN_ACK_TIMEOUT_MS = 2_000;
17
+ export const DEFAULT_RUNTIME_SHUTDOWN_TTL_MS = 15_000;
18
+
19
+ const REQUEST_FILE_NAME = 'livedesk-runtime-shutdown-request.json';
20
+ const ACK_FILE_NAME = 'livedesk-runtime-shutdown-ack.json';
21
+ const MAX_REQUEST_TTL_MS = 60_000;
22
+ const MAX_FUTURE_CLOCK_SKEW_MS = 5_000;
23
+
24
+ function normalizeOwner(value) {
25
+ const owner = {
26
+ pid: Number(value?.pid),
27
+ ownerToken: String(value?.ownerToken || '').trim(),
28
+ ownerInstanceMarker: String(value?.ownerInstanceMarker || '').trim(),
29
+ ownerStartOrder: String(value?.ownerStartOrder || '').trim()
30
+ };
31
+ if (!Number.isInteger(owner.pid)
32
+ || owner.pid <= 1
33
+ || !/^[a-f0-9]{32}$/i.test(owner.ownerToken)
34
+ || !owner.ownerInstanceMarker
35
+ || !owner.ownerStartOrder) {
36
+ return null;
37
+ }
38
+ return owner;
39
+ }
40
+
41
+ function ownersMatch(leftValue, rightValue) {
42
+ const left = normalizeOwner(leftValue);
43
+ const right = normalizeOwner(rightValue);
44
+ return !!left
45
+ && !!right
46
+ && left.pid === right.pid
47
+ && left.ownerToken === right.ownerToken
48
+ && left.ownerInstanceMarker === right.ownerInstanceMarker
49
+ && left.ownerStartOrder === right.ownerStartOrder;
50
+ }
51
+
52
+ function readJson(path) {
53
+ try {
54
+ return JSON.parse(readFileSync(path, 'utf8'));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function fileRevision(path) {
61
+ try {
62
+ const stat = statSync(path, { bigint: true });
63
+ return `${stat.mtimeNs}:${stat.size}`;
64
+ } catch {
65
+ return '';
66
+ }
67
+ }
68
+
69
+ function writeJsonAtomic(path, value) {
70
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
71
+ const temporaryPath = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
72
+ let published = false;
73
+ try {
74
+ writeFileSync(temporaryPath, JSON.stringify(value, null, 2), {
75
+ encoding: 'utf8',
76
+ mode: 0o600,
77
+ flag: 'wx'
78
+ });
79
+ renameSync(temporaryPath, path);
80
+ published = true;
81
+ } finally {
82
+ if (!published) {
83
+ try { unlinkSync(temporaryPath); } catch { /* temporary file was never published */ }
84
+ }
85
+ }
86
+ }
87
+
88
+ function isValidRequest(request, nowMs) {
89
+ const requestedAtMs = Date.parse(String(request?.requestedAt || ''));
90
+ const expiresAtMs = Date.parse(String(request?.expiresAt || ''));
91
+ return request?.protocolVersion === RUNTIME_SHUTDOWN_PROTOCOL_VERSION
92
+ && /^[a-f0-9]{32,128}$/i.test(String(request?.requestId || ''))
93
+ && !!normalizeOwner(request?.target)
94
+ && Number.isFinite(requestedAtMs)
95
+ && Number.isFinite(expiresAtMs)
96
+ && requestedAtMs <= nowMs + MAX_FUTURE_CLOCK_SKEW_MS
97
+ && expiresAtMs > nowMs
98
+ && expiresAtMs > requestedAtMs
99
+ && expiresAtMs - requestedAtMs <= MAX_REQUEST_TTL_MS;
100
+ }
101
+
102
+ function isMatchingAck(ack, request) {
103
+ return ack?.protocolVersion === RUNTIME_SHUTDOWN_PROTOCOL_VERSION
104
+ && ack?.requestId === request.requestId
105
+ && ownersMatch(ack?.target, request.target)
106
+ && ['accepted', 'completed', 'failed'].includes(String(ack?.status || ''));
107
+ }
108
+
109
+ function isSameOwnerDrainAck(ack, request, nowMs) {
110
+ const requestedAtMs = Date.parse(String(request?.requestedAt || ''));
111
+ const acceptedAtMs = Date.parse(String(ack?.acceptedAt || ''));
112
+ return ack?.protocolVersion === RUNTIME_SHUTDOWN_PROTOCOL_VERSION
113
+ && ack?.requestId !== request.requestId
114
+ && /^[a-f0-9]{32,128}$/i.test(String(ack?.requestId || ''))
115
+ && ownersMatch(ack?.target, request.target)
116
+ && ['accepted', 'completed', 'failed'].includes(String(ack?.status || ''))
117
+ && Number.isFinite(requestedAtMs)
118
+ && Number.isFinite(acceptedAtMs)
119
+ && acceptedAtMs >= requestedAtMs - MAX_REQUEST_TTL_MS
120
+ && acceptedAtMs <= nowMs + MAX_FUTURE_CLOCK_SKEW_MS;
121
+ }
122
+
123
+ function clampMilliseconds(value, fallback, minimum, maximum) {
124
+ const parsed = Number(value);
125
+ return Number.isFinite(parsed)
126
+ ? Math.min(maximum, Math.max(minimum, Math.trunc(parsed)))
127
+ : fallback;
128
+ }
129
+
130
+ function wait(milliseconds) {
131
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
132
+ }
133
+
134
+ export function getRuntimeShutdownPaths(stateDir = join(os.homedir(), '.livedesk')) {
135
+ return {
136
+ requestPath: join(stateDir, REQUEST_FILE_NAME),
137
+ ackPath: join(stateDir, ACK_FILE_NAME)
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Contract:
143
+ * - One watcher belongs to one immutable launcher-lock owner.
144
+ * - Only a non-expired request matching all exact owner fields is accepted.
145
+ * - ACK is published before cleanup starts so a replacement can distinguish
146
+ * cooperative shutdown from an unresponsive/stale launcher.
147
+ * - onShutdown is invoked at most once. Exact process-tree teardown remains the
148
+ * caller's fallback if the accepted owner does not actually exit.
149
+ */
150
+ export function startRuntimeShutdownWatcher({
151
+ stateDir,
152
+ owner,
153
+ onShutdown,
154
+ pollMs = DEFAULT_RUNTIME_SHUTDOWN_POLL_MS,
155
+ now = () => Date.now(),
156
+ reportError = error => console.warn(
157
+ `[LiveDesk] Cooperative shutdown mailbox warning: ${error?.message || error}`
158
+ )
159
+ } = {}) {
160
+ const exactOwner = normalizeOwner(owner);
161
+ if (!exactOwner) {
162
+ throw new Error('A cooperative LiveDesk shutdown watcher requires an exact runtime-lock owner.');
163
+ }
164
+ if (typeof onShutdown !== 'function') {
165
+ throw new Error('A cooperative LiveDesk shutdown watcher requires an onShutdown handler.');
166
+ }
167
+
168
+ const paths = getRuntimeShutdownPaths(stateDir);
169
+ const intervalMs = clampMilliseconds(pollMs, DEFAULT_RUNTIME_SHUTDOWN_POLL_MS, 25, 1_000);
170
+ let lastRequestRevision = '';
171
+ let stopped = false;
172
+ let shutdownAccepted = false;
173
+ let polling = false;
174
+ const report = error => {
175
+ try { reportError(error); } catch { /* diagnostics cannot own runtime liveness */ }
176
+ };
177
+
178
+ const publishAck = (request, status, details = {}) => {
179
+ writeJsonAtomic(paths.ackPath, {
180
+ protocolVersion: RUNTIME_SHUTDOWN_PROTOCOL_VERSION,
181
+ requestId: request.requestId,
182
+ target: exactOwner,
183
+ receiverPid: process.pid,
184
+ status,
185
+ acceptedAt: details.acceptedAt,
186
+ completedAt: details.completedAt,
187
+ error: details.error
188
+ });
189
+ };
190
+
191
+ const pollNow = async () => {
192
+ if (stopped || shutdownAccepted || polling) return false;
193
+ polling = true;
194
+ try {
195
+ const revision = fileRevision(paths.requestPath);
196
+ if (!revision || revision === lastRequestRevision) return false;
197
+ lastRequestRevision = revision;
198
+ const request = readJson(paths.requestPath);
199
+ const observedAtMs = Number(now());
200
+ if (!isValidRequest(request, observedAtMs)
201
+ || !ownersMatch(request.target, exactOwner)) {
202
+ return false;
203
+ }
204
+
205
+ const acceptedAt = new Date(observedAtMs).toISOString();
206
+ try {
207
+ // ACK ownership must become durable before cleanup starts. If the
208
+ // atomic write fails, leave the watcher live and retry this unchanged
209
+ // request on the next cadence instead of orphaning children.
210
+ publishAck(request, 'accepted', { acceptedAt });
211
+ } catch (error) {
212
+ lastRequestRevision = '';
213
+ report(error);
214
+ return false;
215
+ }
216
+ shutdownAccepted = true;
217
+ clearInterval(interval);
218
+ try {
219
+ await onShutdown({
220
+ requestId: request.requestId,
221
+ reason: String(request.reason || 'replacement-launch'),
222
+ requesterPid: Number(request.requesterPid || 0),
223
+ requestedAt: request.requestedAt,
224
+ acceptedAt,
225
+ target: exactOwner
226
+ });
227
+ try {
228
+ publishAck(request, 'completed', {
229
+ acceptedAt,
230
+ completedAt: new Date(Number(now())).toISOString()
231
+ });
232
+ } catch (error) {
233
+ report(error);
234
+ }
235
+ } catch (error) {
236
+ try {
237
+ publishAck(request, 'failed', {
238
+ acceptedAt,
239
+ completedAt: new Date(Number(now())).toISOString(),
240
+ error: error instanceof Error ? error.message : String(error)
241
+ });
242
+ } catch (ackError) {
243
+ report(ackError);
244
+ }
245
+ }
246
+ return true;
247
+ } finally {
248
+ polling = false;
249
+ }
250
+ };
251
+
252
+ const interval = setInterval(() => {
253
+ void pollNow().catch(report);
254
+ }, intervalMs);
255
+ interval.unref?.();
256
+ void pollNow().catch(report);
257
+
258
+ return {
259
+ paths,
260
+ pollNow,
261
+ stop() {
262
+ if (stopped) return;
263
+ stopped = true;
264
+ clearInterval(interval);
265
+ }
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Requests graceful cleanup from an exact launcher generation. The request is
271
+ * bounded; callers must retain their exact PID/start-time teardown fallback.
272
+ */
273
+ export async function requestCooperativeRuntimeShutdown({
274
+ stateDir,
275
+ target,
276
+ reason = 'replacement-launch',
277
+ timeoutMs = DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS,
278
+ ackTimeoutMs = DEFAULT_RUNTIME_SHUTDOWN_ACK_TIMEOUT_MS,
279
+ pollMs = DEFAULT_RUNTIME_SHUTDOWN_POLL_MS,
280
+ ttlMs = DEFAULT_RUNTIME_SHUTDOWN_TTL_MS,
281
+ isTargetAlive,
282
+ now = () => Date.now()
283
+ } = {}) {
284
+ const exactTarget = normalizeOwner(target);
285
+ if (!exactTarget) {
286
+ return {
287
+ requested: false,
288
+ acknowledged: false,
289
+ completed: false,
290
+ exited: false,
291
+ timedOut: false,
292
+ error: new Error('An exact runtime-lock owner is required for cooperative shutdown.')
293
+ };
294
+ }
295
+
296
+ const boundedTimeoutMs = clampMilliseconds(
297
+ timeoutMs,
298
+ DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS,
299
+ 100,
300
+ MAX_REQUEST_TTL_MS - 1_000
301
+ );
302
+ const boundedPollMs = clampMilliseconds(
303
+ pollMs,
304
+ DEFAULT_RUNTIME_SHUTDOWN_POLL_MS,
305
+ 25,
306
+ 1_000
307
+ );
308
+ const boundedAckTimeoutMs = Math.min(
309
+ boundedTimeoutMs,
310
+ clampMilliseconds(
311
+ ackTimeoutMs,
312
+ Math.min(DEFAULT_RUNTIME_SHUTDOWN_ACK_TIMEOUT_MS, boundedTimeoutMs),
313
+ 100,
314
+ boundedTimeoutMs
315
+ )
316
+ );
317
+ const boundedTtlMs = clampMilliseconds(
318
+ ttlMs,
319
+ Math.max(DEFAULT_RUNTIME_SHUTDOWN_TTL_MS, boundedTimeoutMs + 2_000),
320
+ boundedTimeoutMs + 1_000,
321
+ MAX_REQUEST_TTL_MS
322
+ );
323
+ const paths = getRuntimeShutdownPaths(stateDir);
324
+ const requestedAtMs = Number(now());
325
+ const request = {
326
+ protocolVersion: RUNTIME_SHUTDOWN_PROTOCOL_VERSION,
327
+ requestId: randomBytes(24).toString('hex'),
328
+ target: exactTarget,
329
+ requesterPid: process.pid,
330
+ reason: String(reason || 'replacement-launch').slice(0, 200),
331
+ requestedAt: new Date(requestedAtMs).toISOString(),
332
+ expiresAt: new Date(requestedAtMs + boundedTtlMs).toISOString()
333
+ };
334
+
335
+ try {
336
+ writeJsonAtomic(paths.requestPath, request);
337
+ } catch (error) {
338
+ return {
339
+ requested: false,
340
+ acknowledged: false,
341
+ completed: false,
342
+ exited: false,
343
+ timedOut: false,
344
+ requestId: request.requestId,
345
+ error
346
+ };
347
+ }
348
+
349
+ const deadline = requestedAtMs + boundedTimeoutMs;
350
+ const ackDeadline = requestedAtMs + boundedAckTimeoutMs;
351
+ let acknowledged = false;
352
+ let completed = false;
353
+ let ackStatus = '';
354
+ let ackError = '';
355
+ let sharedOwnerAck = false;
356
+ while (Number(now()) < deadline) {
357
+ if (typeof isTargetAlive === 'function') {
358
+ try {
359
+ if (!await isTargetAlive(exactTarget)) {
360
+ return {
361
+ requested: true,
362
+ acknowledged,
363
+ completed,
364
+ exited: true,
365
+ timedOut: false,
366
+ requestId: request.requestId,
367
+ ackStatus,
368
+ ackError,
369
+ sharedOwnerAck
370
+ };
371
+ }
372
+ } catch {
373
+ // Exact fallback code owns process inspection errors. Keep waiting for
374
+ // either a valid ACK or its bounded deadline.
375
+ }
376
+ }
377
+
378
+ const ack = readJson(paths.ackPath);
379
+ const observedAtMs = Number(now());
380
+ const exactRequestAck = isMatchingAck(ack, request);
381
+ const sameOwnerDrainAck = !exactRequestAck
382
+ && isSameOwnerDrainAck(ack, request, observedAtMs);
383
+ if (exactRequestAck || sameOwnerDrainAck) {
384
+ acknowledged = true;
385
+ sharedOwnerAck = sameOwnerDrainAck;
386
+ ackStatus = String(ack.status);
387
+ ackError = String(ack.error || '');
388
+ completed = ackStatus === 'completed';
389
+ if (ackStatus === 'failed') break;
390
+ }
391
+ if (!acknowledged && Number(now()) >= ackDeadline) break;
392
+ await wait(Math.min(boundedPollMs, Math.max(1, deadline - Number(now()))));
393
+ }
394
+
395
+ let exited = false;
396
+ if (typeof isTargetAlive === 'function') {
397
+ try {
398
+ exited = !await isTargetAlive(exactTarget);
399
+ } catch {
400
+ exited = false;
401
+ }
402
+ }
403
+ return {
404
+ requested: true,
405
+ acknowledged,
406
+ completed,
407
+ exited,
408
+ timedOut: !exited && ackStatus !== 'failed',
409
+ requestId: request.requestId,
410
+ ackStatus,
411
+ ackError,
412
+ sharedOwnerAck
413
+ };
414
+ }
@@ -1552,7 +1552,7 @@ function remotePolicyAllows(options, command) {
1552
1552
 
1553
1553
  function buildClientUpdateBootstrapScript() {
1554
1554
  return [
1555
- 'const { spawn } = require("node:child_process");',
1555
+ 'const { execFile, spawn } = require("node:child_process");',
1556
1556
  'const fs = require("node:fs");',
1557
1557
  'const path = require("node:path");',
1558
1558
  'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
@@ -1569,15 +1569,28 @@ function buildClientUpdateBootstrapScript() {
1569
1569
  'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
1570
1570
  'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1571
1571
  'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1572
+ 'const runWindowsPowerShell = script => new Promise((resolve, reject) => { execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 15000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || error.message || "PowerShell failed").trim())); return; } resolve(String(stdout || "").trim()); }); });',
1573
+ 'const queryWindowsProcessTable = async () => { const stdout = await runWindowsPowerShell(`$ErrorActionPreference = "Stop"; $records = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object { $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" }; [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks } }); [pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 4`); if (!stdout) throw new Error("Windows process snapshot returned no data"); const parsed = JSON.parse(stdout); const values = parsed?.Records == null ? [] : (Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records]); return values.map(item => ({ pid: Number(item?.ProcessId || 0), parentPid: Number(item?.ParentProcessId || 0), startOrder: String(item?.CreationUtcTicks || "") })).filter(item => Number.isInteger(item.pid) && item.pid > 1 && /^\\d+$/.test(item.startOrder)); };',
1574
+ 'const sameWindowsIdentity = (left, right) => Number(left?.pid || 0) === Number(right?.pid || 0) && String(left?.startOrder || "") === String(right?.startOrder || "");',
1575
+ 'const captureWindowsIdentity = async pid => { const processId = Number(pid || 0); const deadline = Date.now() + 2500; let lastError = null; do { try { const snapshot = await queryWindowsProcessTable(); const identity = snapshot.find(item => item.pid === processId) || null; if (identity) return identity; } catch (error) { lastError = error; } if (Date.now() < deadline) await sleep(50); } while (Date.now() < deadline); throw new Error(`Could not capture immutable Windows CreationDate for pid=${processId}: ${lastError?.message || "process not visible"}`); };',
1576
+ 'const windowsTicksAtMilliseconds = value => BigInt(Math.trunc(Number(value) || Date.now())) * 10000n + 621355968000000000n;',
1577
+ 'const setWindowsRootLifetimeEnd = (child, ticks) => { const candidate = BigInt(ticks); const previous = child.livedeskWindowsRootLifetimeEndTicks; if (!previous || candidate < BigInt(previous)) child.livedeskWindowsRootLifetimeEndTicks = String(candidate); return BigInt(child.livedeskWindowsRootLifetimeEndTicks); };',
1578
+ 'const mergeRootAbsentWindowsTree = (child, rootPid, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const exactCurrentByPid = new Map(); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } const lowerBound = windowsTicksAtMilliseconds(Number(child.livedeskWindowsSpawnedAtMs || Date.now()) - 2000); const upperBound = child.livedeskWindowsRootLifetimeEndTicks ? BigInt(child.livedeskWindowsRootLifetimeEndTicks) : null; let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === rootPid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < lowerBound || (upperBound && recordStart > upperBound)) continue; } catch { continue; } const parent = record.parentPid === rootPid ? { depth: 0 } : exactCurrentByPid.get(record.parentPid); if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
1579
+ 'const mergeExactWindowsTree = (child, root, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const currentRoot = currentByPid.get(root.pid) || null; const rootIsExact = sameWindowsIdentity(currentRoot, root); const rootWasReused = Boolean(currentRoot && !rootIsExact); if (rootWasReused) setWindowsRootLifetimeEnd(child, BigInt(currentRoot.startOrder) - 1n); else if (!currentRoot && child.livedeskWindowsRootExitedAtMs) setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); const rootLifetimeClosed = Boolean(child.livedeskWindowsRootLifetimeEndTicks); if (rootIsExact) tracked.set(`${root.pid}:${root.startOrder}`, { ...root, depth: 0 }); const exactCurrentByPid = new Map(); if (rootIsExact) exactCurrentByPid.set(root.pid, { ...root, depth: 0 }); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === root.pid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < BigInt(root.startOrder)) continue; if (rootLifetimeClosed && recordStart > BigInt(child.livedeskWindowsRootLifetimeEndTicks)) continue; } catch { continue; } let parent = exactCurrentByPid.get(record.parentPid) || null; if (!parent && !currentRoot && rootLifetimeClosed && record.parentPid === root.pid) parent = { ...root, depth: 0 }; if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
1580
+ 'const refreshTrackedWindowsTree = async (child, root = child.livedeskWindowsIdentity || null) => { const snapshot = await queryWindowsProcessTable(); if (root) mergeExactWindowsTree(child, root, snapshot); else mergeRootAbsentWindowsTree(child, Number(child.pid || 0), snapshot); return snapshot; };',
1581
+ 'const stopExactWindowsRecords = async records => { if (!Array.isArray(records) || records.length === 0) return; const encoded = Buffer.from(JSON.stringify(records.map(record => ({ pid: record.pid, startOrder: record.startOrder }))), "utf8").toString("base64"); const script = `$ErrorActionPreference = "Stop"; $targetsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encoded}")); $targets = ConvertFrom-Json -InputObject $targetsJson; $failures = [System.Collections.Generic.List[string]]::new(); foreach ($item in $targets) { $targetPid = [int]$item.pid; $expectedStartTicks = [string]$item.startOrder; try { $target = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $target) { continue }; $targetStartTicks = if ($target.CreationDate) { [string]$target.CreationDate.ToUniversalTime().Ticks } else { "" }; if ($targetStartTicks -ne $expectedStartTicks) { continue }; Stop-Process -Id $targetPid -Force -ErrorAction Stop } catch { $failures.Add("pid=$targetPid $($_.Exception.Message)") } }; if ($failures.Count -gt 0) { [Console]::Error.WriteLine(($failures -join "; ")); exit 1 }`; await runWindowsPowerShell(script); };',
1582
+ 'const trackWindowsChild = child => { if (process.platform !== "win32" || Number(child?.pid || 0) <= 1 || child.livedeskWindowsIdentityPromise) return; child.livedeskWindowsTrackedRecords = new Map(); child.livedeskWindowsSpawnedAtMs = Date.now(); child.livedeskWindowsRootExitedAtMs = null; child.livedeskWindowsTrackingStopped = false; child.once("exit", () => { if (!child.livedeskWindowsRootExitedAtMs) child.livedeskWindowsRootExitedAtMs = Date.now(); setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); }); const refresh = () => { void refreshTrackedWindowsTree(child).then(() => { child.livedeskWindowsTrackingError = null; }).catch(error => { child.livedeskWindowsTrackingError = error; }); }; refresh(); child.livedeskWindowsTrackingTimer = setInterval(refresh, 250); child.livedeskWindowsTrackingTimer.unref?.(); child.livedeskWindowsIdentityPromise = (async () => { while (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) { try { const identity = await captureWindowsIdentity(child.pid); child.livedeskWindowsIdentity = identity; refresh(); return identity; } catch (error) { child.livedeskWindowsIdentityError = error; if (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) await sleep(250); } } return null; })(); };',
1583
+ 'const stopWindowsTracking = child => { child.livedeskWindowsTrackingStopped = true; if (child?.livedeskWindowsTrackingTimer) clearInterval(child.livedeskWindowsTrackingTimer); child.livedeskWindowsTrackingTimer = null; };',
1584
+ 'const terminateExactWindowsTrackedTree = (child, root) => { if (child.livedeskWindowsDrainPromise) return child.livedeskWindowsDrainPromise; child.livedeskWindowsDrainPromise = (async () => { let attempt = 0; let emptyPasses = 0; for (;;) { attempt += 1; try { const snapshot = await refreshTrackedWindowsTree(child, root); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const remaining = [...child.livedeskWindowsTrackedRecords.values()].filter(target => sameWindowsIdentity(currentByPid.get(target.pid), target)).sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0)); if (remaining.length === 0) { emptyPasses += 1; if (emptyPasses >= 2) { stopWindowsTracking(child); return; } } else { emptyPasses = 0; await stopExactWindowsRecords(remaining); } child.livedeskWindowsTrackingError = null; } catch (error) { emptyPasses = 0; child.livedeskWindowsTrackingError = error; if (attempt === 1 || attempt % 10 === 0) process.stderr.write(`LiveDesk update cleanup is retrying exact Windows process-tree drain: ${error?.message || error}\\n`); } await sleep(Math.min(1000, 100 + (attempt * 50))); } })(); return child.livedeskWindowsDrainPromise; };',
1572
1585
  'const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));',
1573
1586
  'const waitForStateLock = ms => Atomics.wait(lockWaitBuffer, 0, 0, Math.max(1, ms));',
1574
1587
  'const removeAbandonedStateLock = () => { let owner = null; try { owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); } catch {} const ownerPid = Number(owner?.pid || 0); if (Number.isInteger(ownerPid) && ownerPid > 1) { if (isAlive(ownerPid)) return false; } else { try { if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5000) return false; } catch { return true; } } try { fs.rmSync(stateLockPath); return true; } catch { return false; } };',
1575
1588
  'const withStateLock = callback => { fs.mkdirSync(path.dirname(stateLockPath), { recursive: true }); const deadline = Date.now() + 10000; let descriptor = null; let token = ""; while (descriptor === null) { try { descriptor = fs.openSync(stateLockPath, "wx", 0o600); token = process.pid + "-" + Date.now() + "-" + Math.random().toString(16).slice(2); fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, acquiredAt: new Date().toISOString() }), "utf8"); } catch (error) { if (descriptor !== null) { try { fs.closeSync(descriptor); } catch {} descriptor = null; try { fs.rmSync(stateLockPath, { force: true }); } catch {} } if (error?.code !== "EEXIST") throw error; if (removeAbandonedStateLock()) continue; if (Date.now() >= deadline) throw new Error("Timed out waiting for the LiveDesk update state lock: " + stateLockPath); waitForStateLock(Math.min(25, Math.max(1, deadline - Date.now()))); } } try { return callback(); } finally { try { fs.closeSync(descriptor); } catch {} try { const owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true }); } catch {} } };',
1576
1589
  'const writeFailure = (error, cancelRequested = false) => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: cancelRequested || previous.cancelRequested === true, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\\n"); process.exitCode = 1; };',
1577
1590
  'const writeCancellation = error => withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return "superseded"; if (previous.operationId !== operationId) return false; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown") return "ready"; if (previous.stage === "failed") return previous.cancelRequested === true ? "cancelled" : "failed"; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: true, error: String(error?.message || error).slice(0, 4000), cancelledAt: now, failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cancel.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return "cancelled"; });',
1578
- 'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { await new Promise(resolve => { const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); let done = false; const finish = () => { if (done) return; done = true; clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { try { killer.kill("SIGKILL"); } catch {} finish(); }, 10000); killer.once("error", finish); killer.once("exit", finish); }); if (isAlive(pid)) { try { child.kill("SIGKILL"); } catch {} } return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
1591
+ 'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { const root = child.livedeskWindowsIdentity || await child.livedeskWindowsIdentityPromise; if (!root && !child.livedeskWindowsRootExitedAtMs) { child.livedeskWindowsTrackingError = child.livedeskWindowsIdentityError || new Error(`No immutable Windows identity is available for pid=${pid}`); while (!child.livedeskWindowsRootExitedAtMs && !child.livedeskWindowsIdentity) await sleep(250); } await terminateExactWindowsTrackedTree(child, child.livedeskWindowsIdentity || root || null); return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
1579
1592
  'const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });',
1580
- 'const monitorPreflight = child => { let settled = false; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const inspect = () => { const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") finish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); while (!settled) { inspect(); if (settled) return; let outcome; try { outcome = writeCancellation(error); } catch (lockError) { finish(lockError); return; } if (outcome === "ready" || outcome === "failed") { inspect(); if (settled) return; } else if (outcome === "cancelled" || outcome === "superseded") { await terminateTree(child); finish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff(); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (!settled) finish(new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ").")); }); inspect(); };',
1593
+ 'const monitorPreflight = child => { let settled = false; let draining = false; let drainPromise = null; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); stopWindowsTracking(child); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const drainAndFinish = (error, preserveFailure = false) => { if (settled) return Promise.resolve(); draining = true; if (!drainPromise) drainPromise = terminateTree(child).then(() => finish(error, preserveFailure)); return drainPromise; }; const inspect = () => { if (draining) return; const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") void drainAndFinish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); draining = true; while (!settled) { let outcome; try { outcome = writeCancellation(error); } catch (lockError) { await drainAndFinish(lockError); return; } if (outcome === "ready") { draining = false; inspect(); return; } if (outcome === "failed" || outcome === "cancelled" || outcome === "superseded") { await drainAndFinish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff().catch(error => { void drainAndFinish(error); }); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (settled || draining) return; const error = new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ")."); void drainAndFinish(error); }); inspect(); };',
1581
1594
  'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = fs.readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = path.join(neutralCwd, unexpectedEntry); throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); } };',
1582
1595
  'if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();',
1583
1596
  'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_workspaces = "false"; env.npm_config_include_workspace_root = "false";',
@@ -1587,7 +1600,7 @@ function buildClientUpdateBootstrapScript() {
1587
1600
  'const args = ["-y", "--prefer-online", "--prefix", neutralCwd, "--workspaces=false", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
1588
1601
  'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
1589
1602
  'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
1590
- 'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
1603
+ 'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); trackWindowsChild(child); child.once("error", writeFailure); child.once("spawn", () => { trackWindowsChild(child); monitorPreflight(child); }); } catch (error) { writeFailure(error); } }',
1591
1604
  '} catch (error) { writeFailure(error, deadlineExpired()); } }',
1592
1605
  ''
1593
1606
  ].join('\n');
@@ -18,6 +18,7 @@ import {
18
18
  installAgentTerminationHandlers,
19
19
  signalAgentTree
20
20
  } from '../src/runtime/agent-process-lifecycle.js';
21
+ import { writeWindowsOwnedProcessManifest } from '../src/runtime/windows-owned-process-manifest.js';
21
22
  import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
22
23
  import {
23
24
  inspectLinuxVideoAcceleration,
@@ -73,7 +74,69 @@ let linuxVideoAccelerationStatus = null;
73
74
  let discoveryWakeController = new AbortController();
74
75
  let networkChangeMonitor = null;
75
76
  const sessionRefreshesInFlight = new WeakMap();
76
- installAgentTerminationHandlers({ getAgentProcess: () => activeAgentProcess });
77
+ const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
78
+ getAgentProcess: () => activeAgentProcess
79
+ });
80
+
81
+ function readWindowsManifestOwnerFromEnvironment(env = process.env) {
82
+ if (process.platform !== 'win32') return null;
83
+ const rawOwner = {
84
+ pid: String(env.LIVEDESK_RUNTIME_OWNER_PID || '').trim(),
85
+ ownerToken: String(env.LIVEDESK_RUNTIME_OWNER_TOKEN || '').trim(),
86
+ ownerInstanceMarker: String(env.LIVEDESK_RUNTIME_OWNER_INSTANCE_MARKER || '').trim(),
87
+ ownerStartOrder: String(env.LIVEDESK_RUNTIME_OWNER_START_ORDER || '').trim()
88
+ };
89
+ const hasAnyOwnerField = Object.values(rawOwner).some(Boolean);
90
+ if (!hasAnyOwnerField) return null;
91
+ const owner = {
92
+ ...rawOwner,
93
+ pid: Number(rawOwner.pid)
94
+ };
95
+ if (owner.pid !== process.pid
96
+ || !/^[a-f0-9]{32}$/i.test(owner.ownerToken)
97
+ || !owner.ownerInstanceMarker.startsWith(`${process.pid}:`)
98
+ || !/^\d+$/.test(owner.ownerStartOrder)) {
99
+ console.warn(
100
+ '[LiveDesk Client] Windows process-manifest publication is disabled because '
101
+ + 'the unified launcher owner tuple is incomplete or does not name this exact process.'
102
+ );
103
+ return null;
104
+ }
105
+ return owner;
106
+ }
107
+
108
+ const WINDOWS_PROCESS_MANIFEST_OWNER = readWindowsManifestOwnerFromEnvironment();
109
+
110
+ function publishWindowsOwnedAgentRecords(records, {
111
+ rootRecord,
112
+ snapshot
113
+ } = {}) {
114
+ if (!WINDOWS_PROCESS_MANIFEST_OWNER || !rootRecord || records.length === 0) return;
115
+ const ownerRecord = (Array.isArray(snapshot) ? snapshot : []).find(
116
+ record => Number(record?.pid || 0) === WINDOWS_PROCESS_MANIFEST_OWNER.pid
117
+ );
118
+ const exactOwnerGeneration = ownerRecord
119
+ && ownerRecord.startOrder === WINDOWS_PROCESS_MANIFEST_OWNER.ownerStartOrder
120
+ && `${ownerRecord.pid}:${ownerRecord.startMarker}`
121
+ === WINDOWS_PROCESS_MANIFEST_OWNER.ownerInstanceMarker;
122
+ if (!exactOwnerGeneration) {
123
+ throw new Error(
124
+ 'The Windows process snapshot did not confirm the unified launcher owner generation.'
125
+ );
126
+ }
127
+ const publication = writeWindowsOwnedProcessManifest({
128
+ stateDir: UNIFIED_CLIENT_STATE_DIR,
129
+ owner: WINDOWS_PROCESS_MANIFEST_OWNER,
130
+ records
131
+ });
132
+ if (!publication.ok) {
133
+ throw publication.error || new Error('Windows owned-process manifest publication failed.');
134
+ }
135
+ }
136
+
137
+ export function requestClientRuntimeShutdown(signal = 'SIGTERM') {
138
+ disposeAgentTerminationHandlers.requestTermination(signal);
139
+ }
77
140
 
78
141
  function requestActiveAgentStop(signal = 'SIGTERM') {
79
142
  const child = activeAgentProcess;
@@ -3233,8 +3296,8 @@ async function chooseClientConnection(supabase, options = {}) {
3233
3296
  }, 150);
3234
3297
  return { ...result, slotNumber, saved: true, restarting: true };
3235
3298
  },
3236
- onRestart: () => process.kill(process.pid, 'SIGTERM'),
3237
- onShutdown: () => process.kill(process.pid, 'SIGTERM'),
3299
+ onRestart: () => requestClientRuntimeShutdown('SIGTERM'),
3300
+ onShutdown: () => requestClientRuntimeShutdown('SIGTERM'),
3238
3301
  onDisconnect: () => requestActiveAgentStop(),
3239
3302
  // Ending the current Agent causes the unified lifecycle loop to
3240
3303
  // discard its old manager/pair token and start Supabase discovery
@@ -4308,7 +4371,10 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4308
4371
  });
4309
4372
  child.livedeskOwnsProcessGroup = ownsProcessGroup;
4310
4373
  child.livedeskWindowsTreeTracker = createWindowsProcessTreeTracker(child, {
4311
- spawnedAtMs: agentSpawnedAtMs
4374
+ spawnedAtMs: agentSpawnedAtMs,
4375
+ publishTrackedRecords: WINDOWS_PROCESS_MANIFEST_OWNER
4376
+ ? publishWindowsOwnedAgentRecords
4377
+ : null
4312
4378
  });
4313
4379
  activeAgentProcess = child;
4314
4380
  if (typeof onStart === 'function') {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.201",
3
+ "version": "0.1.202",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@livedesk/fast-linux-x64": "0.1.409",
45
- "@livedesk/fast-osx-arm64": "0.1.409",
46
- "@livedesk/fast-osx-x64": "0.1.409",
47
- "@livedesk/fast-win-x64": "0.1.409"
44
+ "@livedesk/fast-linux-x64": "0.1.410",
45
+ "@livedesk/fast-osx-arm64": "0.1.410",
46
+ "@livedesk/fast-osx-x64": "0.1.410",
47
+ "@livedesk/fast-win-x64": "0.1.410"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"