@livedesk/client 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -7,10 +7,11 @@ npx @livedesk/client
7
7
  npx @livedesk/client 3
8
8
  ```
9
9
 
10
- The default flow opens Google sign-in, finds the active LiveDesk manager
11
- published by the same account, and connects locally. Omit the number for
12
- first-available placement, or pass `1` to `999` to pin this machine to a screen
13
- wall slot.
10
+ The default flow opens Google sign-in, waits for the active LiveDesk manager
11
+ published by the same account, and connects locally. If the manager is not
12
+ ready yet, the client keeps checking every second instead of exiting. Omit the
13
+ number for first-available placement, or pass `1` to `999` to pin this machine
14
+ to a screen wall slot.
14
15
 
15
16
  On the manager computer, open the LiveDesk dashboard and sign in first. The
16
17
  dashboard keeps its local hub address and private pair token refreshed in the
@@ -30,8 +30,9 @@ Usage:
30
30
  npx @livedesk/client 3
31
31
 
32
32
  Default flow:
33
- Opens Google sign-in, finds the signed-in manager, and connects locally.
33
+ Opens Google sign-in, waits for the signed-in manager, and connects locally.
34
34
  Omit the number for first-available placement, or pass 1-999 to pin a slot.
35
+ If no manager is active yet, the client keeps checking every second.
35
36
 
36
37
  Options:
37
38
  --slot <number> Screen wall slot number for this computer.
@@ -259,7 +260,7 @@ async function createSupabaseClient() {
259
260
  const { createClient } = await import('@supabase/supabase-js');
260
261
  return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
261
262
  auth: {
262
- autoRefreshToken: false,
263
+ autoRefreshToken: true,
263
264
  persistSession: true,
264
265
  detectSessionInUrl: false,
265
266
  flowType: 'pkce',
@@ -278,9 +279,79 @@ function openBrowser(url) {
278
279
  spawn(command, [url], { detached: true, stdio: 'ignore' }).unref();
279
280
  }
280
281
 
282
+ function escapeHtml(value) {
283
+ return String(value ?? '')
284
+ .replaceAll('&', '&amp;')
285
+ .replaceAll('<', '&lt;')
286
+ .replaceAll('>', '&gt;')
287
+ .replaceAll('"', '&quot;')
288
+ .replaceAll("'", '&#39;');
289
+ }
290
+
291
+ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
292
+ const accent = tone === 'error' ? '#991b1b' : tone === 'waiting' ? '#374151' : '#111827';
293
+ return `<!doctype html>
294
+ <html lang="en">
295
+ <head>
296
+ <meta charset="utf-8">
297
+ <meta name="viewport" content="width=device-width, initial-scale=1">
298
+ <title>${escapeHtml(title)}</title>
299
+ <style>
300
+ :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
301
+ * { box-sizing: border-box; }
302
+ html, body { width: 100%; min-height: 100%; margin: 0; }
303
+ body {
304
+ display: grid;
305
+ place-items: center;
306
+ padding: 24px;
307
+ background:
308
+ radial-gradient(circle at 50% 0%, rgba(148, 163, 184, 0.22), transparent 34%),
309
+ linear-gradient(180deg, #f8fafc, #eef2f7);
310
+ color: #111827;
311
+ }
312
+ main {
313
+ width: min(440px, 100%);
314
+ display: grid;
315
+ gap: 14px;
316
+ padding: 28px;
317
+ border: 1px solid rgba(148, 163, 184, 0.34);
318
+ border-radius: 12px;
319
+ background: rgba(255, 255, 255, 0.9);
320
+ box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
321
+ text-align: center;
322
+ }
323
+ .mark {
324
+ width: 42px;
325
+ height: 42px;
326
+ display: grid;
327
+ place-items: center;
328
+ justify-self: center;
329
+ border-radius: 10px;
330
+ background: ${accent};
331
+ color: #ffffff;
332
+ font-weight: 950;
333
+ }
334
+ h1 { margin: 0; font-size: 28px; line-height: 1.05; letter-spacing: 0; }
335
+ p { margin: 0; color: #64748b; font-size: 15px; line-height: 1.5; font-weight: 650; }
336
+ small { color: #94a3b8; font-size: 12px; font-weight: 750; }
337
+ </style>
338
+ </head>
339
+ <body>
340
+ <main>
341
+ <div class="mark">LD</div>
342
+ <h1>${escapeHtml(title)}</h1>
343
+ <p>${escapeHtml(message)}</p>
344
+ <small>LiveDesk Client</small>
345
+ </main>
346
+ </body>
347
+ </html>`;
348
+ }
349
+
281
350
  async function startOAuthCallbackServer(options = {}) {
282
351
  const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
283
352
  const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
353
+ let listeningPort = port;
354
+ let completed = false;
284
355
  let settleCode;
285
356
  let rejectCode;
286
357
  const waitForCode = new Promise((resolve, reject) => {
@@ -288,23 +359,49 @@ async function startOAuthCallbackServer(options = {}) {
288
359
  rejectCode = reject;
289
360
  });
290
361
  const server = createServer((req, res) => {
291
- const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${server.address().port}`);
362
+ const requestUrl = new URL(req.url || '/', `http://${host}:${listeningPort}`);
363
+ if (requestUrl.pathname === '/favicon.ico') {
364
+ res.writeHead(204);
365
+ res.end();
366
+ return;
367
+ }
368
+ if (completed) {
369
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
370
+ res.end(renderOAuthCallbackPage({
371
+ title: 'LiveDesk sign-in complete',
372
+ message: 'You can close this tab and return to the terminal.'
373
+ }));
374
+ return;
375
+ }
292
376
  const code = requestUrl.searchParams.get('code');
293
377
  const error = requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error');
294
378
  if (error) {
295
379
  res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
296
- res.end('<h1>LiveDesk sign-in failed</h1><p>You can close this tab.</p>');
380
+ res.end(renderOAuthCallbackPage({
381
+ title: 'LiveDesk sign-in failed',
382
+ message: error,
383
+ tone: 'error'
384
+ }));
385
+ completed = true;
297
386
  rejectCode(new Error(error));
298
387
  server.close();
299
388
  return;
300
389
  }
301
390
  if (!code) {
302
- res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
303
- res.end('LiveDesk sign-in callback is waiting for an auth code.');
391
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
392
+ res.end(renderOAuthCallbackPage({
393
+ title: 'Waiting for LiveDesk sign-in',
394
+ message: 'This local callback is waiting for an auth code from Google.',
395
+ tone: 'waiting'
396
+ }));
304
397
  return;
305
398
  }
306
399
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
307
- res.end('<h1>LiveDesk sign-in complete</h1><p>You can close this tab and return to the terminal.</p>');
400
+ res.end(renderOAuthCallbackPage({
401
+ title: 'LiveDesk sign-in complete',
402
+ message: 'You can close this tab and return to the terminal.'
403
+ }));
404
+ completed = true;
308
405
  settleCode(code);
309
406
  server.close();
310
407
  });
@@ -313,6 +410,8 @@ async function startOAuthCallbackServer(options = {}) {
313
410
  server.once('error', reject);
314
411
  server.listen(port, host, resolve);
315
412
  });
413
+ const address = server.address();
414
+ listeningPort = typeof address === 'object' && address ? address.port : port;
316
415
  } catch (err) {
317
416
  if (err?.code === 'EADDRINUSE') {
318
417
  throw new Error(`LiveDesk Google sign-in callback 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.`);
@@ -321,7 +420,7 @@ async function startOAuthCallbackServer(options = {}) {
321
420
  }
322
421
  return {
323
422
  get redirectTo() {
324
- return `http://${host}:${server.address().port}/callback`;
423
+ return `http://${host}:${listeningPort}/callback`;
325
424
  },
326
425
  waitForCode
327
426
  };
@@ -394,13 +493,17 @@ function canConnectToEndpoint(endpoint, timeoutMs = 1200) {
394
493
  });
395
494
  }
396
495
 
397
- async function chooseReachableEndpoint(candidates) {
496
+ function sleep(ms) {
497
+ return new Promise(resolve => setTimeout(resolve, ms));
498
+ }
499
+
500
+ async function chooseReachableEndpoint(candidates, options = {}) {
398
501
  for (const endpoint of candidates) {
399
502
  if (await canConnectToEndpoint(endpoint)) {
400
503
  return endpoint;
401
504
  }
402
505
  }
403
- return candidates[0] || DEFAULT_MANAGER;
506
+ return options.requireReachable ? '' : (candidates[0] || DEFAULT_MANAGER);
404
507
  }
405
508
 
406
509
  function normalizeEndpointCandidates(target) {
@@ -411,7 +514,7 @@ function normalizeEndpointCandidates(target) {
411
514
  return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
412
515
  }
413
516
 
414
- async function resolveManagerFromSupabase(supabase) {
517
+ async function resolveManagerFromSupabase(supabase, options = {}) {
415
518
  const { data, error } = await supabase
416
519
  .from('livedesk_remote_host_targets')
417
520
  .select('endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
@@ -433,7 +536,10 @@ async function resolveManagerFromSupabase(supabase) {
433
536
  if (endpointCandidates.length === 0) {
434
537
  throw new Error('The LiveDesk manager record does not contain a reachable local address.');
435
538
  }
436
- const manager = await chooseReachableEndpoint(endpointCandidates);
539
+ const manager = await chooseReachableEndpoint(endpointCandidates, { requireReachable: options.requireReachable === true });
540
+ if (!manager) {
541
+ throw new Error(`No reachable LiveDesk manager endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
542
+ }
437
543
  return {
438
544
  manager,
439
545
  pair: data.pair_token,
@@ -441,6 +547,27 @@ async function resolveManagerFromSupabase(supabase) {
441
547
  };
442
548
  }
443
549
 
550
+ async function waitForManagerFromSupabase(supabase, options = {}) {
551
+ const intervalMs = Math.max(250, Number(options.intervalMs || 1000));
552
+ let attempts = 0;
553
+ let lastMessage = '';
554
+ console.log('Waiting for a LiveDesk manager. This client will keep trying every 1s.');
555
+ while (true) {
556
+ attempts += 1;
557
+ try {
558
+ return await resolveManagerFromSupabase(supabase, { requireReachable: true });
559
+ } catch (err) {
560
+ const message = err instanceof Error ? err.message : String(err);
561
+ if (message !== lastMessage || attempts === 1 || attempts % 10 === 0) {
562
+ const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
563
+ console.log(`Still waiting for LiveDesk manager${suffix}: ${message}`);
564
+ lastMessage = message;
565
+ }
566
+ await sleep(intervalMs);
567
+ }
568
+ }
569
+ }
570
+
444
571
  async function prepareLoginConnection(parsed) {
445
572
  if (parsed.logout) {
446
573
  rmSync(CLIENT_AUTH_PATH, { force: true });
@@ -458,7 +585,7 @@ async function prepareLoginConnection(parsed) {
458
585
  const session = await signInWithGoogle(supabase, { authPort: parsed.authPort });
459
586
  const email = session.user?.email ? ` as ${session.user.email}` : '';
460
587
  console.log(`Signed in to LiveDesk${email}.`);
461
- const resolved = await resolveManagerFromSupabase(supabase);
588
+ const resolved = await waitForManagerFromSupabase(supabase);
462
589
  manager = resolved.manager;
463
590
  pair = resolved.pair;
464
591
  console.log(`Found LiveDesk manager at ${manager}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {