@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.
@@ -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 the verified C# RemoteFast screen engine on supported desktop platforms.
340
- If an npx cache omitted it, LiveDesk restores the exact pinned platform package before connecting.
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 plaintextRefreshToken = String(session?.refresh_token || '').trim();
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
- const persisted = { ...session, ...normalized.session, refresh_token_ref: OS_SECRET_REFERENCE };
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
- const client = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
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
- export function formatDiscoveryError(error) {
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
- const message = getNestedErrorMessage(error);
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
- // A persisted session is not automatically installed into this
1443
- // memory-only Supabase client after a process restart. Returning the
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
- // Always install the verified session into this exact Supabase client.
1498
- // A freshly created client can read the persisted token through getSession()
1499
- // before PostgREST has adopted its Authorization header. Skipping setSession
1500
- // in that state makes the UI look signed in while table and RPC calls run as anon.
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('&', '&amp;')
@@ -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 activateRoleChangeSession(
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 enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(supabase, session, 'set_livedesk_device_role', {
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({ scope: 'local' });
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, currentSession) => {
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
- console.warn('[LiveDesk Client] Hub role transition rejected: Google sign-in is temporarily unavailable.');
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
- let session;
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
- console.warn('[LiveDesk Client] Hub role transition rejected: a fresh Google sign-in is required.');
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 enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(activeSupabase, session, 'set_livedesk_device_role', {
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
- const reason = error?.message || result?.reason || 'role-change-rejected';
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
- export async function requestHubSlotAssignment({ manager, pairToken, deviceId, slotNumber, timeoutMs = 5000 }) {
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
- if (!settled) settle({ ok: false, error: 'hub-slot-response-missing' });
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
- const fetchImpl = explicitAuthenticatedFetch(supabase, options.fetchImpl);
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 data;
4204
- if (fetchImpl) {
4205
- const payload = await fetchAuthenticatedSupabaseJson(session, 'livedesk_remote_host_targets', {
4206
- fetchImpl,
4207
- signal: options.signal,
4208
- query: {
4209
- select: 'node_id,endpoint,endpoint_candidates,pair_token,active,expires_at,updated_at,manager_version',
4210
- product_key: 'eq.livedesk',
4211
- limit: '1'
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 enqueueDeviceRoleMutation(() => {
4284
- if (roleRestartRequest?.role) {
4285
- return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
4286
- }
4287
- return callVerifiedSupabaseRpc(supabase, options.session, 'register_livedesk_device', {
4288
- p_device_id: options.deviceId,
4289
- p_device_name: options.deviceName || os.hostname(),
4290
- p_role: 'client',
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 spec = getFastRuntimeSpec();
4694
- if (!spec) return null;
4695
- try {
4696
- const packagePath = require.resolve(`${spec.packageName}/package.json`);
4697
- const installed = inspectFastRuntimePackage(dirname(packagePath), spec);
4698
- if (installed) return { ...installed, source: 'optional-dependency' };
4699
- } catch {
4700
- // A cached npx install can be complete except for its optional platform package.
4701
- }
4702
- const bundledFastRoot = join(packageRoot, 'fast', spec.rid);
4703
- const bundled = {
4704
- rid: spec.rid,
4705
- packageName: '',
4706
- packageVersion: spec.version,
4707
- executable: join(bundledFastRoot, spec.executableName),
4708
- dll: join(bundledFastRoot, 'livedesk-client-fast.dll'),
4709
- source: 'bundled-development-runtime'
4710
- };
4711
- if (hasFastExecutable(bundled) || hasFastDll(bundled)) return bundled;
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
- const repaired = await ensureRepairedFastRuntime({
4735
- stateDir: UNIFIED_CLIENT_STATE_DIR,
4736
- spec,
4737
- nodeExecutable: process.execPath,
4738
- npmExecPath: process.env.npm_execpath || process.env.NPM_EXECPATH,
4739
- npmExecutable: process.env.LIVEDESK_NPM_EXECUTABLE,
4740
- env: process.env
4741
- });
4742
- clearFastPreflightCache();
4743
- console.log(`[LiveDesk Client] Repaired ${label} in the private LiveDesk runtime cache.`);
4744
- return { ...repaired, source: 'repaired-runtime-cache' };
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: persistedStartupSession,
5368
- loadSavedSession: true,
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
- let fastRuntime = getFastRuntime();
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
- || fastRequired
5481
- || fastRepairError
5482
- || (fastSpec && fastRuntime?.source === 'missing')) {
5483
- const repairDetail = fastRepairError
5484
- ? ` Automatic repair failed: ${summarizeText(fastRepairError?.message || fastRepairError, 500)}`
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 {