@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/bin/livedesk-client-node.js +82 -190
- package/bin/livedesk-client-update-bootstrap.cjs +316 -56
- package/bin/livedesk-client.js +111 -482
- package/package.json +6 -6
- package/src/runtime/client-runtime-server.js +20 -114
- package/src/runtime/fast-runtime-repair.js +0 -300
- package/src/security/device-credential-store.js +0 -219
- package/src/security/secure-direct-client.js +0 -224
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
46
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
47
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
48
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
1183
|
-
?
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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
|
-
}
|