@livedesk/client 0.1.48 → 0.1.50

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.
@@ -494,6 +494,8 @@ function createFakeThumbnailFrame(options, payload, frameSeq) {
494
494
  frameSeq,
495
495
  width,
496
496
  height,
497
+ sourceWidth: width,
498
+ sourceHeight: height,
497
499
  mimeType: 'image/svg+xml',
498
500
  data: Buffer.from(svg, 'utf8').toString('base64'),
499
501
  capturedAt: now
@@ -564,6 +566,8 @@ async function captureNativeDesktopFrame(frameSeq, monitorIndex = 0) {
564
566
  frameSeq,
565
567
  width: dimensions.width,
566
568
  height: dimensions.height,
569
+ sourceWidth: dimensions.width,
570
+ sourceHeight: dimensions.height,
567
571
  mimeType: 'image/jpeg',
568
572
  data: buffer.toString('base64'),
569
573
  monitorIndex: selectedIndex,
@@ -758,6 +762,8 @@ function startLiveStream(socket, options, message, nextFrameSeq, activeStreams)
758
762
  frameSeq: frame.frameSeq,
759
763
  width: frame.width,
760
764
  height: frame.height,
765
+ sourceWidth: frame.sourceWidth || frame.width,
766
+ sourceHeight: frame.sourceHeight || frame.height,
761
767
  mimeType: frame.mimeType,
762
768
  capturedAt: frame.capturedAt,
763
769
  fps,
@@ -898,6 +904,8 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
898
904
  frameSeq: frame.frameSeq,
899
905
  width: frame.width,
900
906
  height: frame.height,
907
+ sourceWidth: frame.sourceWidth || frame.width,
908
+ sourceHeight: frame.sourceHeight || frame.height,
901
909
  mimeType: frame.mimeType,
902
910
  capturedAt: frame.capturedAt,
903
911
  data: frame.data
@@ -910,6 +918,8 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
910
918
  frameSeq: frame.frameSeq,
911
919
  width: frame.width,
912
920
  height: frame.height,
921
+ sourceWidth: frame.sourceWidth || frame.width,
922
+ sourceHeight: frame.sourceHeight || frame.height,
913
923
  capturedAt: frame.capturedAt
914
924
  }
915
925
  });
@@ -35,7 +35,7 @@ Usage:
35
35
  npx @livedesk/client 3
36
36
 
37
37
  Default flow:
38
- Opens Google sign-in, waits for the signed-in LiveDesk Hub, and connects locally.
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
- async function startOAuthCallbackServer(options = {}) {
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 settleCode;
437
- let rejectCode;
438
- const waitForCode = new Promise((resolve, reject) => {
439
- settleCode = resolve;
440
- rejectCode = reject;
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: 'LiveDesk sign-in complete',
453
- message: 'You can close this tab and return to the terminal.'
696
+ title: completedTitle,
697
+ message: completedMessage
454
698
  }));
455
699
  return;
456
700
  }
457
- const code = requestUrl.searchParams.get('code');
458
- const error = requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error');
459
- if (error) {
460
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
461
- res.end(renderOAuthCallbackPage({
462
- title: 'LiveDesk sign-in failed',
463
- message: error,
464
- tone: 'error'
465
- }));
466
- completed = true;
467
- rejectCode(new Error(error));
468
- server.close();
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
- if (!code) {
472
- res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
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: 'Waiting for LiveDesk sign-in',
475
- message: 'This local callback is waiting for an auth code from Google.',
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
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
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 sign-in complete',
483
- message: 'You can close this tab and return to the terminal.'
816
+ title: 'LiveDesk connection failed',
817
+ message: err instanceof Error ? err.message : String(err),
818
+ tone: 'error'
484
819
  }));
485
- completed = true;
486
- settleCode(code);
487
- server.close();
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 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.`);
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 redirectTo() {
504
- return `http://${host}:${listeningPort}/callback`;
841
+ get url() {
842
+ return `http://${host}:${listeningPort}/`;
505
843
  },
506
- waitForCode
844
+ waitForChoice
507
845
  };
508
846
  }
509
847
 
510
- async function signInWithGoogle(supabase, options = {}) {
511
- const { data: existing } = await supabase.auth.getSession();
512
- if (existing?.session?.access_token) {
513
- return existing.session;
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 session = await signInWithGoogle(supabase, { authPort: parsed.authPort });
669
- const email = session.user?.email ? ` as ${session.user.email}` : '';
670
- console.log(`Signed in to LiveDesk${email}.`);
671
- const resolved = await waitForManagerFromSupabase(supabase);
672
- manager = resolved.manager;
673
- pair = resolved.pair;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {