@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.
@@ -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
- return data?.session || session;
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
- if (parsed.startupRun) {
1748
- try {
1749
- savedSession = await refreshSessionIfNeeded(supabase);
1750
- } catch (err) {
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 choice = savedSession?.access_token
1755
- ? { type: 'google', session: savedSession }
1756
- : await chooseClientConnection(supabase, {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {