@livedesk/client 0.1.57 → 0.1.59
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.js +161 -12
- package/package.json +1 -1
package/bin/livedesk-client.js
CHANGED
|
@@ -26,6 +26,7 @@ const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngv
|
|
|
26
26
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
27
27
|
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
28
28
|
const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
|
|
29
|
+
const CLIENT_PIN_PATH = join(CLIENT_STATE_DIR, 'pin.json');
|
|
29
30
|
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
30
31
|
|
|
31
32
|
function printHelp() {
|
|
@@ -39,6 +40,7 @@ Usage:
|
|
|
39
40
|
Default flow:
|
|
40
41
|
Opens a LiveDesk connection page with Google sign-in or a 6-digit PIN.
|
|
41
42
|
After auth, that page stays open as this computer's client dashboard.
|
|
43
|
+
Saved Google sign-in or saved PIN starts the client automatically.
|
|
42
44
|
Omit the number for first-available placement, or pass 1-999 to pin a slot.
|
|
43
45
|
If no LiveDesk Hub is active yet, the client keeps checking every 5 seconds.
|
|
44
46
|
|
|
@@ -404,6 +406,29 @@ function createFileStorage(filePath) {
|
|
|
404
406
|
};
|
|
405
407
|
}
|
|
406
408
|
|
|
409
|
+
function readSavedSessionFromFile() {
|
|
410
|
+
try {
|
|
411
|
+
const state = JSON.parse(readFileSync(CLIENT_AUTH_PATH, 'utf8'));
|
|
412
|
+
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
413
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const session = JSON.parse(raw);
|
|
417
|
+
return session?.access_token ? session : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function writeSavedSessionToFile(session) {
|
|
424
|
+
if (!session?.access_token || !session?.refresh_token) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
const storage = createFileStorage(CLIENT_AUTH_PATH);
|
|
428
|
+
storage.setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify(session));
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
407
432
|
async function createSupabaseClient() {
|
|
408
433
|
const { createClient } = await import('@supabase/supabase-js');
|
|
409
434
|
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
@@ -459,19 +484,22 @@ function formatDiscoveryError(error) {
|
|
|
459
484
|
|
|
460
485
|
async function refreshSessionIfNeeded(supabase) {
|
|
461
486
|
const { data: existing } = await supabase.auth.getSession();
|
|
462
|
-
const session = existing?.session;
|
|
487
|
+
const session = existing?.session || readSavedSessionFromFile();
|
|
463
488
|
if (!session?.access_token) {
|
|
464
489
|
return null;
|
|
465
490
|
}
|
|
466
491
|
const expiresAt = Number(session.expires_at || 0);
|
|
467
492
|
if (expiresAt <= 0 || expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS) {
|
|
493
|
+
writeSavedSessionToFile(session);
|
|
468
494
|
return session;
|
|
469
495
|
}
|
|
470
496
|
const { data, error } = await supabase.auth.refreshSession(session);
|
|
471
497
|
if (error) {
|
|
472
498
|
throw error;
|
|
473
499
|
}
|
|
474
|
-
|
|
500
|
+
const refreshed = data?.session || session;
|
|
501
|
+
writeSavedSessionToFile(refreshed);
|
|
502
|
+
return refreshed;
|
|
475
503
|
}
|
|
476
504
|
|
|
477
505
|
function openBrowser(url) {
|
|
@@ -941,6 +969,19 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '', startup =
|
|
|
941
969
|
startupToggle.addEventListener('change', syncStartupFields);
|
|
942
970
|
syncStartupFields();
|
|
943
971
|
}
|
|
972
|
+
async function watchAutoConnection() {
|
|
973
|
+
try {
|
|
974
|
+
const response = await fetch('/api/state', { cache: 'no-store' });
|
|
975
|
+
if (!response.ok) return;
|
|
976
|
+
const state = await response.json();
|
|
977
|
+
if (state.completed) {
|
|
978
|
+
location.replace('/');
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
setInterval(watchAutoConnection, 800);
|
|
984
|
+
watchAutoConnection();
|
|
944
985
|
</script>
|
|
945
986
|
</body>
|
|
946
987
|
</html>`;
|
|
@@ -982,6 +1023,7 @@ function renderConnectionDashboardPage(state = {}) {
|
|
|
982
1023
|
message,
|
|
983
1024
|
slot,
|
|
984
1025
|
startup: Boolean(state.startup),
|
|
1026
|
+
completed: true,
|
|
985
1027
|
statusLabel
|
|
986
1028
|
}).replaceAll('<', '\\u003c');
|
|
987
1029
|
|
|
@@ -1263,6 +1305,32 @@ function normalizePairingPin(value) {
|
|
|
1263
1305
|
return /^\d{6}$/.test(pin) ? pin : '';
|
|
1264
1306
|
}
|
|
1265
1307
|
|
|
1308
|
+
function readSavedPin() {
|
|
1309
|
+
try {
|
|
1310
|
+
const state = JSON.parse(readFileSync(CLIENT_PIN_PATH, 'utf8'));
|
|
1311
|
+
return normalizePairingPin(state?.pin);
|
|
1312
|
+
} catch {
|
|
1313
|
+
return '';
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function writeSavedPin(pin) {
|
|
1318
|
+
const normalizedPin = normalizePairingPin(pin);
|
|
1319
|
+
if (!normalizedPin) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
mkdirSync(dirname(CLIENT_PIN_PATH), { recursive: true });
|
|
1323
|
+
writeFileSync(CLIENT_PIN_PATH, JSON.stringify({
|
|
1324
|
+
pin: normalizedPin,
|
|
1325
|
+
updatedAt: new Date().toISOString()
|
|
1326
|
+
}, null, 2));
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function clearSavedPin() {
|
|
1331
|
+
rmSync(CLIENT_PIN_PATH, { force: true });
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1266
1334
|
function readRequestBody(req, maxBytes = 4096) {
|
|
1267
1335
|
return new Promise((resolve, reject) => {
|
|
1268
1336
|
let body = '';
|
|
@@ -1314,11 +1382,42 @@ async function resolveManagerFromPin(supabase, pin) {
|
|
|
1314
1382
|
};
|
|
1315
1383
|
}
|
|
1316
1384
|
|
|
1385
|
+
async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
1386
|
+
const intervalMs = Math.max(1000, Number(options.intervalMs || DISCOVERY_RETRY_MS));
|
|
1387
|
+
const intervalSeconds = Math.max(1, Math.round(intervalMs / 1000));
|
|
1388
|
+
const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
|
|
1389
|
+
let attempts = 0;
|
|
1390
|
+
let lastMessage = '';
|
|
1391
|
+
console.log(`Waiting for a LiveDesk Hub from saved PIN. This client will keep trying every ${intervalSeconds}s.`);
|
|
1392
|
+
while (true) {
|
|
1393
|
+
if (shouldStop()) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
attempts += 1;
|
|
1397
|
+
try {
|
|
1398
|
+
return await resolveManagerFromPin(supabase, pin);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
if (shouldStop()) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
const message = formatDiscoveryError(err);
|
|
1404
|
+
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
1405
|
+
const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
|
|
1406
|
+
console.log(`Still waiting for LiveDesk Hub by saved PIN${suffix}: ${message}`);
|
|
1407
|
+
lastMessage = message;
|
|
1408
|
+
}
|
|
1409
|
+
await sleep(intervalMs);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1317
1414
|
async function startConnectionChoiceServer(supabase, options = {}) {
|
|
1318
1415
|
const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
|
|
1319
1416
|
const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
1320
1417
|
const slot = normalizeSlotNumber(options.slot);
|
|
1321
1418
|
const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
|
|
1419
|
+
const savedSession = options.savedSession?.access_token ? options.savedSession : null;
|
|
1420
|
+
const savedPin = normalizePairingPin(options.savedPin);
|
|
1322
1421
|
let pendingStartup = isWindowsStartupRegistered();
|
|
1323
1422
|
let listeningPort = port;
|
|
1324
1423
|
let completed = false;
|
|
@@ -1379,6 +1478,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1379
1478
|
message: dashboardState.message || completedMessage,
|
|
1380
1479
|
slot: normalizeSlotNumber(slot) ? `Slot ${String(slot).padStart(3, '0')}` : 'First available',
|
|
1381
1480
|
startup: Boolean(pendingStartup),
|
|
1481
|
+
completed,
|
|
1382
1482
|
statusLabel: dashboardState.loggedOut
|
|
1383
1483
|
? 'Signed out for next restart'
|
|
1384
1484
|
: manager
|
|
@@ -1389,6 +1489,38 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1389
1489
|
};
|
|
1390
1490
|
};
|
|
1391
1491
|
|
|
1492
|
+
const startSavedConnection = async () => {
|
|
1493
|
+
if (completed || dashboardState.loggedOut) {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (savedSession?.access_token) {
|
|
1497
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
1498
|
+
complete({ type: 'google', session: savedSession }, 'LiveDesk client dashboard', 'Saved Google sign-in found. The client is starting automatically.');
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (!savedPin) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
|
|
1505
|
+
try {
|
|
1506
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
1507
|
+
shouldStop: () => completed || dashboardState.loggedOut
|
|
1508
|
+
});
|
|
1509
|
+
if (!resolved) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (completed || dashboardState.loggedOut) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
1516
|
+
complete({ type: 'pin', ...resolved }, 'LiveDesk client dashboard', 'Saved PIN accepted. The client is starting automatically.');
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
if (!completed) {
|
|
1519
|
+
dashboardState.message = `Saved PIN did not connect: ${formatDiscoveryError(err)}`;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1392
1524
|
const handleError = (res, error, pin = '') => {
|
|
1393
1525
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1394
1526
|
res.end(renderChoice({
|
|
@@ -1421,6 +1553,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1421
1553
|
} catch {
|
|
1422
1554
|
}
|
|
1423
1555
|
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
1556
|
+
clearSavedPin();
|
|
1424
1557
|
dashboardState.choice = null;
|
|
1425
1558
|
dashboardState.loggedOut = true;
|
|
1426
1559
|
dashboardState.manager = '';
|
|
@@ -1453,6 +1586,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1453
1586
|
pendingStartup = normalizeStartupChoice(requestUrl.searchParams.get('startup'), pendingStartup);
|
|
1454
1587
|
const { data: existing } = await supabase.auth.getSession();
|
|
1455
1588
|
if (existing?.session?.access_token) {
|
|
1589
|
+
writeSavedSessionToFile(existing.session);
|
|
1456
1590
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1457
1591
|
const choice = { type: 'google', session: existing.session };
|
|
1458
1592
|
complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
|
|
@@ -1494,6 +1628,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1494
1628
|
pin = normalizePairingPin(pin);
|
|
1495
1629
|
try {
|
|
1496
1630
|
const resolved = await resolveManagerFromPin(supabase, pin);
|
|
1631
|
+
writeSavedPin(pin);
|
|
1497
1632
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1498
1633
|
const choice = { type: 'pin', ...resolved };
|
|
1499
1634
|
complete(choice, 'LiveDesk client dashboard', 'PIN accepted. The client is starting automatically.');
|
|
@@ -1545,6 +1680,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1545
1680
|
server.close();
|
|
1546
1681
|
return;
|
|
1547
1682
|
}
|
|
1683
|
+
writeSavedSessionToFile(sessionData.session);
|
|
1548
1684
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1549
1685
|
const choice = { type: 'google', session: sessionData.session };
|
|
1550
1686
|
complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
|
|
@@ -1582,6 +1718,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1582
1718
|
});
|
|
1583
1719
|
const address = server.address();
|
|
1584
1720
|
listeningPort = typeof address === 'object' && address ? address.port : port;
|
|
1721
|
+
if (savedSession?.access_token || savedPin) {
|
|
1722
|
+
setImmediate(() => {
|
|
1723
|
+
void startSavedConnection();
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1585
1726
|
} catch (err) {
|
|
1586
1727
|
if (err?.code === 'EADDRINUSE') {
|
|
1587
1728
|
throw new Error(`LiveDesk connection page port ${port} is already in use. Close the app using it or run with --auth-port <port> and add that callback URL in Supabase Auth redirect URLs.`);
|
|
@@ -1744,20 +1885,28 @@ async function prepareLoginConnection(parsed) {
|
|
|
1744
1885
|
const supabase = await createSupabaseClient();
|
|
1745
1886
|
const startupArgs = buildStartupClientArgs(parsed);
|
|
1746
1887
|
let savedSession = null;
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
console.warn(`LiveDesk saved sign-in could not be refreshed: ${err?.message || err}`);
|
|
1752
|
-
}
|
|
1888
|
+
try {
|
|
1889
|
+
savedSession = await refreshSessionIfNeeded(supabase);
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
console.warn(`LiveDesk saved sign-in could not be refreshed: ${err?.message || err}`);
|
|
1753
1892
|
}
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1893
|
+
const savedPin = readSavedPin();
|
|
1894
|
+
let choice = null;
|
|
1895
|
+
if (parsed.startupRun && savedSession?.access_token) {
|
|
1896
|
+
choice = { type: 'google', session: savedSession };
|
|
1897
|
+
} else if (parsed.startupRun && savedPin) {
|
|
1898
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin);
|
|
1899
|
+
choice = { type: 'pin', ...resolved };
|
|
1900
|
+
}
|
|
1901
|
+
if (!choice) {
|
|
1902
|
+
choice = await chooseClientConnection(supabase, {
|
|
1757
1903
|
authPort: parsed.authPort,
|
|
1758
1904
|
slot: parsed.slot,
|
|
1759
|
-
startupArgs
|
|
1905
|
+
startupArgs,
|
|
1906
|
+
savedSession,
|
|
1907
|
+
savedPin
|
|
1760
1908
|
});
|
|
1909
|
+
}
|
|
1761
1910
|
connectionPage = choice.connectionPage || null;
|
|
1762
1911
|
if (choice.type === 'pin') {
|
|
1763
1912
|
manager = choice.manager;
|