livedesk 0.1.395 → 0.1.396
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.js +26 -8
- package/bootstrap/hub-auth-handoff.js +97 -0
- package/client/bin/livedesk-client.js +68 -8
- package/client/src/runtime/client-runtime-server.js +1 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-NPl8KozM.js → index-CxlGjz9d.js} +1 -1
- package/web/dist/index.html +1 -1
package/bin/livedesk.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createRoleBootstrapServer } from '../bootstrap/role-bootstrap-server.js
|
|
|
15
15
|
import { parseLsofPids } from '../bootstrap/port-inspection.js';
|
|
16
16
|
import { isKnownLiveDeskClientAgentProcess } from '../bootstrap/client-process-identity.js';
|
|
17
17
|
import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
|
|
18
|
+
import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
|
|
18
19
|
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1514,14 +1515,31 @@ async function runManager(args, resolvedRole = null) {
|
|
|
1514
1515
|
});
|
|
1515
1516
|
};
|
|
1516
1517
|
|
|
1517
|
-
startHubProcess();
|
|
1518
|
-
updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1518
|
+
startHubProcess();
|
|
1519
|
+
updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
|
|
1520
|
+
|
|
1521
|
+
const hubReady = waitForManager(hubProbeBaseUrl);
|
|
1522
|
+
const hubStartup = hubReady.then(async ok => {
|
|
1523
|
+
if (!ok) {
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
const handoff = await transferSavedClientSessionToHub(hubProbeBaseUrl, MANAGER_STATE_DIR);
|
|
1527
|
+
if (handoff.ok) {
|
|
1528
|
+
reportLauncherUpdate('[LiveDesk Hub] Saved Client sign-in adopted by the Hub.');
|
|
1529
|
+
if (!handoff.hostTargetOk) {
|
|
1530
|
+
reportLauncherUpdate('[LiveDesk Hub] Sign-in was adopted; host publication will continue through lease renewal.', true);
|
|
1531
|
+
}
|
|
1532
|
+
} else if (!handoff.skipped) {
|
|
1533
|
+
reportLauncherUpdate(`[LiveDesk Hub] Saved Client sign-in could not be adopted: ${handoff.reason}. Continue sign-in in the Hub page.`, true);
|
|
1534
|
+
}
|
|
1535
|
+
return true;
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
if (options.openBrowserOnStart) {
|
|
1539
|
+
void hubStartup.then(ok => {
|
|
1540
|
+
if (ok) {
|
|
1541
|
+
openBrowser(openUrl);
|
|
1542
|
+
} else {
|
|
1525
1543
|
console.warn(`LiveDesk Hub did not answer at ${openUrl} yet. Open it manually when it is ready.`);
|
|
1526
1544
|
}
|
|
1527
1545
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { normalizeRuntimeAuthSession } from '../runtime-core/src/auth-session.js';
|
|
4
|
+
|
|
5
|
+
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
6
|
+
const MINIMUM_SESSION_LIFETIME_SECONDS = 30;
|
|
7
|
+
|
|
8
|
+
function parseStoredSession(value) {
|
|
9
|
+
if (value && typeof value === 'object') {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Contract:
|
|
24
|
+
* - Reads only the unified local Client auth store.
|
|
25
|
+
* - Returns only a refreshable access session that will remain valid long
|
|
26
|
+
* enough for the new local Hub to verify it.
|
|
27
|
+
* - Never logs or serializes token values outside the loopback handoff.
|
|
28
|
+
*/
|
|
29
|
+
export async function readSavedClientSessionForHub(
|
|
30
|
+
stateDir,
|
|
31
|
+
nowSeconds = Math.floor(Date.now() / 1000)
|
|
32
|
+
) {
|
|
33
|
+
try {
|
|
34
|
+
const authState = JSON.parse(await readFile(join(stateDir, 'auth.json'), 'utf8'));
|
|
35
|
+
const candidate = parseStoredSession(authState?.[CLIENT_AUTH_STORAGE_KEY]);
|
|
36
|
+
const normalized = normalizeRuntimeAuthSession(candidate, { requireRefreshToken: true });
|
|
37
|
+
if (!normalized.ok) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const expiresAt = Number(normalized.session.expires_at || 0);
|
|
41
|
+
if (!Number.isFinite(expiresAt)
|
|
42
|
+
|| expiresAt - Number(nowSeconds || 0) <= MINIMUM_SESSION_LIFETIME_SECONDS) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return normalized.session;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function transferSavedClientSessionToHub(
|
|
52
|
+
baseUrl,
|
|
53
|
+
stateDir,
|
|
54
|
+
fetchImpl = fetch,
|
|
55
|
+
nowSeconds = Math.floor(Date.now() / 1000)
|
|
56
|
+
) {
|
|
57
|
+
const session = await readSavedClientSessionForHub(stateDir, nowSeconds);
|
|
58
|
+
if (!session) {
|
|
59
|
+
return { ok: false, skipped: true, reason: 'no-valid-saved-client-session' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetchImpl(new URL('/api/auth/session', baseUrl), {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
Accept: 'application/json'
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
accessToken: session.access_token,
|
|
71
|
+
refreshToken: session.refresh_token,
|
|
72
|
+
expiresAt: session.expires_at
|
|
73
|
+
}),
|
|
74
|
+
signal: AbortSignal.timeout(5_000)
|
|
75
|
+
});
|
|
76
|
+
const data = await response.json().catch(() => null);
|
|
77
|
+
if (!response.ok || data?.authenticated !== true) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
skipped: false,
|
|
81
|
+
reason: String(data?.error || `hub-auth-session-http-${response.status}`)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
skipped: false,
|
|
87
|
+
authenticated: true,
|
|
88
|
+
hostTargetOk: data?.hostTarget?.ok !== false
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
skipped: false,
|
|
94
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -21,6 +21,7 @@ const packageRoot = resolve(__dirname, '..');
|
|
|
21
21
|
const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
|
|
22
22
|
const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
|
|
23
23
|
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
24
|
+
const DEFAULT_HUB_CLIENT_PORT = 5197;
|
|
24
25
|
const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
|
|
25
26
|
const DEFAULT_AUTH_CALLBACK_PORT = 5179;
|
|
26
27
|
const ENDPOINT_PROBE_TIMEOUT_MS = 900;
|
|
@@ -369,15 +370,64 @@ function parseLauncherArgs(argv) {
|
|
|
369
370
|
};
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
function normalizePort(value) {
|
|
373
|
+
function normalizePort(value) {
|
|
373
374
|
const port = Number(String(value || '').trim());
|
|
374
375
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
375
376
|
return 0;
|
|
376
377
|
}
|
|
377
378
|
return port;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function resolveHubClientPort(env = process.env) {
|
|
382
|
+
return normalizePort(env.REMOTE_HUB_PORT || env.LIVEDESK_HUB_REMOTE_PORT) || DEFAULT_HUB_CLIENT_PORT;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function preflightHubClientPort(port = resolveHubClientPort()) {
|
|
386
|
+
const normalizedPort = normalizePort(port);
|
|
387
|
+
if (!normalizedPort) {
|
|
388
|
+
return Promise.resolve({ ok: false, port: Number(port) || 0, code: 'invalid-hub-client-port' });
|
|
389
|
+
}
|
|
390
|
+
return new Promise(resolvePreflight => {
|
|
391
|
+
const probe = net.createServer();
|
|
392
|
+
let completed = false;
|
|
393
|
+
const finish = result => {
|
|
394
|
+
if (completed) return;
|
|
395
|
+
completed = true;
|
|
396
|
+
resolvePreflight({ port: normalizedPort, ...result });
|
|
397
|
+
};
|
|
398
|
+
probe.unref?.();
|
|
399
|
+
probe.once('error', error => {
|
|
400
|
+
finish({
|
|
401
|
+
ok: false,
|
|
402
|
+
code: error?.code === 'EADDRINUSE' ? 'hub-client-port-in-use' : 'hub-client-port-unavailable',
|
|
403
|
+
systemCode: String(error?.code || '')
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
probe.listen({ host: '0.0.0.0', port: normalizedPort, exclusive: true }, () => {
|
|
407
|
+
probe.close(error => {
|
|
408
|
+
if (error) {
|
|
409
|
+
finish({ ok: false, code: 'hub-client-port-unavailable', systemCode: String(error?.code || '') });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
finish({ ok: true, code: 'ok' });
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function hubClientPortPreflightError(preflight) {
|
|
419
|
+
const port = Number(preflight?.port || resolveHubClientPort());
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
code: String(preflight?.code || 'hub-client-port-unavailable'),
|
|
423
|
+
port,
|
|
424
|
+
error: preflight?.code === 'hub-client-port-in-use'
|
|
425
|
+
? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
|
|
426
|
+
: `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeSlotNumber(value) {
|
|
381
431
|
const number = Number(String(value || '').trim());
|
|
382
432
|
if (!Number.isInteger(number) || number < 1 || number > 999) {
|
|
383
433
|
return '';
|
|
@@ -2648,12 +2698,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
2648
2698
|
payload = Object.fromEntries(params.entries());
|
|
2649
2699
|
}
|
|
2650
2700
|
const nextRole = String(payload.role || '').trim().toLowerCase();
|
|
2651
|
-
if (nextRole !== 'hub') {
|
|
2701
|
+
if (nextRole !== 'hub') {
|
|
2652
2702
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
|
|
2653
2703
|
res.end(JSON.stringify({ ok: false, error: 'client-can-only-transition-to-hub' }));
|
|
2654
|
-
return;
|
|
2655
|
-
}
|
|
2656
|
-
const
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const portPreflight = await preflightHubClientPort();
|
|
2707
|
+
if (!portPreflight.ok) {
|
|
2708
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
|
|
2709
|
+
res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
2657
2713
|
const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
|
|
2658
2714
|
const { data, error } = await supabase.rpc('set_livedesk_device_role', {
|
|
2659
2715
|
p_device_id: deviceId,
|
|
@@ -2948,6 +3004,10 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
2948
3004
|
if (!supabase) {
|
|
2949
3005
|
return { ok: false, error: 'supabase-session-required' };
|
|
2950
3006
|
}
|
|
3007
|
+
const portPreflight = await preflightHubClientPort();
|
|
3008
|
+
if (!portPreflight.ok) {
|
|
3009
|
+
return hubClientPortPreflightError(portPreflight);
|
|
3010
|
+
}
|
|
2951
3011
|
const session = await refreshSessionIfNeeded(supabase);
|
|
2952
3012
|
if (!session?.access_token) {
|
|
2953
3013
|
return { ok: false, error: 'supabase-session-required' };
|
|
@@ -739,6 +739,7 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
739
739
|
osVersion: os.release(),
|
|
740
740
|
pid: process.pid,
|
|
741
741
|
cpuUsagePercent: Math.round(cpuUsagePercent * 10) / 10,
|
|
742
|
+
cpuLogicalCores: Math.max(1, os.cpus().length),
|
|
742
743
|
memoryTotalBytes: totalMemoryBytes,
|
|
743
744
|
memoryUsedBytes: Math.max(0, totalMemoryBytes - freeMemoryBytes),
|
|
744
745
|
uptimeSeconds: Math.max(0, Math.floor(os.uptime())),
|