@livedesk/client 0.1.235 → 0.1.237

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/client",
3
- "version": "0.1.235",
3
+ "version": "0.1.237",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,16 +36,16 @@
36
36
  "dependencies": {
37
37
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
38
38
  "ffmpeg-static": "^5.3.0",
39
- "@livedesk/runtime-core": "0.1.4",
39
+ "@livedesk/runtime-core": "0.1.5",
40
40
  "@supabase/supabase-js": "^2.110.0",
41
41
  "node-screenshots": "^0.2.8",
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.433",
46
- "@livedesk/fast-osx-arm64": "0.1.433",
47
- "@livedesk/fast-osx-x64": "0.1.433",
48
- "@livedesk/fast-win-x64": "0.1.433"
45
+ "@livedesk/fast-linux-x64": "0.1.435",
46
+ "@livedesk/fast-osx-arm64": "0.1.435",
47
+ "@livedesk/fast-osx-x64": "0.1.435",
48
+ "@livedesk/fast-win-x64": "0.1.435"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -5,7 +5,6 @@ import { accessSync, chmodSync, constants as fsConstants, existsSync, mkdirSync,
5
5
  import { delimiter, dirname, join, parse, posix, resolve } from 'node:path';
6
6
  import os from 'node:os';
7
7
  import { createRuntimeManager, normalizeRuntimeAuthSession, runtimeRoleError, RuntimeState } from '../../../runtime-core/src/index.js';
8
- import { createOsSecretStore, OS_SECRET_REFERENCE } from '../../../runtime-core/src/os-secret-store.js';
9
8
 
10
9
  const DEFAULT_HOST = '127.0.0.1';
11
10
  const DEFAULT_PORT = 5179;
@@ -21,11 +20,6 @@ const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngv
21
20
  const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
22
21
  const CLIENT_STATE_DIR = process.env.LIVEDESK_STATE_DIR || (process.env.LIVEDESK_UNIFIED_RUNTIME === '1' ? join(os.homedir(), '.livedesk') : join(os.homedir(), '.livedesk-client'));
23
22
  const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
24
- const CLIENT_REFRESH_SECRET_STORE = createOsSecretStore({
25
- service: 'LiveDesk',
26
- account: 'client-refresh-token',
27
- dataDir: CLIENT_STATE_DIR
28
- });
29
23
  const DEFAULT_TRUSTED_WEB_ORIGINS = Object.freeze([
30
24
  'https://livedesk.pages.dev',
31
25
  'http://127.0.0.1:5173',
@@ -957,34 +951,12 @@ function readSavedSession() {
957
951
  try {
958
952
  const raw = JSON.parse(readFileSync(CLIENT_AUTH_PATH, 'utf8'))?.[CLIENT_AUTH_STORAGE_KEY];
959
953
  const session = typeof raw === 'string' ? JSON.parse(raw) : null;
960
- if (!session?.access_token) return null;
961
- const plaintextRefreshToken = normalizeString(session.refresh_token, 8192);
962
- const refreshToken = plaintextRefreshToken || (session.refresh_token_ref === OS_SECRET_REFERENCE
963
- ? CLIENT_REFRESH_SECRET_STORE.read()
964
- : '');
965
- if (!refreshToken) return null;
966
- if (plaintextRefreshToken) {
967
- if (!CLIENT_REFRESH_SECRET_STORE.write(plaintextRefreshToken)) {
968
- writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
969
- return null;
970
- }
971
- const migrated = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
972
- delete migrated.refresh_token;
973
- writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(migrated) });
974
- }
975
- return { ...session, refresh_token: refreshToken };
954
+ return session?.access_token ? session : null;
976
955
  } catch {
977
956
  return null;
978
957
  }
979
958
  }
980
959
 
981
- function resolveSavedSessionSecret(session) {
982
- if (!session || typeof session !== 'object') return null;
983
- const refreshToken = normalizeString(session.refresh_token, 8192)
984
- || (session.refresh_token_ref === OS_SECRET_REFERENCE ? CLIENT_REFRESH_SECRET_STORE.read() : '');
985
- return session.access_token && refreshToken ? { ...session, refresh_token: refreshToken } : null;
986
- }
987
-
988
960
  function writePrivateJsonAtomic(path, value) {
989
961
  mkdirSync(dirname(path), { recursive: true });
990
962
  const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
@@ -1002,46 +974,14 @@ function writePrivateJsonAtomic(path, value) {
1002
974
  function writeSavedSession(session) {
1003
975
  if (!session?.access_token || !session?.refresh_token) return false;
1004
976
  if (process.env.LIVEDESK_DESKTOP_HOST === '1') return true;
1005
- if (!CLIENT_REFRESH_SECRET_STORE.write(session.refresh_token)) return false;
1006
- const persisted = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
1007
- delete persisted.refresh_token;
1008
- writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(persisted) });
977
+ writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(session) });
1009
978
  return true;
1010
979
  }
1011
980
 
1012
981
  function clearSavedSession() {
1013
- CLIENT_REFRESH_SECRET_STORE.clear();
1014
982
  writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
1015
983
  }
1016
984
 
1017
- export async function revokeClientProviderSession(session, options = {}) {
1018
- const accessToken = normalizeString(session?.access_token, 8192);
1019
- if (!accessToken) return { ok: true, skipped: true, reason: 'no-access-token' };
1020
- try {
1021
- if (typeof options.revokeProviderSession === 'function') {
1022
- const result = await options.revokeProviderSession(session);
1023
- return result && typeof result === 'object' ? result : { ok: result !== false };
1024
- }
1025
- const fetchImpl = typeof options.fetchImpl === 'function' ? options.fetchImpl : fetch;
1026
- const response = await fetchImpl(`${SUPABASE_URL.replace(/\/+$/, '')}/auth/v1/logout?scope=local`, {
1027
- method: 'POST',
1028
- headers: {
1029
- apikey: SUPABASE_PUBLISHABLE_KEY,
1030
- Authorization: `Bearer ${accessToken}`,
1031
- Accept: 'application/json'
1032
- },
1033
- signal: AbortSignal.timeout(10_000)
1034
- });
1035
- if (response.ok) return { ok: true, revoked: true };
1036
- if ([401, 403, 404].includes(response.status)) {
1037
- return { ok: true, alreadyInvalid: true, status: response.status };
1038
- }
1039
- return { ok: false, error: `provider-session-revoke-failed:${response.status}` };
1040
- } catch (error) {
1041
- return { ok: false, error: normalizeString(error?.message || error, 240) || 'provider-session-revoke-failed' };
1042
- }
1043
- }
1044
-
1045
985
  function readBody(req) {
1046
986
  return new Promise((resolveBody, reject) => {
1047
987
  let body = '';
@@ -1176,17 +1116,13 @@ export function createClientRuntimeServer(options = {}) {
1176
1116
  const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
1177
1117
  const deviceName = normalizeString(options.deviceName || os.hostname(), 160) || os.hostname();
1178
1118
  const appVersion = normalizeString(options.appVersion || process.env.LIVEDESK_NPM_LAUNCHER_VERSION, 80) || 'dev';
1179
- const providedSavedSession = resolveSavedSessionSecret(options.savedSession);
1180
1119
  const savedSession = options.loadSavedSession === false
1181
1120
  ? null
1182
- : providedSavedSession
1183
- ? providedSavedSession
1121
+ : options.savedSession?.refresh_token
1122
+ ? options.savedSession
1184
1123
  : readSavedSession();
1185
1124
  const savedSessionPersisted = Boolean(savedSession?.refresh_token);
1186
1125
  const savedAccountProfile = readClientAccountProfile(savedSession);
1187
- const hydrateAccountProfile = typeof options.hydrateAccountProfile === 'function'
1188
- ? options.hydrateAccountProfile
1189
- : hydrateClientAccountProfile;
1190
1126
  const diagnosticPlatform = normalizeString(options.diagnosticPlatform || process.platform, 20);
1191
1127
  const diagnosticMonotonicNow = typeof options.diagnosticMonotonicNow === 'function'
1192
1128
  ? options.diagnosticMonotonicNow
@@ -1701,11 +1637,6 @@ export function createClientRuntimeServer(options = {}) {
1701
1637
  return;
1702
1638
  }
1703
1639
  if (pathname === '/api/auth/session' && req.method === 'DELETE') {
1704
- const activeSession = lastChoice?.session || savedSession || readSavedSession();
1705
- const providerLogout = await revokeClientProviderSession(activeSession, {
1706
- revokeProviderSession: options.revokeProviderSession,
1707
- fetchImpl: options.fetchImpl
1708
- });
1709
1640
  clearSavedSession();
1710
1641
  loggedOut = true;
1711
1642
  completed = false;
@@ -1723,13 +1654,7 @@ export function createClientRuntimeServer(options = {}) {
1723
1654
  avatarUrl: ''
1724
1655
  };
1725
1656
  runtime.setAuthenticated(false);
1726
- respondJson(providerLogout.ok ? 200 : 502, {
1727
- ok: providerLogout.ok,
1728
- authenticated: false,
1729
- localCleared: true,
1730
- role: 'client',
1731
- providerLogout
1732
- });
1657
+ respondJson(200, { ok: true, authenticated: false, role: 'client' });
1733
1658
  return;
1734
1659
  }
1735
1660
  if (pathname === '/api/runtime/restart' && req.method === 'POST') {
@@ -1859,11 +1784,7 @@ export function createClientRuntimeServer(options = {}) {
1859
1784
  return;
1860
1785
  }
1861
1786
  try {
1862
- const result = await options.changeRole?.(
1863
- 'hub',
1864
- runtime.getSnapshot(),
1865
- lastChoice?.session || savedSession || null
1866
- );
1787
+ const result = await options.changeRole?.('hub', runtime.getSnapshot());
1867
1788
  respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
1868
1789
  } catch (error) {
1869
1790
  respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
@@ -1877,11 +1798,7 @@ export function createClientRuntimeServer(options = {}) {
1877
1798
  return;
1878
1799
  }
1879
1800
  try {
1880
- const result = await options.changeRole?.(
1881
- 'hub',
1882
- runtime.getSnapshot(),
1883
- lastChoice?.session || savedSession || null
1884
- );
1801
+ const result = await options.changeRole?.('hub', runtime.getSnapshot());
1885
1802
  respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
1886
1803
  } catch (error) {
1887
1804
  respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
@@ -1971,32 +1888,21 @@ export function createClientRuntimeServer(options = {}) {
1971
1888
  const saved = normalizeRuntimeAuthSession(savedSession, { requireRefreshToken: true });
1972
1889
  if (saved.ok) {
1973
1890
  setImmediate(() => {
1974
- // Hub discovery must not wait for a display-only user-profile
1975
- // request. The launcher already refreshed and installed this
1976
- // DPAPI/keychain-backed session before starting the runtime.
1977
- const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
1978
- complete(
1979
- { type: 'google', session: normalizedSession },
1980
- 'Saved sign-in restored. Finding the LiveDesk Hub.'
1981
- );
1982
- void trackDiagnosticTask(async () => {
1891
+ void (async () => {
1983
1892
  try {
1984
- const hydratedSession = await hydrateAccountProfile(normalizedSession);
1985
- if (closed || !hydratedSession?.access_token) return;
1986
- if (writeSavedSession(hydratedSession)) {
1987
- state.auth.persisted = true;
1988
- }
1989
- // complete() updates profile fields after startup without
1990
- // restarting discovery or opening another OAuth flow.
1991
- complete(
1992
- { type: 'google', session: hydratedSession },
1993
- 'Saved sign-in restored. Finding the LiveDesk Hub.'
1994
- );
1995
- } catch {
1996
- // Name/avatar hydration is optional. A provider timeout must
1997
- // never invalidate the local session or interrupt remote work.
1893
+ // Older persisted sessions kept only id/email on `user`, while
1894
+ // Supabase still carries Google name/avatar claims in the
1895
+ // access-token payload. Recover those display-only claims
1896
+ // locally before any best-effort network hydration.
1897
+ const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
1898
+ const hydratedSession = await hydrateClientAccountProfile(normalizedSession);
1899
+ if (!writeSavedSession(hydratedSession)) throw new Error('refresh-token-required');
1900
+ state.auth.persisted = true;
1901
+ complete({ type: 'google', session: hydratedSession }, 'Saved sign-in found. Finding the LiveDesk Hub.');
1902
+ } catch (error) {
1903
+ recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
1998
1904
  }
1999
- });
1905
+ })();
2000
1906
  });
2001
1907
  }
2002
1908
  }
@@ -1,300 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { spawn } from 'node:child_process';
3
- import {
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- rmSync,
9
- statSync,
10
- writeFileSync
11
- } from 'node:fs';
12
- import { dirname, join, resolve } from 'node:path';
13
-
14
- const DEFAULT_TIMEOUT_MS = 120_000;
15
- const LOCK_POLL_MS = 200;
16
- const LOCK_STALE_MS = 10 * 60_000;
17
- const MAX_OUTPUT_CHARS = 64 * 1024;
18
-
19
- const PLATFORM_SPECS = Object.freeze({
20
- 'win32:x64': Object.freeze({
21
- packageName: '@livedesk/fast-win-x64',
22
- rid: 'win-x64',
23
- executableName: 'livedesk-client-fast.exe'
24
- }),
25
- 'linux:x64': Object.freeze({
26
- packageName: '@livedesk/fast-linux-x64',
27
- rid: 'linux-x64',
28
- executableName: 'livedesk-client-fast'
29
- }),
30
- 'darwin:x64': Object.freeze({
31
- packageName: '@livedesk/fast-osx-x64',
32
- rid: 'osx-x64',
33
- executableName: 'livedesk-client-fast'
34
- }),
35
- 'darwin:arm64': Object.freeze({
36
- packageName: '@livedesk/fast-osx-arm64',
37
- rid: 'osx-arm64',
38
- executableName: 'livedesk-client-fast'
39
- })
40
- });
41
-
42
- function exactVersion(value) {
43
- const version = String(value || '').trim();
44
- return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
45
- ? version
46
- : '';
47
- }
48
-
49
- function boundedText(previous, chunk) {
50
- const next = `${previous}${String(chunk || '')}`;
51
- return next.length <= MAX_OUTPUT_CHARS ? next : next.slice(next.length - MAX_OUTPUT_CHARS);
52
- }
53
-
54
- function delay(milliseconds) {
55
- return new Promise(resolveDelay => setTimeout(resolveDelay, milliseconds));
56
- }
57
-
58
- function packageDirectory(installRoot, packageName) {
59
- return join(installRoot, 'node_modules', ...String(packageName).split('/'));
60
- }
61
-
62
- function safeSegment(value) {
63
- return String(value || '').replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160) || 'unknown';
64
- }
65
-
66
- function readJson(filePath) {
67
- try {
68
- return JSON.parse(readFileSync(filePath, 'utf8'));
69
- } catch {
70
- return null;
71
- }
72
- }
73
-
74
- function isPidAlive(value) {
75
- const pid = Number(value);
76
- if (!Number.isInteger(pid) || pid <= 1) return false;
77
- try {
78
- process.kill(pid, 0);
79
- return true;
80
- } catch (error) {
81
- return error?.code === 'EPERM';
82
- }
83
- }
84
-
85
- function reclaimStaleLock(lockPath) {
86
- try {
87
- const owner = readJson(join(lockPath, 'owner.json'));
88
- const ageMs = Math.max(0, Date.now() - Number(statSync(lockPath).mtimeMs || 0));
89
- const ownerAlive = isPidAlive(owner?.pid);
90
- if ((owner?.pid && !ownerAlive && ageMs >= 1_000) || ageMs >= LOCK_STALE_MS) {
91
- rmSync(lockPath, { recursive: true, force: true });
92
- return true;
93
- }
94
- } catch {
95
- // A concurrent owner may be creating or removing the lock.
96
- }
97
- return false;
98
- }
99
-
100
- export function resolveFastPlatformSpec({
101
- platform = process.platform,
102
- arch = process.arch,
103
- manifest = null
104
- } = {}) {
105
- const base = PLATFORM_SPECS[`${platform}:${arch}`];
106
- if (!base) return null;
107
- const version = exactVersion(manifest?.optionalDependencies?.[base.packageName]);
108
- if (!version) return null;
109
- return Object.freeze({ ...base, version });
110
- }
111
-
112
- export function inspectFastRuntimePackage(packageRoot, spec) {
113
- if (!packageRoot || !spec) return null;
114
- const manifest = readJson(join(packageRoot, 'package.json'));
115
- if (manifest?.name !== spec.packageName || manifest?.version !== spec.version) return null;
116
- if (Array.isArray(manifest.os) && !manifest.os.includes(process.platform)) return null;
117
- if (Array.isArray(manifest.cpu) && !manifest.cpu.includes(process.arch)) return null;
118
- const fastRoot = join(packageRoot, 'fast');
119
- const executable = join(fastRoot, spec.executableName);
120
- const dll = join(fastRoot, 'livedesk-client-fast.dll');
121
- if (!existsSync(executable) && !existsSync(dll)) return null;
122
- return Object.freeze({
123
- rid: spec.rid,
124
- packageName: spec.packageName,
125
- packageVersion: spec.version,
126
- packageRoot,
127
- executable,
128
- dll
129
- });
130
- }
131
-
132
- export function repairedFastInstallRoot(stateDir, spec) {
133
- return join(
134
- resolve(stateDir),
135
- 'runtime-packages',
136
- `${safeSegment(spec.packageName)}-${safeSegment(spec.version)}`
137
- );
138
- }
139
-
140
- export function inspectRepairedFastRuntime(stateDir, spec) {
141
- const installRoot = repairedFastInstallRoot(stateDir, spec);
142
- return inspectFastRuntimePackage(packageDirectory(installRoot, spec.packageName), spec);
143
- }
144
-
145
- function resolveNpmInvocation({ nodeExecutable, npmExecPath, npmExecutable }) {
146
- const exactNode = resolve(String(nodeExecutable || process.execPath));
147
- const cli = String(npmExecPath || '').trim();
148
- const cliCandidates = cli
149
- ? (/npx-cli\.js$/i.test(cli) ? [join(dirname(cli), 'npm-cli.js')] : [cli])
150
- : [];
151
- const npmCli = cliCandidates.find(candidate => existsSync(candidate));
152
- if (npmCli) {
153
- return { command: exactNode, argsPrefix: [resolve(npmCli)] };
154
- }
155
- const executable = String(npmExecutable || '').trim();
156
- if (executable) return { command: executable, argsPrefix: [] };
157
- const nodeDir = dirname(exactNode);
158
- const candidates = process.platform === 'win32'
159
- ? [join(nodeDir, 'npm.cmd'), join(nodeDir, 'npm.exe')]
160
- : [join(nodeDir, 'npm')];
161
- const fallback = candidates.find(existsSync);
162
- return fallback ? { command: fallback, argsPrefix: [] } : null;
163
- }
164
-
165
- function runInstall({ invocation, installRoot, spec, timeoutMs, env }) {
166
- return new Promise((resolveInstall, rejectInstall) => {
167
- const args = [
168
- ...invocation.argsPrefix,
169
- 'install',
170
- '--ignore-scripts',
171
- '--no-audit',
172
- '--no-fund',
173
- '--package-lock=false',
174
- '--save=false',
175
- '--omit=dev',
176
- '--prefix', installRoot,
177
- `${spec.packageName}@${spec.version}`
178
- ];
179
- const child = spawn(invocation.command, args, {
180
- env: {
181
- ...env,
182
- npm_config_audit: 'false',
183
- npm_config_fund: 'false',
184
- npm_config_ignore_scripts: 'true',
185
- npm_config_package_lock: 'false'
186
- },
187
- windowsHide: true,
188
- stdio: ['ignore', 'pipe', 'pipe']
189
- });
190
- let stdout = '';
191
- let stderr = '';
192
- let settled = false;
193
- let timer = null;
194
- const finish = (error, result = null) => {
195
- if (settled) return;
196
- settled = true;
197
- if (timer) clearTimeout(timer);
198
- if (error) rejectInstall(error);
199
- else resolveInstall(result);
200
- };
201
- child.stdout?.on('data', chunk => { stdout = boundedText(stdout, chunk); });
202
- child.stderr?.on('data', chunk => { stderr = boundedText(stderr, chunk); });
203
- child.once('error', error => finish(error));
204
- child.once('exit', (code, signal) => finish(null, {
205
- code: Number.isInteger(code) ? code : 1,
206
- signal: String(signal || ''),
207
- stdout,
208
- stderr
209
- }));
210
- timer = setTimeout(() => {
211
- try { child.kill('SIGKILL'); } catch { }
212
- finish(new Error(`remote-fast-repair-timeout:${timeoutMs}`));
213
- }, timeoutMs);
214
- timer.unref?.();
215
- });
216
- }
217
-
218
- export async function ensureRepairedFastRuntime({
219
- stateDir,
220
- spec,
221
- nodeExecutable = process.execPath,
222
- npmExecPath = process.env.npm_execpath || process.env.NPM_EXECPATH,
223
- npmExecutable = process.env.LIVEDESK_NPM_EXECUTABLE,
224
- timeoutMs = DEFAULT_TIMEOUT_MS,
225
- env = process.env,
226
- installRunner = runInstall
227
- } = {}) {
228
- if (!stateDir || !spec) throw new Error('remote-fast-repair-plan-required');
229
- const alreadyRepaired = inspectRepairedFastRuntime(stateDir, spec);
230
- if (alreadyRepaired) return alreadyRepaired;
231
-
232
- const installRoot = repairedFastInstallRoot(stateDir, spec);
233
- mkdirSync(dirname(installRoot), { recursive: true });
234
- const lockPath = `${installRoot}.lock`;
235
- const deadline = Date.now() + Math.max(10_000, Math.min(300_000, Number(timeoutMs) || DEFAULT_TIMEOUT_MS));
236
- let ownsLock = false;
237
- while (!ownsLock && Date.now() < deadline) {
238
- try {
239
- mkdirSync(lockPath, { recursive: false });
240
- writeFileSync(join(lockPath, 'owner.json'), JSON.stringify({
241
- pid: process.pid,
242
- createdAt: new Date().toISOString(),
243
- packageName: spec.packageName,
244
- version: spec.version
245
- }), { encoding: 'utf8', mode: 0o600 });
246
- ownsLock = true;
247
- } catch {
248
- const concurrent = inspectRepairedFastRuntime(stateDir, spec);
249
- if (concurrent) return concurrent;
250
- if (!reclaimStaleLock(lockPath)) await delay(LOCK_POLL_MS);
251
- }
252
- }
253
- if (!ownsLock) throw new Error('remote-fast-repair-lock-timeout');
254
-
255
- const temporaryRoot = `${installRoot}.tmp-${process.pid}-${randomBytes(6).toString('hex')}`;
256
- try {
257
- const afterLock = inspectRepairedFastRuntime(stateDir, spec);
258
- if (afterLock) return afterLock;
259
- rmSync(temporaryRoot, { recursive: true, force: true });
260
- mkdirSync(temporaryRoot, { recursive: true });
261
- writeFileSync(join(temporaryRoot, 'package.json'), JSON.stringify({
262
- private: true,
263
- name: 'livedesk-fast-runtime-repair',
264
- version: '0.0.0'
265
- }), { encoding: 'utf8', mode: 0o600 });
266
-
267
- const invocation = resolveNpmInvocation({ nodeExecutable, npmExecPath, npmExecutable });
268
- if (!invocation && installRunner === runInstall) throw new Error('remote-fast-repair-npm-unavailable');
269
- const remainingMs = Math.max(1_000, deadline - Date.now());
270
- const result = await installRunner({
271
- invocation,
272
- installRoot: temporaryRoot,
273
- spec,
274
- timeoutMs: remainingMs,
275
- env
276
- });
277
- if (result?.code !== 0) {
278
- const detail = String(result?.stderr || result?.stdout || `exit-${result?.code ?? 'unknown'}`)
279
- .replace(/\s+/g, ' ')
280
- .trim()
281
- .slice(0, 600);
282
- throw new Error(`remote-fast-repair-install-failed:${detail || 'unknown'}`);
283
- }
284
- const temporaryRuntime = inspectFastRuntimePackage(
285
- packageDirectory(temporaryRoot, spec.packageName),
286
- spec
287
- );
288
- if (!temporaryRuntime) throw new Error('remote-fast-repair-package-invalid');
289
-
290
- rmSync(installRoot, { recursive: true, force: true });
291
- mkdirSync(dirname(installRoot), { recursive: true });
292
- renameSync(temporaryRoot, installRoot);
293
- const committed = inspectRepairedFastRuntime(stateDir, spec);
294
- if (!committed) throw new Error('remote-fast-repair-commit-invalid');
295
- return committed;
296
- } finally {
297
- rmSync(temporaryRoot, { recursive: true, force: true });
298
- rmSync(lockPath, { recursive: true, force: true });
299
- }
300
- }