@livedesk/client 0.1.47 → 0.1.49
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 +392 -74
- package/fast/linux-x64/livedesk-client-fast.dll +0 -0
- package/fast/linux-x64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-arm64/livedesk-client-fast.dll +0 -0
- package/fast/osx-arm64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-x64/livedesk-client-fast.dll +0 -0
- package/fast/osx-x64/livedesk-client-fast.pdb +0 -0
- package/fast/win-x64/livedesk-client-fast.dll +0 -0
- package/fast/win-x64/livedesk-client-fast.pdb +0 -0
- package/package.json +1 -1
package/bin/livedesk-client.js
CHANGED
|
@@ -35,7 +35,7 @@ Usage:
|
|
|
35
35
|
npx @livedesk/client 3
|
|
36
36
|
|
|
37
37
|
Default flow:
|
|
38
|
-
Opens
|
|
38
|
+
Opens a LiveDesk connection page with Google sign-in or a 6-digit PIN.
|
|
39
39
|
Omit the number for first-available placement, or pass 1-999 to pin a slot.
|
|
40
40
|
If no LiveDesk Hub is active yet, the client keeps checking every 5 seconds.
|
|
41
41
|
|
|
@@ -428,18 +428,262 @@ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
|
|
|
428
428
|
</html>`;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
|
|
431
|
+
function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
432
|
+
const errorBlock = error
|
|
433
|
+
? `<div class="error">${escapeHtml(error)}</div>`
|
|
434
|
+
: '';
|
|
435
|
+
return `<!doctype html>
|
|
436
|
+
<html lang="en">
|
|
437
|
+
<head>
|
|
438
|
+
<meta charset="utf-8">
|
|
439
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
440
|
+
<title>Connect LiveDesk</title>
|
|
441
|
+
<style>
|
|
442
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
443
|
+
* { box-sizing: border-box; }
|
|
444
|
+
html, body { width: 100%; min-height: 100%; margin: 0; }
|
|
445
|
+
body {
|
|
446
|
+
display: grid;
|
|
447
|
+
place-items: center;
|
|
448
|
+
padding: 24px;
|
|
449
|
+
background:
|
|
450
|
+
radial-gradient(circle at 50% 0%, rgba(148, 163, 184, 0.22), transparent 34%),
|
|
451
|
+
linear-gradient(180deg, #f8fafc, #eef2f7);
|
|
452
|
+
color: #111827;
|
|
453
|
+
}
|
|
454
|
+
main {
|
|
455
|
+
width: min(440px, 100%);
|
|
456
|
+
display: grid;
|
|
457
|
+
gap: 18px;
|
|
458
|
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
|
459
|
+
border-radius: 18px;
|
|
460
|
+
padding: 28px;
|
|
461
|
+
background: rgba(255, 255, 255, 0.92);
|
|
462
|
+
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
|
|
463
|
+
}
|
|
464
|
+
.brand {
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
gap: 12px;
|
|
468
|
+
}
|
|
469
|
+
.mark {
|
|
470
|
+
width: 42px;
|
|
471
|
+
height: 42px;
|
|
472
|
+
display: grid;
|
|
473
|
+
place-items: center;
|
|
474
|
+
border-radius: 10px;
|
|
475
|
+
background: #111827;
|
|
476
|
+
color: #ffffff;
|
|
477
|
+
font-weight: 950;
|
|
478
|
+
}
|
|
479
|
+
h1 { margin: 0; font-size: 28px; line-height: 1.05; letter-spacing: 0; }
|
|
480
|
+
p { margin: 5px 0 0; color: #64748b; font-size: 14px; line-height: 1.45; font-weight: 650; }
|
|
481
|
+
.panel {
|
|
482
|
+
display: grid;
|
|
483
|
+
gap: 12px;
|
|
484
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
485
|
+
border-radius: 14px;
|
|
486
|
+
padding: 14px;
|
|
487
|
+
background: #ffffff;
|
|
488
|
+
}
|
|
489
|
+
form {
|
|
490
|
+
display: grid;
|
|
491
|
+
gap: 12px;
|
|
492
|
+
margin: 0;
|
|
493
|
+
}
|
|
494
|
+
label {
|
|
495
|
+
display: grid;
|
|
496
|
+
gap: 8px;
|
|
497
|
+
color: #475569;
|
|
498
|
+
font-size: 12px;
|
|
499
|
+
font-weight: 900;
|
|
500
|
+
text-transform: uppercase;
|
|
501
|
+
letter-spacing: 0.04em;
|
|
502
|
+
}
|
|
503
|
+
input {
|
|
504
|
+
height: 48px;
|
|
505
|
+
border: 1px solid rgba(148, 163, 184, 0.45);
|
|
506
|
+
border-radius: 12px;
|
|
507
|
+
padding: 0 14px;
|
|
508
|
+
color: #111827;
|
|
509
|
+
font-size: 24px;
|
|
510
|
+
font-weight: 950;
|
|
511
|
+
letter-spacing: 0.18em;
|
|
512
|
+
text-align: center;
|
|
513
|
+
outline: 0;
|
|
514
|
+
}
|
|
515
|
+
input:focus {
|
|
516
|
+
border-color: #111827;
|
|
517
|
+
box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
|
|
518
|
+
}
|
|
519
|
+
button, .google {
|
|
520
|
+
height: 44px;
|
|
521
|
+
display: inline-flex;
|
|
522
|
+
align-items: center;
|
|
523
|
+
justify-content: center;
|
|
524
|
+
gap: 8px;
|
|
525
|
+
border: 0;
|
|
526
|
+
border-radius: 12px;
|
|
527
|
+
background: #111827;
|
|
528
|
+
color: #ffffff;
|
|
529
|
+
font-size: 14px;
|
|
530
|
+
font-weight: 900;
|
|
531
|
+
text-decoration: none;
|
|
532
|
+
cursor: pointer;
|
|
533
|
+
}
|
|
534
|
+
.google {
|
|
535
|
+
background: #ffffff;
|
|
536
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
537
|
+
color: #111827;
|
|
538
|
+
}
|
|
539
|
+
.divider {
|
|
540
|
+
display: grid;
|
|
541
|
+
grid-template-columns: 1fr auto 1fr;
|
|
542
|
+
gap: 10px;
|
|
543
|
+
align-items: center;
|
|
544
|
+
color: #94a3b8;
|
|
545
|
+
font-size: 11px;
|
|
546
|
+
font-weight: 900;
|
|
547
|
+
text-transform: uppercase;
|
|
548
|
+
letter-spacing: 0.08em;
|
|
549
|
+
}
|
|
550
|
+
.divider::before, .divider::after {
|
|
551
|
+
content: "";
|
|
552
|
+
height: 1px;
|
|
553
|
+
background: rgba(148, 163, 184, 0.3);
|
|
554
|
+
}
|
|
555
|
+
.error {
|
|
556
|
+
border: 1px solid rgba(220, 38, 38, 0.22);
|
|
557
|
+
border-radius: 12px;
|
|
558
|
+
padding: 10px 12px;
|
|
559
|
+
background: rgba(254, 226, 226, 0.75);
|
|
560
|
+
color: #991b1b;
|
|
561
|
+
font-size: 13px;
|
|
562
|
+
font-weight: 800;
|
|
563
|
+
}
|
|
564
|
+
small {
|
|
565
|
+
color: #94a3b8;
|
|
566
|
+
text-align: center;
|
|
567
|
+
font-size: 12px;
|
|
568
|
+
font-weight: 750;
|
|
569
|
+
}
|
|
570
|
+
</style>
|
|
571
|
+
</head>
|
|
572
|
+
<body>
|
|
573
|
+
<main>
|
|
574
|
+
<div class="brand">
|
|
575
|
+
<div class="mark">LD</div>
|
|
576
|
+
<div>
|
|
577
|
+
<h1>Connect LiveDesk</h1>
|
|
578
|
+
<p>Use Google for your own Hub, or enter the 6-digit PIN shown on the screen wall.</p>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
${errorBlock}
|
|
582
|
+
<section class="panel">
|
|
583
|
+
<form method="post" action="/pin">
|
|
584
|
+
<label>
|
|
585
|
+
Pairing PIN
|
|
586
|
+
<input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
|
|
587
|
+
</label>
|
|
588
|
+
<button type="submit">Connect with PIN</button>
|
|
589
|
+
</form>
|
|
590
|
+
</section>
|
|
591
|
+
<div class="divider">or</div>
|
|
592
|
+
<a class="google" href="/google">Continue with Google</a>
|
|
593
|
+
<small>LiveDesk Client</small>
|
|
594
|
+
</main>
|
|
595
|
+
</body>
|
|
596
|
+
</html>`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function normalizePairingPin(value) {
|
|
600
|
+
const pin = String(value || '').trim();
|
|
601
|
+
return /^\d{6}$/.test(pin) ? pin : '';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function readRequestBody(req, maxBytes = 4096) {
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
let body = '';
|
|
607
|
+
req.setEncoding('utf8');
|
|
608
|
+
req.on('data', chunk => {
|
|
609
|
+
body += chunk;
|
|
610
|
+
if (body.length > maxBytes) {
|
|
611
|
+
reject(new Error('Request body is too large.'));
|
|
612
|
+
req.destroy();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
req.on('end', () => resolve(body));
|
|
616
|
+
req.on('error', reject);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function resolveManagerFromPin(supabase, pin) {
|
|
621
|
+
const normalizedPin = normalizePairingPin(pin);
|
|
622
|
+
if (!normalizedPin) {
|
|
623
|
+
throw new Error('Enter a 6-digit LiveDesk PIN.');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const { data, error } = await supabase.rpc('resolve_livedesk_remote_host_pin', {
|
|
627
|
+
p_pairing_pin: normalizedPin
|
|
628
|
+
});
|
|
629
|
+
if (error) {
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
633
|
+
if (!result?.ok) {
|
|
634
|
+
throw new Error(result?.reason || 'pin-not-found');
|
|
635
|
+
}
|
|
636
|
+
if (!result.pair_token) {
|
|
637
|
+
throw new Error('The PIN matched a Hub record without a private connection token.');
|
|
638
|
+
}
|
|
639
|
+
const endpointCandidates = normalizeEndpointCandidates(result);
|
|
640
|
+
if (endpointCandidates.length === 0) {
|
|
641
|
+
throw new Error('The PIN matched a Hub record without a reachable address.');
|
|
642
|
+
}
|
|
643
|
+
const manager = await chooseReachableEndpoint(endpointCandidates, { requireReachable: true });
|
|
644
|
+
if (!manager) {
|
|
645
|
+
throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
manager,
|
|
650
|
+
pair: result.pair_token,
|
|
651
|
+
endpointCandidates
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function startConnectionChoiceServer(supabase, options = {}) {
|
|
432
656
|
const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
|
|
433
657
|
const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
434
658
|
let listeningPort = port;
|
|
435
659
|
let completed = false;
|
|
436
|
-
let
|
|
437
|
-
let
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
660
|
+
let completedTitle = 'LiveDesk connection complete';
|
|
661
|
+
let completedMessage = 'You can close this tab and return to the terminal.';
|
|
662
|
+
let settleChoice;
|
|
663
|
+
let rejectChoice;
|
|
664
|
+
const waitForChoice = new Promise((resolve, reject) => {
|
|
665
|
+
settleChoice = resolve;
|
|
666
|
+
rejectChoice = reject;
|
|
441
667
|
});
|
|
668
|
+
|
|
669
|
+
const complete = (choice, title = completedTitle, message = completedMessage) => {
|
|
670
|
+
completed = true;
|
|
671
|
+
completedTitle = title;
|
|
672
|
+
completedMessage = message;
|
|
673
|
+
settleChoice(choice);
|
|
674
|
+
setImmediate(() => server.close());
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const handleError = (res, error, pin = '') => {
|
|
678
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
679
|
+
res.end(renderConnectionChoicePage({
|
|
680
|
+
error: error instanceof Error ? error.message : String(error),
|
|
681
|
+
pin
|
|
682
|
+
}));
|
|
683
|
+
};
|
|
684
|
+
|
|
442
685
|
const server = createServer((req, res) => {
|
|
686
|
+
void (async () => {
|
|
443
687
|
const requestUrl = new URL(req.url || '/', `http://${host}:${listeningPort}`);
|
|
444
688
|
if (requestUrl.pathname === '/favicon.ico') {
|
|
445
689
|
res.writeHead(204);
|
|
@@ -449,42 +693,136 @@ async function startOAuthCallbackServer(options = {}) {
|
|
|
449
693
|
if (completed) {
|
|
450
694
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
451
695
|
res.end(renderOAuthCallbackPage({
|
|
452
|
-
title:
|
|
453
|
-
message:
|
|
696
|
+
title: completedTitle,
|
|
697
|
+
message: completedMessage
|
|
454
698
|
}));
|
|
455
699
|
return;
|
|
456
700
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
res.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
701
|
+
|
|
702
|
+
if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
|
|
703
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
704
|
+
res.end(renderConnectionChoicePage());
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (requestUrl.pathname === '/google') {
|
|
709
|
+
const { data: existing } = await supabase.auth.getSession();
|
|
710
|
+
if (existing?.session?.access_token) {
|
|
711
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
712
|
+
res.end(renderOAuthCallbackPage({
|
|
713
|
+
title: 'LiveDesk sign-in ready',
|
|
714
|
+
message: 'You can close this tab and return to the terminal.'
|
|
715
|
+
}));
|
|
716
|
+
complete({ type: 'google', session: existing.session }, 'LiveDesk sign-in ready');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
721
|
+
provider: 'google',
|
|
722
|
+
options: {
|
|
723
|
+
redirectTo: `http://${host}:${listeningPort}/callback`,
|
|
724
|
+
scopes: 'email profile'
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
if (error) {
|
|
728
|
+
handleError(res, error);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!data?.url) {
|
|
732
|
+
handleError(res, new Error('Supabase did not return a Google sign-in URL.'));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
res.writeHead(302, { Location: data.url });
|
|
736
|
+
res.end();
|
|
469
737
|
return;
|
|
470
738
|
}
|
|
471
|
-
|
|
472
|
-
|
|
739
|
+
|
|
740
|
+
if (requestUrl.pathname === '/pin') {
|
|
741
|
+
let pin = requestUrl.searchParams.get('pin') || '';
|
|
742
|
+
if (req.method !== 'GET') {
|
|
743
|
+
const body = await readRequestBody(req);
|
|
744
|
+
const params = new URLSearchParams(body);
|
|
745
|
+
pin = params.get('pin') || pin;
|
|
746
|
+
}
|
|
747
|
+
pin = normalizePairingPin(pin);
|
|
748
|
+
try {
|
|
749
|
+
const resolved = await resolveManagerFromPin(supabase, pin);
|
|
750
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
751
|
+
res.end(renderOAuthCallbackPage({
|
|
752
|
+
title: 'LiveDesk PIN accepted',
|
|
753
|
+
message: 'You can close this tab and return to the terminal.'
|
|
754
|
+
}));
|
|
755
|
+
complete({ type: 'pin', ...resolved }, 'LiveDesk PIN accepted');
|
|
756
|
+
} catch (err) {
|
|
757
|
+
handleError(res, err, pin);
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (requestUrl.pathname === '/callback') {
|
|
763
|
+
const code = requestUrl.searchParams.get('code');
|
|
764
|
+
const error = requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error');
|
|
765
|
+
if (error) {
|
|
766
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
767
|
+
res.end(renderOAuthCallbackPage({
|
|
768
|
+
title: 'LiveDesk sign-in failed',
|
|
769
|
+
message: error,
|
|
770
|
+
tone: 'error'
|
|
771
|
+
}));
|
|
772
|
+
completed = true;
|
|
773
|
+
rejectChoice(new Error(error));
|
|
774
|
+
server.close();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!code) {
|
|
778
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
779
|
+
res.end(renderConnectionChoicePage({
|
|
780
|
+
error: 'Google did not return an auth code. Try Google sign-in again.'
|
|
781
|
+
}));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const { data: sessionData, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
|
|
785
|
+
if (exchangeError || !sessionData?.session) {
|
|
786
|
+
const message = exchangeError || new Error('Google sign-in did not return a LiveDesk session.');
|
|
787
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
788
|
+
res.end(renderOAuthCallbackPage({
|
|
789
|
+
title: 'LiveDesk sign-in failed',
|
|
790
|
+
message: message instanceof Error ? message.message : String(message),
|
|
791
|
+
tone: 'error'
|
|
792
|
+
}));
|
|
793
|
+
completed = true;
|
|
794
|
+
rejectChoice(message instanceof Error ? message : new Error(String(message)));
|
|
795
|
+
server.close();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
473
799
|
res.end(renderOAuthCallbackPage({
|
|
474
|
-
title: '
|
|
475
|
-
message: '
|
|
476
|
-
tone: 'waiting'
|
|
800
|
+
title: 'LiveDesk sign-in complete',
|
|
801
|
+
message: 'You can close this tab and return to the terminal.'
|
|
477
802
|
}));
|
|
803
|
+
complete({ type: 'google', session: sessionData.session }, 'LiveDesk sign-in complete');
|
|
478
804
|
return;
|
|
479
805
|
}
|
|
480
|
-
|
|
806
|
+
|
|
807
|
+
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
808
|
+
res.end(renderConnectionChoicePage({
|
|
809
|
+
error: 'Unknown LiveDesk connection route.'
|
|
810
|
+
}));
|
|
811
|
+
})().catch(err => {
|
|
812
|
+
if (!res.headersSent) {
|
|
813
|
+
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
814
|
+
}
|
|
481
815
|
res.end(renderOAuthCallbackPage({
|
|
482
|
-
title: 'LiveDesk
|
|
483
|
-
message:
|
|
816
|
+
title: 'LiveDesk connection failed',
|
|
817
|
+
message: err instanceof Error ? err.message : String(err),
|
|
818
|
+
tone: 'error'
|
|
484
819
|
}));
|
|
485
|
-
completed
|
|
486
|
-
|
|
487
|
-
|
|
820
|
+
if (!completed) {
|
|
821
|
+
completed = true;
|
|
822
|
+
rejectChoice(err instanceof Error ? err : new Error(String(err)));
|
|
823
|
+
server.close();
|
|
824
|
+
}
|
|
825
|
+
});
|
|
488
826
|
});
|
|
489
827
|
try {
|
|
490
828
|
await new Promise((resolve, reject) => {
|
|
@@ -495,50 +833,23 @@ async function startOAuthCallbackServer(options = {}) {
|
|
|
495
833
|
listeningPort = typeof address === 'object' && address ? address.port : port;
|
|
496
834
|
} catch (err) {
|
|
497
835
|
if (err?.code === 'EADDRINUSE') {
|
|
498
|
-
throw new Error(`LiveDesk
|
|
836
|
+
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.`);
|
|
499
837
|
}
|
|
500
838
|
throw err;
|
|
501
839
|
}
|
|
502
840
|
return {
|
|
503
|
-
get
|
|
504
|
-
return `http://${host}:${listeningPort}
|
|
841
|
+
get url() {
|
|
842
|
+
return `http://${host}:${listeningPort}/`;
|
|
505
843
|
},
|
|
506
|
-
|
|
844
|
+
waitForChoice
|
|
507
845
|
};
|
|
508
846
|
}
|
|
509
847
|
|
|
510
|
-
async function
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const callback = await startOAuthCallbackServer({ authPort: options.authPort });
|
|
517
|
-
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
518
|
-
provider: 'google',
|
|
519
|
-
options: {
|
|
520
|
-
redirectTo: callback.redirectTo,
|
|
521
|
-
scopes: 'email profile'
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
if (error) {
|
|
525
|
-
throw error;
|
|
526
|
-
}
|
|
527
|
-
if (!data?.url) {
|
|
528
|
-
throw new Error('Supabase did not return a Google sign-in URL.');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
console.log('Opening Google sign-in for LiveDesk...');
|
|
532
|
-
openBrowser(data.url);
|
|
533
|
-
const code = await callback.waitForCode;
|
|
534
|
-
const { data: sessionData, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
|
|
535
|
-
if (exchangeError) {
|
|
536
|
-
throw exchangeError;
|
|
537
|
-
}
|
|
538
|
-
if (!sessionData?.session) {
|
|
539
|
-
throw new Error('Google sign-in did not return a LiveDesk session.');
|
|
540
|
-
}
|
|
541
|
-
return sessionData.session;
|
|
848
|
+
async function chooseClientConnection(supabase, options = {}) {
|
|
849
|
+
const connectionPage = await startConnectionChoiceServer(supabase, { authPort: options.authPort });
|
|
850
|
+
console.log('Opening LiveDesk connection page...');
|
|
851
|
+
openBrowser(connectionPage.url);
|
|
852
|
+
return connectionPage.waitForChoice;
|
|
542
853
|
}
|
|
543
854
|
|
|
544
855
|
function parseManagerEndpoint(value) {
|
|
@@ -665,12 +976,19 @@ async function prepareLoginConnection(parsed) {
|
|
|
665
976
|
|
|
666
977
|
if (shouldLogin) {
|
|
667
978
|
const supabase = await createSupabaseClient();
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
979
|
+
const choice = await chooseClientConnection(supabase, { authPort: parsed.authPort });
|
|
980
|
+
if (choice.type === 'pin') {
|
|
981
|
+
manager = choice.manager;
|
|
982
|
+
pair = choice.pair;
|
|
983
|
+
console.log('Connected by LiveDesk PIN.');
|
|
984
|
+
} else {
|
|
985
|
+
const session = choice.session;
|
|
986
|
+
const email = session?.user?.email ? ` as ${session.user.email}` : '';
|
|
987
|
+
console.log(`Signed in to LiveDesk${email}.`);
|
|
988
|
+
const resolved = await waitForManagerFromSupabase(supabase);
|
|
989
|
+
manager = resolved.manager;
|
|
990
|
+
pair = resolved.pair;
|
|
991
|
+
}
|
|
674
992
|
console.log(`Found LiveDesk Hub at ${manager}.`);
|
|
675
993
|
}
|
|
676
994
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|