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