@livedesk/client 0.1.235 → 0.1.236
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.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/bin/livedesk-client.js
CHANGED
|
@@ -20,20 +20,11 @@ import {
|
|
|
20
20
|
} from '../src/runtime/agent-process-lifecycle.js';
|
|
21
21
|
import { writeWindowsOwnedProcessManifest } from '../src/runtime/windows-owned-process-manifest.js';
|
|
22
22
|
import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
|
|
23
|
-
import { createClientDeviceCredentialStore } from '../src/security/device-credential-store.js';
|
|
24
|
-
import { connectSecureDirect } from '../src/security/secure-direct-client.js';
|
|
25
23
|
import {
|
|
26
24
|
inspectLinuxVideoAcceleration,
|
|
27
25
|
installLinuxVideoAcceleration
|
|
28
26
|
} from '../src/runtime/linux-video-acceleration.js';
|
|
29
|
-
import {
|
|
30
|
-
ensureRepairedFastRuntime,
|
|
31
|
-
inspectFastRuntimePackage,
|
|
32
|
-
inspectRepairedFastRuntime,
|
|
33
|
-
resolveFastPlatformSpec
|
|
34
|
-
} from '../src/runtime/fast-runtime-repair.js';
|
|
35
27
|
import { normalizeRuntimeAuthSession } from '../../runtime-core/src/auth-session.js';
|
|
36
|
-
import { createOsSecretStore, OS_SECRET_REFERENCE } from '../../runtime-core/src/os-secret-store.js';
|
|
37
28
|
import { startRoleTransitionSupervisor } from '../../runtime-core/src/role-transition-supervisor.js';
|
|
38
29
|
|
|
39
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -75,11 +66,6 @@ const UNIFIED_CLIENT_AUTH_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'auth.json');
|
|
|
75
66
|
const UNIFIED_CLIENT_PIN_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'pin.json');
|
|
76
67
|
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
77
68
|
const CLIENT_HUB_TARGET_CACHE_STORAGE_KEY = 'livedesk.client.last-hub-target';
|
|
78
|
-
const CLIENT_REFRESH_SECRET_STORE = createOsSecretStore({
|
|
79
|
-
service: 'LiveDesk',
|
|
80
|
-
account: 'client-refresh-token',
|
|
81
|
-
dataDir: UNIFIED_CLIENT_STATE_DIR
|
|
82
|
-
});
|
|
83
69
|
const DEVICE_ROLE_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-role.json');
|
|
84
70
|
const FAST_PREFLIGHT_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'fast-preflight.json');
|
|
85
71
|
const CLIENT_SLOT_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-slot.json');
|
|
@@ -96,7 +82,6 @@ let agentRestartRequest = null;
|
|
|
96
82
|
let linuxVideoAccelerationStatus = null;
|
|
97
83
|
let discoveryWakeController = new AbortController();
|
|
98
84
|
let networkChangeMonitor = null;
|
|
99
|
-
let deviceRoleMutationTail = Promise.resolve();
|
|
100
85
|
const sessionRefreshesInFlight = new WeakMap();
|
|
101
86
|
const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
|
|
102
87
|
getAgentProcess: () => activeAgentProcess
|
|
@@ -184,17 +169,6 @@ export function requestLocalDiscoveryWake(reason = 'local-trigger') {
|
|
|
184
169
|
previous.abort(String(reason || 'local-trigger'));
|
|
185
170
|
}
|
|
186
171
|
|
|
187
|
-
export function enqueueDeviceRoleMutation(operation) {
|
|
188
|
-
if (typeof operation !== 'function') {
|
|
189
|
-
return Promise.reject(new TypeError('device-role-mutation-operation-required'));
|
|
190
|
-
}
|
|
191
|
-
const pending = deviceRoleMutationTail
|
|
192
|
-
.catch(() => undefined)
|
|
193
|
-
.then(operation);
|
|
194
|
-
deviceRoleMutationTail = pending.catch(() => undefined);
|
|
195
|
-
return pending;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
172
|
function networkInterfaceSignature() {
|
|
199
173
|
return Object.entries(os.networkInterfaces())
|
|
200
174
|
.flatMap(([name, entries]) => (entries || [])
|
|
@@ -336,8 +310,8 @@ Options:
|
|
|
336
310
|
--version Show the agent version.
|
|
337
311
|
--help Show this help.
|
|
338
312
|
|
|
339
|
-
Auto uses
|
|
340
|
-
|
|
313
|
+
Auto uses C# RemoteFast when supported and falls back to Node only when a
|
|
314
|
+
packaged RemoteFast runtime is unavailable.
|
|
341
315
|
Enable Windows auto-start from the connection page when this client signs in.
|
|
342
316
|
`.trimStart());
|
|
343
317
|
}
|
|
@@ -346,14 +320,6 @@ function readPackageVersion() {
|
|
|
346
320
|
return readClientPackageVersion();
|
|
347
321
|
}
|
|
348
322
|
|
|
349
|
-
function readClientPackageManifest() {
|
|
350
|
-
try {
|
|
351
|
-
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
352
|
-
} catch {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
323
|
function isTruthy(value) {
|
|
358
324
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
359
325
|
}
|
|
@@ -1088,20 +1054,7 @@ function readSavedSessionFromFile() {
|
|
|
1088
1054
|
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
1089
1055
|
if (typeof raw !== 'string' || !raw.trim()) continue;
|
|
1090
1056
|
const session = JSON.parse(raw);
|
|
1091
|
-
const
|
|
1092
|
-
const refreshToken = plaintextRefreshToken || (session?.refresh_token_ref === OS_SECRET_REFERENCE
|
|
1093
|
-
? CLIENT_REFRESH_SECRET_STORE.read()
|
|
1094
|
-
: '');
|
|
1095
|
-
if (plaintextRefreshToken) {
|
|
1096
|
-
if (!CLIENT_REFRESH_SECRET_STORE.write(plaintextRefreshToken)) {
|
|
1097
|
-
createFileStorage(path).removeItem(CLIENT_AUTH_STORAGE_KEY);
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
const migrated = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
1101
|
-
delete migrated.refresh_token;
|
|
1102
|
-
createFileStorage(path).setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify(migrated));
|
|
1103
|
-
}
|
|
1104
|
-
const normalized = normalizeRuntimeAuthSession({ ...session, refresh_token: refreshToken }, { requireRefreshToken: true });
|
|
1057
|
+
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1105
1058
|
if (normalized.ok) return { ...session, ...normalized.session };
|
|
1106
1059
|
} catch {
|
|
1107
1060
|
// Try the next compatible state location.
|
|
@@ -1115,16 +1068,12 @@ function writeSavedSessionToFile(session) {
|
|
|
1115
1068
|
if (!normalized.ok) {
|
|
1116
1069
|
return false;
|
|
1117
1070
|
}
|
|
1118
|
-
if (!CLIENT_REFRESH_SECRET_STORE.write(normalized.session.refresh_token)) return false;
|
|
1119
1071
|
const storage = createFileStorage(preferredClientAuthPath());
|
|
1120
|
-
|
|
1121
|
-
delete persisted.refresh_token;
|
|
1122
|
-
storage.setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify(persisted));
|
|
1072
|
+
storage.setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify({ ...session, ...normalized.session }));
|
|
1123
1073
|
return true;
|
|
1124
1074
|
}
|
|
1125
1075
|
|
|
1126
1076
|
function clearSavedSession() {
|
|
1127
|
-
CLIENT_REFRESH_SECRET_STORE.clear();
|
|
1128
1077
|
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
1129
1078
|
rmSync(UNIFIED_CLIENT_AUTH_PATH, { force: true });
|
|
1130
1079
|
}
|
|
@@ -1168,99 +1117,9 @@ export async function fetchSupabaseWithDeadline(
|
|
|
1168
1117
|
}
|
|
1169
1118
|
}
|
|
1170
1119
|
|
|
1171
|
-
const LIVEDESK_AUTHENTICATED_FETCH = '__liveDeskAuthenticatedFetch';
|
|
1172
|
-
|
|
1173
|
-
function decodeSupabaseJwtRole(accessToken) {
|
|
1174
|
-
try {
|
|
1175
|
-
const parts = String(accessToken || '').split('.');
|
|
1176
|
-
if (parts.length !== 3) return '';
|
|
1177
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
1178
|
-
return String(payload?.role || '').trim().toLowerCase();
|
|
1179
|
-
} catch {
|
|
1180
|
-
return '';
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function authenticatedSupabaseError(response, payload, fallback) {
|
|
1185
|
-
const message = String(
|
|
1186
|
-
payload?.message
|
|
1187
|
-
|| payload?.error_description
|
|
1188
|
-
|| payload?.error
|
|
1189
|
-
|| payload?.details
|
|
1190
|
-
|| fallback
|
|
1191
|
-
|| `Supabase request failed with HTTP ${response.status}.`
|
|
1192
|
-
).trim();
|
|
1193
|
-
const error = new Error(message);
|
|
1194
|
-
error.code = String(payload?.code || '').trim();
|
|
1195
|
-
error.status = Number(response.status || 0);
|
|
1196
|
-
error.details = String(payload?.details || '').trim();
|
|
1197
|
-
error.hint = String(payload?.hint || '').trim();
|
|
1198
|
-
return error;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
export async function fetchAuthenticatedSupabaseJson(session, pathname, options = {}) {
|
|
1202
|
-
const normalized = normalizeRuntimeAuthSession(session);
|
|
1203
|
-
if (!normalized.ok) throw new Error(normalized.error);
|
|
1204
|
-
const role = decodeSupabaseJwtRole(normalized.session.access_token);
|
|
1205
|
-
if (role && role !== 'authenticated') {
|
|
1206
|
-
throw new Error(`LiveDesk sign-in token has unexpected database role ${role}. Sign out and sign in again.`);
|
|
1207
|
-
}
|
|
1208
|
-
const cleanPath = String(pathname || '').replace(/^\/+/, '');
|
|
1209
|
-
if (!cleanPath || cleanPath.includes('..')) throw new Error('invalid-supabase-rest-path');
|
|
1210
|
-
const url = new URL(`${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/${cleanPath}`);
|
|
1211
|
-
for (const [key, value] of Object.entries(options.query || {})) {
|
|
1212
|
-
if (value === undefined || value === null || value === '') continue;
|
|
1213
|
-
url.searchParams.set(key, String(value));
|
|
1214
|
-
}
|
|
1215
|
-
const headers = new Headers(options.headers || {});
|
|
1216
|
-
headers.set('apikey', SUPABASE_PUBLISHABLE_KEY);
|
|
1217
|
-
headers.set('Authorization', `Bearer ${normalized.session.access_token}`);
|
|
1218
|
-
headers.set('Accept', 'application/json');
|
|
1219
|
-
const method = String(options.method || 'GET').toUpperCase();
|
|
1220
|
-
let body;
|
|
1221
|
-
if (options.body !== undefined) {
|
|
1222
|
-
headers.set('Content-Type', 'application/json');
|
|
1223
|
-
body = JSON.stringify(options.body);
|
|
1224
|
-
}
|
|
1225
|
-
const fetchImpl = typeof options.fetchImpl === 'function'
|
|
1226
|
-
? options.fetchImpl
|
|
1227
|
-
: fetchSupabaseWithDeadline;
|
|
1228
|
-
const response = await fetchImpl(url, { method, headers, body, signal: options.signal });
|
|
1229
|
-
const text = await response.text();
|
|
1230
|
-
let payload = null;
|
|
1231
|
-
if (text) {
|
|
1232
|
-
try { payload = JSON.parse(text); }
|
|
1233
|
-
catch { payload = { message: text }; }
|
|
1234
|
-
}
|
|
1235
|
-
if (!response.ok) throw authenticatedSupabaseError(response, payload, options.errorMessage);
|
|
1236
|
-
return payload;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
function explicitAuthenticatedFetch(supabase, override = null) {
|
|
1240
|
-
if (typeof override === 'function') return override;
|
|
1241
|
-
return typeof supabase?.[LIVEDESK_AUTHENTICATED_FETCH] === 'function'
|
|
1242
|
-
? supabase[LIVEDESK_AUTHENTICATED_FETCH]
|
|
1243
|
-
: null;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
export async function callVerifiedSupabaseRpc(supabase, session, functionName, args = {}, options = {}) {
|
|
1247
|
-
const name = String(functionName || '').trim();
|
|
1248
|
-
if (!/^[a-z0-9_]+$/u.test(name)) throw new Error('invalid-supabase-rpc-name');
|
|
1249
|
-
const fetchImpl = explicitAuthenticatedFetch(supabase, options.fetchImpl);
|
|
1250
|
-
if (!fetchImpl) return supabase.rpc(name, args);
|
|
1251
|
-
const data = await fetchAuthenticatedSupabaseJson(session, `rpc/${name}`, {
|
|
1252
|
-
method: 'POST',
|
|
1253
|
-
body: args,
|
|
1254
|
-
fetchImpl,
|
|
1255
|
-
signal: options.signal,
|
|
1256
|
-
errorMessage: `${name} failed.`
|
|
1257
|
-
});
|
|
1258
|
-
return { data, error: null };
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
1120
|
async function createSupabaseClient() {
|
|
1262
1121
|
const { createClient } = await import('@supabase/supabase-js');
|
|
1263
|
-
|
|
1122
|
+
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
1264
1123
|
global: {
|
|
1265
1124
|
fetch: fetchSupabaseWithDeadline
|
|
1266
1125
|
},
|
|
@@ -1273,13 +1132,6 @@ async function createSupabaseClient() {
|
|
|
1273
1132
|
storage: createFileStorage(preferredClientAuthPath())
|
|
1274
1133
|
}
|
|
1275
1134
|
});
|
|
1276
|
-
Object.defineProperty(client, LIVEDESK_AUTHENTICATED_FETCH, {
|
|
1277
|
-
value: fetchSupabaseWithDeadline,
|
|
1278
|
-
enumerable: false,
|
|
1279
|
-
configurable: false,
|
|
1280
|
-
writable: false
|
|
1281
|
-
});
|
|
1282
|
-
return client;
|
|
1283
1135
|
}
|
|
1284
1136
|
|
|
1285
1137
|
function getNestedErrorCode(error) {
|
|
@@ -1327,18 +1179,12 @@ function isTransientNetworkError(error) {
|
|
|
1327
1179
|
return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
|
|
1328
1180
|
}
|
|
1329
1181
|
|
|
1330
|
-
|
|
1182
|
+
function formatDiscoveryError(error) {
|
|
1331
1183
|
if (isTransientNetworkError(error)) {
|
|
1332
1184
|
const code = getNestedErrorCode(error);
|
|
1333
1185
|
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1334
1186
|
}
|
|
1335
|
-
|
|
1336
|
-
if (message) return message;
|
|
1337
|
-
for (const key of ['error_description', 'reason', 'details', 'hint', 'error', 'code']) {
|
|
1338
|
-
const value = error?.[key];
|
|
1339
|
-
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
1340
|
-
}
|
|
1341
|
-
return 'LiveDesk could not read the current Hub registration yet.';
|
|
1187
|
+
return error instanceof Error ? error.message : String(error);
|
|
1342
1188
|
}
|
|
1343
1189
|
|
|
1344
1190
|
function createHubDiscoveryError(code, message, cause = null) {
|
|
@@ -1439,11 +1285,8 @@ async function refreshSessionIfNeededCore(supabase) {
|
|
|
1439
1285
|
const session = { ...candidate, ...normalized.session };
|
|
1440
1286
|
const expiresAt = Number(session.expires_at || 0);
|
|
1441
1287
|
if (expiresAt <= 0 || expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
// file value without setSession makes the UI say "Signed in" while
|
|
1445
|
-
// PostgREST still sends the publishable-key request as anon.
|
|
1446
|
-
return activateSupabaseSession(supabase, session);
|
|
1288
|
+
writeSavedSessionToFile(session);
|
|
1289
|
+
return session;
|
|
1447
1290
|
}
|
|
1448
1291
|
const { data, error } = await supabase.auth.refreshSession(session);
|
|
1449
1292
|
if (error) {
|
|
@@ -1494,10 +1337,14 @@ async function activateSupabaseSession(supabase, session) {
|
|
|
1494
1337
|
if (!normalized.ok) {
|
|
1495
1338
|
throw new Error(normalized.error);
|
|
1496
1339
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1340
|
+
const { data: current } = await supabase.auth.getSession();
|
|
1341
|
+
if (current?.session?.access_token === normalized.session.access_token) {
|
|
1342
|
+
const activeSession = current.session;
|
|
1343
|
+
if (!writeSavedSessionToFile(activeSession)) {
|
|
1344
|
+
throw new Error('refresh-token-required');
|
|
1345
|
+
}
|
|
1346
|
+
return activeSession;
|
|
1347
|
+
}
|
|
1501
1348
|
const { data, error } = await supabase.auth.setSession({
|
|
1502
1349
|
access_token: normalized.session.access_token,
|
|
1503
1350
|
refresh_token: normalized.session.refresh_token
|
|
@@ -1516,27 +1363,6 @@ async function activateSupabaseSession(supabase, session) {
|
|
|
1516
1363
|
return activeSession;
|
|
1517
1364
|
}
|
|
1518
1365
|
|
|
1519
|
-
export async function activateRoleChangeSession(supabase, currentSession = null) {
|
|
1520
|
-
if (!supabase?.auth) {
|
|
1521
|
-
throw new Error('Google sign-in is temporarily unavailable. Try again in a moment.');
|
|
1522
|
-
}
|
|
1523
|
-
const current = normalizeRuntimeAuthSession(currentSession, { requireRefreshToken: true });
|
|
1524
|
-
let session = current.ok ? current.session : await refreshSessionIfNeeded(supabase);
|
|
1525
|
-
if (!session?.access_token) {
|
|
1526
|
-
throw new Error('Sign in again before switching this computer to Hub.');
|
|
1527
|
-
}
|
|
1528
|
-
try {
|
|
1529
|
-
return await activateSupabaseSession(supabase, session);
|
|
1530
|
-
} catch (error) {
|
|
1531
|
-
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
1532
|
-
session = await refreshSessionIfNeeded(supabase);
|
|
1533
|
-
if (!session?.access_token) {
|
|
1534
|
-
throw new Error('Sign in again before switching this computer to Hub.');
|
|
1535
|
-
}
|
|
1536
|
-
return activateSupabaseSession(supabase, session);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
1366
|
function escapeHtml(value) {
|
|
1541
1367
|
return String(value ?? '')
|
|
1542
1368
|
.replaceAll('&', '&')
|
|
@@ -3254,17 +3080,14 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3254
3080
|
res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
|
|
3255
3081
|
return;
|
|
3256
3082
|
}
|
|
3257
|
-
const session = await
|
|
3258
|
-
supabase,
|
|
3259
|
-
dashboardState.choice?.session || savedSession
|
|
3260
|
-
);
|
|
3083
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
3261
3084
|
const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
|
|
3262
|
-
const { data, error } = await
|
|
3085
|
+
const { data, error } = await supabase.rpc('set_livedesk_device_role', {
|
|
3263
3086
|
p_device_id: deviceId,
|
|
3264
3087
|
p_role: 'hub',
|
|
3265
3088
|
p_assigned_hub_id: null,
|
|
3266
3089
|
p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
|
|
3267
|
-
})
|
|
3090
|
+
});
|
|
3268
3091
|
const result = Array.isArray(data) ? data[0] : data;
|
|
3269
3092
|
if (error || result?.ok === false || !session?.access_token) {
|
|
3270
3093
|
const reason = error?.message || result?.reason || 'role-change-rejected';
|
|
@@ -3296,7 +3119,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3296
3119
|
|
|
3297
3120
|
if (requestUrl.pathname === '/logout') {
|
|
3298
3121
|
try {
|
|
3299
|
-
await supabase.auth.signOut(
|
|
3122
|
+
await supabase.auth.signOut();
|
|
3300
3123
|
} catch {
|
|
3301
3124
|
}
|
|
3302
3125
|
clearSavedSession();
|
|
@@ -3567,62 +3390,35 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3567
3390
|
relayEndpoint: options.relayEndpoint
|
|
3568
3391
|
});
|
|
3569
3392
|
},
|
|
3570
|
-
changeRole: async (role, snapshot
|
|
3571
|
-
console.log('[LiveDesk Client] Hub role transition requested. Validating the signed-in session and local Hub port.');
|
|
3393
|
+
changeRole: async (role, snapshot) => {
|
|
3572
3394
|
const activeSupabase = await getSupabase();
|
|
3573
3395
|
if (!activeSupabase) {
|
|
3574
|
-
|
|
3575
|
-
return { ok: false, error: 'Google sign-in is temporarily unavailable. Try again in a moment.' };
|
|
3396
|
+
return { ok: false, error: 'supabase-session-required' };
|
|
3576
3397
|
}
|
|
3577
3398
|
const portPreflight = await preflightHubClientPort();
|
|
3578
3399
|
if (!portPreflight.ok) {
|
|
3579
|
-
console.warn(`[LiveDesk Client] Hub role transition rejected: ${hubClientPortPreflightError(portPreflight).error}`);
|
|
3580
3400
|
return hubClientPortPreflightError(portPreflight);
|
|
3581
3401
|
}
|
|
3582
|
-
|
|
3583
|
-
try {
|
|
3584
|
-
session = await activateRoleChangeSession(activeSupabase, currentSession);
|
|
3585
|
-
} catch (error) {
|
|
3586
|
-
const message = formatDiscoveryError(error);
|
|
3587
|
-
console.warn(`[LiveDesk Client] Hub role transition rejected: ${message}`);
|
|
3588
|
-
if (/sign in again|refresh-token-required|invalid refresh token/i.test(message)) {
|
|
3589
|
-
return {
|
|
3590
|
-
ok: false,
|
|
3591
|
-
error: 'role-change-auth-required',
|
|
3592
|
-
message: 'Sign in again before switching this computer to Hub.',
|
|
3593
|
-
authRequired: true
|
|
3594
|
-
};
|
|
3595
|
-
}
|
|
3596
|
-
return { ok: false, error: message };
|
|
3597
|
-
}
|
|
3402
|
+
const session = await refreshSessionIfNeeded(activeSupabase);
|
|
3598
3403
|
if (!session?.access_token) {
|
|
3599
|
-
|
|
3600
|
-
return {
|
|
3601
|
-
ok: false,
|
|
3602
|
-
error: 'role-change-auth-required',
|
|
3603
|
-
message: 'Sign in again before switching this computer to Hub.',
|
|
3604
|
-
authRequired: true
|
|
3605
|
-
};
|
|
3404
|
+
return { ok: false, error: 'supabase-session-required' };
|
|
3606
3405
|
}
|
|
3607
3406
|
const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
|
|
3608
|
-
const { data, error } = await
|
|
3407
|
+
const { data, error } = await activeSupabase.rpc('set_livedesk_device_role', {
|
|
3609
3408
|
p_device_id: options.deviceId,
|
|
3610
3409
|
p_role: role,
|
|
3611
3410
|
p_assigned_hub_id: null,
|
|
3612
3411
|
p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
|
|
3613
|
-
})
|
|
3412
|
+
});
|
|
3614
3413
|
const result = Array.isArray(data) ? data[0] : data;
|
|
3615
3414
|
if (error || result?.ok === false) {
|
|
3616
|
-
|
|
3617
|
-
console.warn(`[LiveDesk Client] Hub role transition rejected: ${reason}`);
|
|
3618
|
-
return { ok: false, error: reason };
|
|
3415
|
+
return { ok: false, error: error?.message || result?.reason || 'role-change-rejected' };
|
|
3619
3416
|
}
|
|
3620
3417
|
const roleVersion = Number.isInteger(result?.role_version) ? result.role_version : expectedRoleVersion + 1;
|
|
3621
3418
|
writeLocalRoleCache(role, options.deviceId, roleVersion);
|
|
3622
3419
|
roleRestartRequest = { role, requestedAt: new Date().toISOString() };
|
|
3623
3420
|
requestLocalDiscoveryWake('role-change');
|
|
3624
3421
|
try { requestActiveAgentStop(); } catch { /* the lifecycle loop observes the role request */ }
|
|
3625
|
-
console.log(`[LiveDesk Client] Hub role transition accepted at role revision ${roleVersion}. Starting the Hub runtime.`);
|
|
3626
3422
|
return { ok: true, restarting: true, role, roleVersion };
|
|
3627
3423
|
},
|
|
3628
3424
|
videoAcceleration: linuxVideoAccelerationStatus,
|
|
@@ -3774,57 +3570,45 @@ export function shouldSkipAutomaticDirectProbe(endpoint, options = {}) {
|
|
|
3774
3570
|
&& !isEndpointOnLocalNetwork(endpoint, options.networkInterfaces);
|
|
3775
3571
|
}
|
|
3776
3572
|
|
|
3777
|
-
|
|
3573
|
+
function requestHubSlotAssignment({ manager, pairToken, deviceId, slotNumber, timeoutMs = 5000 }) {
|
|
3778
3574
|
const endpoint = parseManagerEndpoint(manager);
|
|
3779
3575
|
const normalizedPairToken = String(pairToken || '').trim();
|
|
3780
3576
|
const normalizedDeviceId = String(deviceId || '').trim();
|
|
3781
3577
|
const normalizedSlot = normalizeSlotNumber(slotNumber);
|
|
3782
3578
|
if (!endpoint) {
|
|
3783
|
-
return { ok: false, error: 'hub-endpoint-unavailable' };
|
|
3579
|
+
return Promise.resolve({ ok: false, error: 'hub-endpoint-unavailable' });
|
|
3784
3580
|
}
|
|
3785
3581
|
if (!normalizedPairToken) {
|
|
3786
|
-
return { ok: false, error: 'hub-pair-token-unavailable' };
|
|
3582
|
+
return Promise.resolve({ ok: false, error: 'hub-pair-token-unavailable' });
|
|
3787
3583
|
}
|
|
3788
3584
|
if (!normalizedDeviceId) {
|
|
3789
|
-
return { ok: false, error: 'device-id-required' };
|
|
3585
|
+
return Promise.resolve({ ok: false, error: 'device-id-required' });
|
|
3790
3586
|
}
|
|
3791
3587
|
if (!normalizedSlot) {
|
|
3792
|
-
return { ok: false, error: 'invalid-slot-number' };
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
let socket;
|
|
3796
|
-
try {
|
|
3797
|
-
const credentialStore = createClientDeviceCredentialStore({ deviceId: normalizedDeviceId });
|
|
3798
|
-
socket = await connectSecureDirect({
|
|
3799
|
-
host: endpoint.host,
|
|
3800
|
-
port: endpoint.port,
|
|
3801
|
-
channel: 'control',
|
|
3802
|
-
deviceId: normalizedDeviceId,
|
|
3803
|
-
enrollmentToken: normalizedPairToken,
|
|
3804
|
-
credentialStore,
|
|
3805
|
-
timeoutMs
|
|
3806
|
-
});
|
|
3807
|
-
} catch (error) {
|
|
3808
|
-
return { ok: false, error: error?.code || error?.message || 'hub-slot-secure-connect-failed' };
|
|
3588
|
+
return Promise.resolve({ ok: false, error: 'invalid-slot-number' });
|
|
3809
3589
|
}
|
|
3810
3590
|
|
|
3811
3591
|
return new Promise(resolveAssignment => {
|
|
3592
|
+
const socket = net.createConnection(endpoint);
|
|
3812
3593
|
let settled = false;
|
|
3813
3594
|
let buffer = '';
|
|
3814
|
-
const timer = setTimeout(
|
|
3815
|
-
() => settle({ ok: false, error: 'hub-slot-request-timeout' }),
|
|
3816
|
-
Math.max(250, Math.min(30_000, Number(timeoutMs) || 5000))
|
|
3817
|
-
);
|
|
3818
|
-
timer.unref?.();
|
|
3819
3595
|
const settle = result => {
|
|
3820
3596
|
if (settled) return;
|
|
3821
3597
|
settled = true;
|
|
3822
|
-
clearTimeout(timer);
|
|
3823
3598
|
socket.removeAllListeners();
|
|
3824
3599
|
socket.destroy();
|
|
3825
3600
|
resolveAssignment(result);
|
|
3826
3601
|
};
|
|
3827
3602
|
socket.setEncoding('utf8');
|
|
3603
|
+
socket.setTimeout(timeoutMs);
|
|
3604
|
+
socket.once('connect', () => {
|
|
3605
|
+
socket.write(`${JSON.stringify({
|
|
3606
|
+
type: 'slot.assign',
|
|
3607
|
+
pairToken: normalizedPairToken,
|
|
3608
|
+
deviceId: normalizedDeviceId,
|
|
3609
|
+
slotNumber: Number(normalizedSlot)
|
|
3610
|
+
})}\n`);
|
|
3611
|
+
});
|
|
3828
3612
|
socket.on('data', chunk => {
|
|
3829
3613
|
buffer += chunk;
|
|
3830
3614
|
const newlineIndex = buffer.indexOf('\n');
|
|
@@ -3840,15 +3624,11 @@ export async function requestHubSlotAssignment({ manager, pairToken, deviceId, s
|
|
|
3840
3624
|
settle({ ok: false, error: 'invalid-hub-response' });
|
|
3841
3625
|
}
|
|
3842
3626
|
});
|
|
3627
|
+
socket.once('timeout', () => settle({ ok: false, error: 'hub-slot-request-timeout' }));
|
|
3843
3628
|
socket.once('error', error => settle({ ok: false, error: error?.message || 'hub-slot-request-failed' }));
|
|
3844
3629
|
socket.once('close', () => {
|
|
3845
|
-
|
|
3630
|
+
if (!settled) settle({ ok: false, error: 'hub-slot-response-missing' });
|
|
3846
3631
|
});
|
|
3847
|
-
socket.write(`${JSON.stringify({
|
|
3848
|
-
type: 'slot.assign',
|
|
3849
|
-
deviceId: normalizedDeviceId,
|
|
3850
|
-
slotNumber: Number(normalizedSlot)
|
|
3851
|
-
})}\n`);
|
|
3852
3632
|
});
|
|
3853
3633
|
}
|
|
3854
3634
|
|
|
@@ -4032,9 +3812,7 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
4032
3812
|
}
|
|
4033
3813
|
const timeoutMs = normalizeFreshRegistryTimeoutMs(options.timeoutMs);
|
|
4034
3814
|
const controller = new AbortController();
|
|
4035
|
-
const externalSignal = options.signal;
|
|
4036
3815
|
let timeoutHandle = null;
|
|
4037
|
-
let removeExternalAbort = () => undefined;
|
|
4038
3816
|
const lookupOutcome = Promise.resolve()
|
|
4039
3817
|
.then(() => resolveFreshTarget(controller.signal))
|
|
4040
3818
|
.then(
|
|
@@ -4048,17 +3826,6 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
4048
3826
|
);
|
|
4049
3827
|
});
|
|
4050
3828
|
const outcomes = [lookupOutcome, timeoutOutcome];
|
|
4051
|
-
if (externalSignal) {
|
|
4052
|
-
outcomes.push(new Promise(resolveAbort => {
|
|
4053
|
-
const onAbort = () => resolveAbort({ type: 'local-trigger' });
|
|
4054
|
-
if (externalSignal.aborted) {
|
|
4055
|
-
onAbort();
|
|
4056
|
-
return;
|
|
4057
|
-
}
|
|
4058
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
4059
|
-
removeExternalAbort = () => externalSignal.removeEventListener('abort', onAbort);
|
|
4060
|
-
}));
|
|
4061
|
-
}
|
|
4062
3829
|
if (options.wakePromise && typeof options.wakePromise.then === 'function') {
|
|
4063
3830
|
outcomes.push(Promise.resolve(options.wakePromise).then(
|
|
4064
3831
|
event => ({ type: 'hub-online', event }),
|
|
@@ -4067,12 +3834,7 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
4067
3834
|
}
|
|
4068
3835
|
const outcome = await Promise.race(outcomes).finally(() => {
|
|
4069
3836
|
clearTimeout(timeoutHandle);
|
|
4070
|
-
removeExternalAbort();
|
|
4071
3837
|
});
|
|
4072
|
-
if (outcome.type === 'local-trigger') {
|
|
4073
|
-
controller.abort('local-trigger');
|
|
4074
|
-
return outcome;
|
|
4075
|
-
}
|
|
4076
3838
|
if (outcome.type === 'hub-online') {
|
|
4077
3839
|
controller.abort('hub-online');
|
|
4078
3840
|
return outcome;
|
|
@@ -4185,45 +3947,20 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4185
3947
|
if (options.signal?.aborted) {
|
|
4186
3948
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4187
3949
|
}
|
|
4188
|
-
|
|
4189
|
-
const supplied = normalizeRuntimeAuthSession(options.session, { requireRefreshToken: true });
|
|
4190
|
-
const suppliedExpiresAt = Number(supplied.session?.expires_at || 0);
|
|
4191
|
-
const suppliedFresh = supplied.ok && (suppliedExpiresAt <= 0
|
|
4192
|
-
|| suppliedExpiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS);
|
|
4193
|
-
let session = suppliedFresh ? supplied.session : await refreshSessionIfNeeded(supabase);
|
|
4194
|
-
if (!session?.access_token) {
|
|
4195
|
-
throw createHubDiscoveryError('hub-registry-auth-required', 'Sign in again before finding the LiveDesk Hub.');
|
|
4196
|
-
}
|
|
4197
|
-
if (!fetchImpl && suppliedFresh) {
|
|
4198
|
-
session = await activateSupabaseSession(supabase, session);
|
|
4199
|
-
}
|
|
3950
|
+
await refreshSessionIfNeeded(supabase);
|
|
4200
3951
|
if (options.signal?.aborted) {
|
|
4201
3952
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4202
3953
|
}
|
|
4203
|
-
let
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
errorMessage: 'LiveDesk could not read the current Hub registration.'
|
|
4214
|
-
});
|
|
4215
|
-
data = Array.isArray(payload) ? payload[0] : payload;
|
|
4216
|
-
} else {
|
|
4217
|
-
let query = supabase
|
|
4218
|
-
.from('livedesk_remote_host_targets')
|
|
4219
|
-
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
4220
|
-
.eq('product_key', 'livedesk');
|
|
4221
|
-
if (options.signal && typeof query.abortSignal === 'function') {
|
|
4222
|
-
query = query.abortSignal(options.signal);
|
|
4223
|
-
}
|
|
4224
|
-
const result = await query.maybeSingle();
|
|
4225
|
-
if (result.error) throw result.error;
|
|
4226
|
-
data = result.data;
|
|
3954
|
+
let query = supabase
|
|
3955
|
+
.from('livedesk_remote_host_targets')
|
|
3956
|
+
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
3957
|
+
.eq('product_key', 'livedesk');
|
|
3958
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
3959
|
+
query = query.abortSignal(options.signal);
|
|
3960
|
+
}
|
|
3961
|
+
const { data, error } = await query.maybeSingle();
|
|
3962
|
+
if (error) {
|
|
3963
|
+
throw error;
|
|
4227
3964
|
}
|
|
4228
3965
|
if (!data?.active) {
|
|
4229
3966
|
throw createHubDiscoveryError(
|
|
@@ -4276,28 +4013,17 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
|
4276
4013
|
if (!options.deviceId || !options.session?.access_token) {
|
|
4277
4014
|
return null;
|
|
4278
4015
|
}
|
|
4279
|
-
if (roleRestartRequest?.role) {
|
|
4280
|
-
return { ok: false, skipped: true, reason: 'role-transition-pending' };
|
|
4281
|
-
}
|
|
4282
4016
|
try {
|
|
4283
|
-
const { data, error } = await
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
p_assigned_hub_id: options.assignedHubId || null,
|
|
4292
|
-
p_platform: os.platform(),
|
|
4293
|
-
p_os_version: os.release(),
|
|
4294
|
-
p_app_version: readPackageVersion()
|
|
4295
|
-
});
|
|
4017
|
+
const { data, error } = await supabase.rpc('register_livedesk_device', {
|
|
4018
|
+
p_device_id: options.deviceId,
|
|
4019
|
+
p_device_name: options.deviceName || os.hostname(),
|
|
4020
|
+
p_role: 'client',
|
|
4021
|
+
p_assigned_hub_id: options.assignedHubId || null,
|
|
4022
|
+
p_platform: os.platform(),
|
|
4023
|
+
p_os_version: os.release(),
|
|
4024
|
+
p_app_version: readPackageVersion()
|
|
4296
4025
|
});
|
|
4297
4026
|
const result = Array.isArray(data) ? data[0] : data;
|
|
4298
|
-
if (result?.skipped === true && result?.reason === 'role-transition-pending') {
|
|
4299
|
-
return result;
|
|
4300
|
-
}
|
|
4301
4027
|
if (error || result?.ok === false) {
|
|
4302
4028
|
console.warn(`[LiveDesk Client] Device role registration unavailable: ${error?.message || result?.reason || 'unknown-error'}`);
|
|
4303
4029
|
}
|
|
@@ -4318,7 +4044,6 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4318
4044
|
let lastMessage = '';
|
|
4319
4045
|
let wakeListener = null;
|
|
4320
4046
|
let initialError = options.initialError || null;
|
|
4321
|
-
let discoverySession = options.session || null;
|
|
4322
4047
|
console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
4323
4048
|
try {
|
|
4324
4049
|
while (true) {
|
|
@@ -4328,16 +4053,7 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4328
4053
|
if (!wakeListener) {
|
|
4329
4054
|
try {
|
|
4330
4055
|
wakeListener = await createWakeListener({
|
|
4331
|
-
getAccessToken: async () =>
|
|
4332
|
-
const normalized = normalizeRuntimeAuthSession(discoverySession, { requireRefreshToken: true });
|
|
4333
|
-
const expiresAt = Number(normalized.session?.expires_at || 0);
|
|
4334
|
-
if (normalized.ok && (expiresAt <= 0
|
|
4335
|
-
|| expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS)) {
|
|
4336
|
-
return normalized.session.access_token;
|
|
4337
|
-
}
|
|
4338
|
-
discoverySession = await refreshSessionIfNeeded(supabase);
|
|
4339
|
-
return discoverySession?.access_token || '';
|
|
4340
|
-
}
|
|
4056
|
+
getAccessToken: async () => (await refreshSessionIfNeeded(supabase))?.access_token || ''
|
|
4341
4057
|
});
|
|
4342
4058
|
} catch (error) {
|
|
4343
4059
|
if (attempts === 0) {
|
|
@@ -4356,19 +4072,13 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4356
4072
|
signal => resolveManagerFromSupabase(supabase, {
|
|
4357
4073
|
allowRelayFallback: options.allowRelayFallback === true,
|
|
4358
4074
|
probeEndpoint: options.probeEndpoint,
|
|
4359
|
-
session: discoverySession,
|
|
4360
4075
|
signal
|
|
4361
4076
|
}),
|
|
4362
4077
|
{
|
|
4363
4078
|
timeoutMs: options.freshTimeoutMs,
|
|
4364
|
-
wakePromise: wakeListener?.promise
|
|
4365
|
-
signal: discoveryWakeController.signal
|
|
4079
|
+
wakePromise: wakeListener?.promise
|
|
4366
4080
|
}
|
|
4367
4081
|
);
|
|
4368
|
-
if (outcome.type === 'local-trigger') {
|
|
4369
|
-
if (shouldStop()) return null;
|
|
4370
|
-
continue;
|
|
4371
|
-
}
|
|
4372
4082
|
if (outcome.type === 'hub-online') {
|
|
4373
4083
|
wakeListener?.close();
|
|
4374
4084
|
wakeListener = null;
|
|
@@ -4527,7 +4237,6 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4527
4237
|
}
|
|
4528
4238
|
return await resolveManagerFromSupabase(supabase, {
|
|
4529
4239
|
allowRelayFallback,
|
|
4530
|
-
session,
|
|
4531
4240
|
signal
|
|
4532
4241
|
});
|
|
4533
4242
|
}
|
|
@@ -4535,18 +4244,6 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4535
4244
|
} catch (error) {
|
|
4536
4245
|
initialDiscoveryError = error;
|
|
4537
4246
|
}
|
|
4538
|
-
if (roleRestartRequest?.role) {
|
|
4539
|
-
return {
|
|
4540
|
-
...parsed,
|
|
4541
|
-
manager,
|
|
4542
|
-
pair,
|
|
4543
|
-
slot: normalizeSlotNumber(parsed.slot),
|
|
4544
|
-
connectionPage,
|
|
4545
|
-
forwarded,
|
|
4546
|
-
rediscoverOnDisconnect: shouldLogin,
|
|
4547
|
-
rediscoverOnInvalidPair: shouldLogin
|
|
4548
|
-
};
|
|
4549
|
-
}
|
|
4550
4247
|
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4551
4248
|
if (resolved?.discoverySource === 'cache') {
|
|
4552
4249
|
writeSavedSessionToFile(session);
|
|
@@ -4560,13 +4257,12 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4560
4257
|
if (!resolved) {
|
|
4561
4258
|
resolved = await waitForManagerFromSupabase(supabase, {
|
|
4562
4259
|
allowRelayFallback,
|
|
4563
|
-
session,
|
|
4564
4260
|
initialError: initialDiscoveryError,
|
|
4565
4261
|
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
4566
4262
|
});
|
|
4567
4263
|
discoverySource = 'supabase';
|
|
4568
4264
|
}
|
|
4569
|
-
if (roleRestartRequest?.role) {
|
|
4265
|
+
if (!resolved && roleRestartRequest?.role) {
|
|
4570
4266
|
return {
|
|
4571
4267
|
...parsed,
|
|
4572
4268
|
manager,
|
|
@@ -4681,67 +4377,42 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4681
4377
|
};
|
|
4682
4378
|
}
|
|
4683
4379
|
|
|
4684
|
-
function getFastRuntimeSpec() {
|
|
4685
|
-
return resolveFastPlatformSpec({
|
|
4686
|
-
platform: os.platform(),
|
|
4687
|
-
arch: os.arch(),
|
|
4688
|
-
manifest: readClientPackageManifest()
|
|
4689
|
-
});
|
|
4690
|
-
}
|
|
4691
|
-
|
|
4692
4380
|
function getFastRuntime() {
|
|
4693
|
-
const
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
const repaired = inspectRepairedFastRuntime(UNIFIED_CLIENT_STATE_DIR, spec);
|
|
4713
|
-
if (repaired) return { ...repaired, source: 'repaired-runtime-cache' };
|
|
4714
|
-
return { ...bundled, packageName: spec.packageName, source: 'missing' };
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
|
-
async function repairFastRuntimeIfMissing(runtime, connectionPage = null) {
|
|
4718
|
-
if (hasFastExecutable(runtime) || hasFastDll(runtime)) return runtime;
|
|
4719
|
-
const spec = getFastRuntimeSpec();
|
|
4720
|
-
if (!spec) return runtime;
|
|
4721
|
-
const label = `${spec.packageName}@${spec.version}`;
|
|
4722
|
-
console.warn(`[LiveDesk Client] RemoteFast ${spec.rid} is missing from the npx installation. Repairing ${label}.`);
|
|
4723
|
-
connectionPage?.update({
|
|
4724
|
-
message: `Installing the verified ${spec.rid} screen engine. This is needed only when the npx cache omitted it.`,
|
|
4725
|
-
agent: {
|
|
4726
|
-
requestedEngine: 'fast',
|
|
4727
|
-
engine: 'fast',
|
|
4728
|
-
state: 'repairing',
|
|
4729
|
-
runtimeId: spec.rid,
|
|
4730
|
-
command: label,
|
|
4731
|
-
args: []
|
|
4381
|
+
const platform = os.platform();
|
|
4382
|
+
const arch = os.arch();
|
|
4383
|
+
const resolvePackagedRuntime = (packageName, rid, executableName) => {
|
|
4384
|
+
try {
|
|
4385
|
+
const packagePath = require.resolve(`${packageName}/package.json`);
|
|
4386
|
+
const fastRoot = join(dirname(packagePath), 'fast');
|
|
4387
|
+
return {
|
|
4388
|
+
rid,
|
|
4389
|
+
packageName,
|
|
4390
|
+
executable: join(fastRoot, executableName),
|
|
4391
|
+
dll: join(fastRoot, 'livedesk-client-fast.dll')
|
|
4392
|
+
};
|
|
4393
|
+
} catch {
|
|
4394
|
+
return {
|
|
4395
|
+
rid,
|
|
4396
|
+
packageName: '',
|
|
4397
|
+
executable: join(packageRoot, 'fast', rid, executableName),
|
|
4398
|
+
dll: join(packageRoot, 'fast', rid, 'livedesk-client-fast.dll')
|
|
4399
|
+
};
|
|
4732
4400
|
}
|
|
4733
|
-
}
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4401
|
+
};
|
|
4402
|
+
if (platform === 'win32' && arch === 'x64') {
|
|
4403
|
+
return resolvePackagedRuntime('@livedesk/fast-win-x64', 'win-x64', 'livedesk-client-fast.exe');
|
|
4404
|
+
}
|
|
4405
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
4406
|
+
return resolvePackagedRuntime('@livedesk/fast-osx-arm64', 'osx-arm64', 'livedesk-client-fast');
|
|
4407
|
+
}
|
|
4408
|
+
if (platform === 'darwin' && arch === 'x64') {
|
|
4409
|
+
return resolvePackagedRuntime('@livedesk/fast-osx-x64', 'osx-x64', 'livedesk-client-fast');
|
|
4410
|
+
}
|
|
4411
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
4412
|
+
return resolvePackagedRuntime('@livedesk/fast-linux-x64', 'linux-x64', 'livedesk-client-fast');
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
return null;
|
|
4745
4416
|
}
|
|
4746
4417
|
|
|
4747
4418
|
function summarizeText(value, maxLength = 600) {
|
|
@@ -5345,12 +5016,6 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
|
|
|
5345
5016
|
};
|
|
5346
5017
|
let connectionPage = null;
|
|
5347
5018
|
if (isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
|
|
5348
|
-
// Hydrate the local runtime from the DPAPI/keychain-backed saved session
|
|
5349
|
-
// before opening the browser. Starting the page as anonymous and only
|
|
5350
|
-
// reading this session later lets the renderer begin a redundant Google
|
|
5351
|
-
// OAuth flow on every process restart. The discovery path below still
|
|
5352
|
-
// refreshes and installs this session into the exact Supabase client.
|
|
5353
|
-
const persistedStartupSession = readSavedSessionFromFile();
|
|
5354
5019
|
let resolveStarted;
|
|
5355
5020
|
let rejectStarted;
|
|
5356
5021
|
const started = new Promise((resolve, reject) => {
|
|
@@ -5364,8 +5029,8 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
|
|
|
5364
5029
|
engine: parsed.engine,
|
|
5365
5030
|
slot: parsed.slot,
|
|
5366
5031
|
startupArgs: buildStartupClientArgs(parsed),
|
|
5367
|
-
savedSession:
|
|
5368
|
-
loadSavedSession:
|
|
5032
|
+
savedSession: null,
|
|
5033
|
+
loadSavedSession: false,
|
|
5369
5034
|
savedPin: null,
|
|
5370
5035
|
allowRelayFallback: transportAllowsRelay(parsed.transport),
|
|
5371
5036
|
relayEndpoint: parsed.relay,
|
|
@@ -5401,31 +5066,7 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
|
|
|
5401
5066
|
connectionPage?.close?.();
|
|
5402
5067
|
process.exit(0);
|
|
5403
5068
|
}
|
|
5404
|
-
|
|
5405
|
-
const fastSpec = getFastRuntimeSpec();
|
|
5406
|
-
let fastRepairError = null;
|
|
5407
|
-
if (prepared.engine !== 'node'
|
|
5408
|
-
&& fastSpec
|
|
5409
|
-
&& !hasFastExecutable(fastRuntime)
|
|
5410
|
-
&& !hasFastDll(fastRuntime)) {
|
|
5411
|
-
try {
|
|
5412
|
-
fastRuntime = await repairFastRuntimeIfMissing(fastRuntime, prepared.connectionPage);
|
|
5413
|
-
} catch (error) {
|
|
5414
|
-
fastRepairError = error;
|
|
5415
|
-
const message = summarizeText(error?.message || error, 600);
|
|
5416
|
-
console.error(`[LiveDesk Client] RemoteFast repair failed: ${message}`);
|
|
5417
|
-
prepared.connectionPage?.update({
|
|
5418
|
-
agent: {
|
|
5419
|
-
requestedEngine: prepared.engine,
|
|
5420
|
-
engine: 'fast',
|
|
5421
|
-
state: 'failed',
|
|
5422
|
-
runtimeId: fastSpec.rid,
|
|
5423
|
-
error: message
|
|
5424
|
-
},
|
|
5425
|
-
message: 'The screen engine could not be restored. LiveDesk did not start a reduced Node connection.'
|
|
5426
|
-
});
|
|
5427
|
-
}
|
|
5428
|
-
}
|
|
5069
|
+
const fastRuntime = getFastRuntime();
|
|
5429
5070
|
const fastRequired = requiresFastTransport(prepared);
|
|
5430
5071
|
if (fastRequired && prepared.engine === 'node') {
|
|
5431
5072
|
throw new Error(
|
|
@@ -5476,25 +5117,13 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
|
|
|
5476
5117
|
state: 'running'
|
|
5477
5118
|
});
|
|
5478
5119
|
});
|
|
5479
|
-
} else if (prepared.engine === 'fast'
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
const unavailableMessage = `C# RemoteFast is unavailable: ${fastLaunch.reason}.${repairDetail}`;
|
|
5487
|
-
console.error(`${unavailableMessage} The reduced Node connection was not started because it cannot provide the required LiveDesk screen stream.`);
|
|
5488
|
-
prepared.connectionPage?.update({
|
|
5489
|
-
agent: {
|
|
5490
|
-
requestedEngine: prepared.engine,
|
|
5491
|
-
engine: 'fast',
|
|
5492
|
-
state: 'failed',
|
|
5493
|
-
runtimeId: fastRuntime?.rid || fastSpec?.rid || '',
|
|
5494
|
-
error: unavailableMessage
|
|
5495
|
-
},
|
|
5496
|
-
message: 'LiveDesk could not start the verified screen engine. Restart the same @latest command after network access is restored.'
|
|
5497
|
-
});
|
|
5120
|
+
} else if (prepared.engine === 'fast' || fastRequired) {
|
|
5121
|
+
console.error(
|
|
5122
|
+
`C# RemoteFast is unavailable: ${fastLaunch.reason}. `
|
|
5123
|
+
+ (fastRequired
|
|
5124
|
+
? 'The requested P2P, WebSocket, or encrypted relay transport requires the packaged RemoteFast runtime; the legacy Node agent was not started.'
|
|
5125
|
+
: 'Use --engine node to run the legacy Node agent.')
|
|
5126
|
+
);
|
|
5498
5127
|
prepared.connectionPage?.close?.();
|
|
5499
5128
|
process.exit(2);
|
|
5500
5129
|
} else {
|