@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 +5 -4
- package/bin/livedesk-client.js +140 -13
- package/package.json +1 -1
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,
|
|
11
|
-
published by the same account, and connects locally.
|
|
12
|
-
|
|
13
|
-
|
|
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
|
package/bin/livedesk-client.js
CHANGED
|
@@ -30,8 +30,9 @@ Usage:
|
|
|
30
30
|
npx @livedesk/client 3
|
|
31
31
|
|
|
32
32
|
Default flow:
|
|
33
|
-
Opens Google sign-in,
|
|
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:
|
|
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('&', '&')
|
|
285
|
+
.replaceAll('<', '<')
|
|
286
|
+
.replaceAll('>', '>')
|
|
287
|
+
.replaceAll('"', '"')
|
|
288
|
+
.replaceAll("'", ''');
|
|
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
|
|
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(
|
|
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/
|
|
303
|
-
res.end(
|
|
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(
|
|
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}:${
|
|
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
|
-
|
|
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
|
|
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}.`);
|