@oxyhq/core 12.1.0 → 12.2.0

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.
Files changed (66) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/locales/ar-SA.json +27 -2
  3. package/dist/cjs/i18n/locales/ca-ES.json +27 -2
  4. package/dist/cjs/i18n/locales/de-DE.json +27 -2
  5. package/dist/cjs/i18n/locales/en-US.json +26 -3
  6. package/dist/cjs/i18n/locales/es-ES.json +26 -3
  7. package/dist/cjs/i18n/locales/fr-FR.json +27 -2
  8. package/dist/cjs/i18n/locales/it-IT.json +27 -2
  9. package/dist/cjs/i18n/locales/ja-JP.json +27 -2
  10. package/dist/cjs/i18n/locales/ko-KR.json +27 -2
  11. package/dist/cjs/i18n/locales/locales/ar-SA.json +27 -2
  12. package/dist/cjs/i18n/locales/locales/ca-ES.json +27 -2
  13. package/dist/cjs/i18n/locales/locales/de-DE.json +27 -2
  14. package/dist/cjs/i18n/locales/locales/en-US.json +26 -3
  15. package/dist/cjs/i18n/locales/locales/es-ES.json +26 -3
  16. package/dist/cjs/i18n/locales/locales/fr-FR.json +27 -2
  17. package/dist/cjs/i18n/locales/locales/it-IT.json +27 -2
  18. package/dist/cjs/i18n/locales/locales/ja-JP.json +27 -2
  19. package/dist/cjs/i18n/locales/locales/ko-KR.json +27 -2
  20. package/dist/cjs/i18n/locales/locales/pt-PT.json +27 -2
  21. package/dist/cjs/i18n/locales/locales/zh-CN.json +27 -2
  22. package/dist/cjs/i18n/locales/pt-PT.json +27 -2
  23. package/dist/cjs/i18n/locales/zh-CN.json +27 -2
  24. package/dist/cjs/session/accountDialogController.js +112 -2
  25. package/dist/esm/.tsbuildinfo +1 -1
  26. package/dist/esm/i18n/locales/ar-SA.json +27 -2
  27. package/dist/esm/i18n/locales/ca-ES.json +27 -2
  28. package/dist/esm/i18n/locales/de-DE.json +27 -2
  29. package/dist/esm/i18n/locales/en-US.json +26 -3
  30. package/dist/esm/i18n/locales/es-ES.json +26 -3
  31. package/dist/esm/i18n/locales/fr-FR.json +27 -2
  32. package/dist/esm/i18n/locales/it-IT.json +27 -2
  33. package/dist/esm/i18n/locales/ja-JP.json +27 -2
  34. package/dist/esm/i18n/locales/ko-KR.json +27 -2
  35. package/dist/esm/i18n/locales/locales/ar-SA.json +27 -2
  36. package/dist/esm/i18n/locales/locales/ca-ES.json +27 -2
  37. package/dist/esm/i18n/locales/locales/de-DE.json +27 -2
  38. package/dist/esm/i18n/locales/locales/en-US.json +26 -3
  39. package/dist/esm/i18n/locales/locales/es-ES.json +26 -3
  40. package/dist/esm/i18n/locales/locales/fr-FR.json +27 -2
  41. package/dist/esm/i18n/locales/locales/it-IT.json +27 -2
  42. package/dist/esm/i18n/locales/locales/ja-JP.json +27 -2
  43. package/dist/esm/i18n/locales/locales/ko-KR.json +27 -2
  44. package/dist/esm/i18n/locales/locales/pt-PT.json +27 -2
  45. package/dist/esm/i18n/locales/locales/zh-CN.json +27 -2
  46. package/dist/esm/i18n/locales/pt-PT.json +27 -2
  47. package/dist/esm/i18n/locales/zh-CN.json +27 -2
  48. package/dist/esm/session/accountDialogController.js +112 -2
  49. package/dist/types/.tsbuildinfo +1 -1
  50. package/dist/types/index.d.ts +1 -1
  51. package/dist/types/session/accountDialogController.d.ts +81 -1
  52. package/package.json +4 -4
  53. package/src/i18n/locales/ar-SA.json +27 -2
  54. package/src/i18n/locales/ca-ES.json +27 -2
  55. package/src/i18n/locales/de-DE.json +27 -2
  56. package/src/i18n/locales/en-US.json +26 -3
  57. package/src/i18n/locales/es-ES.json +26 -3
  58. package/src/i18n/locales/fr-FR.json +27 -2
  59. package/src/i18n/locales/it-IT.json +27 -2
  60. package/src/i18n/locales/ja-JP.json +27 -2
  61. package/src/i18n/locales/ko-KR.json +27 -2
  62. package/src/i18n/locales/pt-PT.json +27 -2
  63. package/src/i18n/locales/zh-CN.json +27 -2
  64. package/src/index.ts +1 -0
  65. package/src/session/__tests__/accountDialogController.test.ts +133 -1
  66. package/src/session/accountDialogController.ts +145 -2
@@ -24,6 +24,12 @@
24
24
  * - `commonsAvailability` — whether Commons is installed on this device
25
25
  * (native only, via the injected `canOpenApp` probe), so the QR view can
26
26
  * offer a "Get Commons" fallback instead of a same-device dead end.
27
+ * - `startPasskeyHubSignIn` — on a non-Oxy web origin (where a WebAuthn
28
+ * ceremony can't run locally, the RP-ID is bound to oxy.so), open a popup
29
+ * at the auth.oxy.so passkey hub instead, scoped to the SAME device-flow
30
+ * session `showQr` creates, and complete it via the SAME poll/socket/claim
31
+ * engine. Falls back to `showQr`'s plain QR rendering if the popup is
32
+ * blocked.
27
33
  *
28
34
  * Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
29
35
  * password, social login, and 2FA were removed ecosystem-wide. Account
@@ -32,6 +38,7 @@
32
38
  */
33
39
  import { logger } from '../logger/index.js';
34
40
  import { extractErrorStatus } from '../utils/errorUtils.js';
41
+ import { CENTRAL_IDP_APEX } from '../utils/authWebUrl.js';
35
42
  import { projectSwitchableAccounts, switchableAccountIds, } from './accountProjection.js';
36
43
  /**
37
44
  * Slow FALLBACK poll cadence for the QR flow. The `/auth-session` socket delivers
@@ -86,6 +93,10 @@ export class AccountDialogController {
86
93
  * single-use token concurrently.
87
94
  */
88
95
  this.pollInFlight = false;
96
+ /** The popup opened by {@link startPasskeyHubSignIn}, or `null`. */
97
+ this.activePopup = null;
98
+ /** Watches {@link activePopup}'s `closed` state; see {@link watchPopup}. */
99
+ this.popupWatchTimer = null;
89
100
  // --- Store plumbing ---
90
101
  this.unsubscribeSession = null;
91
102
  this.unsubscribeTokens = null;
@@ -104,6 +115,8 @@ export class AccountDialogController {
104
115
  this.openUrl = options.openUrl;
105
116
  this.canOpenApp = options.canOpenApp;
106
117
  this.socketFactory = options.socketFactory;
118
+ this.openPopup = options.openPopup;
119
+ this.hubBaseUrl = options.hubBaseUrl ?? `https://auth.${CENTRAL_IDP_APEX}`;
107
120
  this.snapshot = this.computeSnapshot();
108
121
  }
109
122
  // =========================================================================
@@ -177,6 +190,7 @@ export class AccountDialogController {
177
190
  }
178
191
  this.clearPollTimer();
179
192
  this.closeAuthSessionSocket();
193
+ this.closeActivePopup();
180
194
  this.signInToken = null;
181
195
  this.listeners.clear();
182
196
  }
@@ -444,9 +458,62 @@ export class AccountDialogController {
444
458
  async showQr() {
445
459
  this.cancelSignIn();
446
460
  this.setView('qr');
461
+ await this.startDeviceFlowSession();
462
+ }
463
+ /**
464
+ * Web-only: "Sign in with a passkey" on a non-Oxy origin cannot run the
465
+ * WebAuthn ceremony locally — a credential minted with `WEBAUTHN_RP_ID=oxy.so`
466
+ * can only be asserted from `oxy.so`/a subdomain/loopback, a browser-enforced
467
+ * boundary `isOxyRpOrigin()` (consumer-side) already gates on. Instead, open
468
+ * a popup at the auth.oxy.so passkey hub, scoped to the SAME device-flow
469
+ * session {@link showQr} would create (same `authorizeCode`/`sessionToken`
470
+ * pair), and let the SAME poll/socket/claim engine complete it once the hub
471
+ * authorizes the session (`POST /auth/session/authorize-code/:authorizeCode`,
472
+ * bearer-authed, server-side — this method never sees or transmits the
473
+ * secret `sessionToken`).
474
+ *
475
+ * The popup MUST be opened synchronously, before any `await`, or the
476
+ * browser's popup blocker loses the click-gesture attribution and silently
477
+ * blocks it — so {@link openPopup} is invoked as the very first step, before
478
+ * the device-flow session (which needs an async call) even exists. If the
479
+ * popup is blocked (or no `openPopup` was injected), this falls back to
480
+ * `showQr`'s plain QR rendering — no dead end.
481
+ */
482
+ async startPasskeyHubSignIn() {
483
+ this.cancelSignIn();
484
+ this.setView('qr');
485
+ const popup = this.openPopup?.() ?? null;
486
+ if (!popup) {
487
+ await this.startDeviceFlowSession();
488
+ return;
489
+ }
490
+ const handle = await this.startDeviceFlowSession();
491
+ if (!handle) {
492
+ popup.close();
493
+ return;
494
+ }
495
+ // The user may have closed the popup during the async session creation
496
+ // above, before any watcher was attached to catch it — guard rather than
497
+ // navigate a dead window (browsers vary on whether that throws).
498
+ if (popup.closed) {
499
+ this.failSignIn('Sign-in was cancelled.');
500
+ return;
501
+ }
502
+ this.activePopup = popup;
503
+ popup.location.href = `${this.hubBaseUrl}/hub-passkey?code=${encodeURIComponent(handle.authorizeCode)}`;
504
+ this.watchPopup(popup);
505
+ }
506
+ /**
507
+ * Shared device-flow session creation for both {@link showQr} (renders the
508
+ * QR) and {@link startPasskeyHubSignIn} (also opens the hub popup) — the
509
+ * same `startCommonsSignIn` → poll/socket wiring either way. Returns the
510
+ * handle on success (already reflected in `signIn`), or `null` on failure
511
+ * (already set as `signIn.error`).
512
+ */
513
+ async startDeviceFlowSession() {
447
514
  if (!this.clientId) {
448
515
  this.setSignIn({ ...IDLE_SIGN_IN, phase: 'error', error: 'This app is not configured for sign-in (missing clientId).' });
449
- return;
516
+ return null;
450
517
  }
451
518
  this.setSignIn({ ...IDLE_SIGN_IN, phase: 'starting' });
452
519
  try {
@@ -471,9 +538,49 @@ export class AccountDialogController {
471
538
  // stay live as the fallback, so a user who dismisses the app-open still
472
539
  // completes the sign-in by scanning.
473
540
  void this.deepLinkIntoCommonsIfAvailable(handle.qrPayload);
541
+ return handle;
474
542
  }
475
543
  catch (error) {
476
544
  this.setSignIn({ ...IDLE_SIGN_IN, phase: 'error', error: errorMessage(error) });
545
+ return null;
546
+ }
547
+ }
548
+ /**
549
+ * Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
550
+ * popup without completing sign-in gets prompt feedback (there is no DOM
551
+ * event for a cross-origin popup closing — polling is the only mechanism).
552
+ * Cleared by {@link closeActivePopup}, which every sign-in exit path already
553
+ * calls.
554
+ */
555
+ watchPopup(popup) {
556
+ this.clearPopupWatchTimer();
557
+ this.popupWatchTimer = setInterval(() => {
558
+ if (!popup.closed)
559
+ return;
560
+ this.clearPopupWatchTimer();
561
+ if (this.signIn.phase === 'starting' || this.signIn.phase === 'waiting') {
562
+ this.failSignIn('Sign-in was cancelled.');
563
+ }
564
+ }, 1000);
565
+ }
566
+ clearPopupWatchTimer() {
567
+ if (this.popupWatchTimer !== null) {
568
+ clearInterval(this.popupWatchTimer);
569
+ this.popupWatchTimer = null;
570
+ }
571
+ }
572
+ /** Tear down the popup watch timer and close the popup, if any. Idempotent. */
573
+ closeActivePopup() {
574
+ this.clearPopupWatchTimer();
575
+ const popup = this.activePopup;
576
+ this.activePopup = null;
577
+ if (!popup || popup.closed)
578
+ return;
579
+ try {
580
+ popup.close();
581
+ }
582
+ catch (error) {
583
+ logger.debug('[AccountDialogController] popup close failed', { component: 'AccountDialogController' }, error);
477
584
  }
478
585
  }
479
586
  /**
@@ -523,10 +630,11 @@ export class AccountDialogController {
523
630
  this.openUrl(qrPayload);
524
631
  }
525
632
  }
526
- /** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
633
+ /** Tear down the active sign-in device flow (timers + socket + popup + token) and reset to idle. */
527
634
  cancelSignIn() {
528
635
  this.clearPollTimer();
529
636
  this.closeAuthSessionSocket();
637
+ this.closeActivePopup();
530
638
  this.signInToken = null;
531
639
  if (this.signIn !== IDLE_SIGN_IN) {
532
640
  this.setSignIn(IDLE_SIGN_IN);
@@ -635,6 +743,7 @@ export class AccountDialogController {
635
743
  this.signInToken = null;
636
744
  this.clearPollTimer();
637
745
  this.closeAuthSessionSocket();
746
+ this.closeActivePopup();
638
747
  this.signIn = IDLE_SIGN_IN;
639
748
  this.view = 'accounts';
640
749
  this.emit();
@@ -666,6 +775,7 @@ export class AccountDialogController {
666
775
  failSignIn(message) {
667
776
  this.clearPollTimer();
668
777
  this.closeAuthSessionSocket();
778
+ this.closeActivePopup();
669
779
  this.signInToken = null;
670
780
  this.setSignIn({ ...IDLE_SIGN_IN, phase: 'error', error: message });
671
781
  }