@oxy.so/core 1.12.1 → 1.16.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/crypto/webIdentityCarrier.js +1 -1
- package/dist/cjs/i18n/locales/ar-SA.json +33 -88
- package/dist/cjs/i18n/locales/ca-ES.json +35 -90
- package/dist/cjs/i18n/locales/de-DE.json +33 -88
- package/dist/cjs/i18n/locales/en-US.json +45 -141
- package/dist/cjs/i18n/locales/es-ES.json +44 -140
- package/dist/cjs/i18n/locales/fr-FR.json +33 -88
- package/dist/cjs/i18n/locales/it-IT.json +33 -88
- package/dist/cjs/i18n/locales/ja-JP.json +33 -88
- package/dist/cjs/i18n/locales/ko-KR.json +33 -88
- package/dist/cjs/i18n/locales/locales/ar-SA.json +33 -88
- package/dist/cjs/i18n/locales/locales/ca-ES.json +35 -90
- package/dist/cjs/i18n/locales/locales/de-DE.json +33 -88
- package/dist/cjs/i18n/locales/locales/en-US.json +45 -141
- package/dist/cjs/i18n/locales/locales/es-ES.json +44 -140
- package/dist/cjs/i18n/locales/locales/fr-FR.json +33 -88
- package/dist/cjs/i18n/locales/locales/it-IT.json +33 -88
- package/dist/cjs/i18n/locales/locales/ja-JP.json +33 -88
- package/dist/cjs/i18n/locales/locales/ko-KR.json +33 -88
- package/dist/cjs/i18n/locales/locales/pt-PT.json +33 -88
- package/dist/cjs/i18n/locales/locales/zh-CN.json +34 -89
- package/dist/cjs/i18n/locales/pt-PT.json +33 -88
- package/dist/cjs/i18n/locales/zh-CN.json +34 -89
- package/dist/cjs/index.js +10 -7
- package/dist/cjs/mixins/OxyServices.utility.js +140 -0
- package/dist/cjs/server/securityHeaders.js +19 -1
- package/dist/cjs/session/accountDialogController.js +37 -142
- package/dist/cjs/utils/authWebUrl.js +7 -6
- package/dist/cjs/utils/oauthPkce.js +5 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/webIdentityCarrier.js +1 -1
- package/dist/esm/i18n/locales/ar-SA.json +33 -88
- package/dist/esm/i18n/locales/ca-ES.json +35 -90
- package/dist/esm/i18n/locales/de-DE.json +33 -88
- package/dist/esm/i18n/locales/en-US.json +45 -141
- package/dist/esm/i18n/locales/es-ES.json +44 -140
- package/dist/esm/i18n/locales/fr-FR.json +33 -88
- package/dist/esm/i18n/locales/it-IT.json +33 -88
- package/dist/esm/i18n/locales/ja-JP.json +33 -88
- package/dist/esm/i18n/locales/ko-KR.json +33 -88
- package/dist/esm/i18n/locales/locales/ar-SA.json +33 -88
- package/dist/esm/i18n/locales/locales/ca-ES.json +35 -90
- package/dist/esm/i18n/locales/locales/de-DE.json +33 -88
- package/dist/esm/i18n/locales/locales/en-US.json +45 -141
- package/dist/esm/i18n/locales/locales/es-ES.json +44 -140
- package/dist/esm/i18n/locales/locales/fr-FR.json +33 -88
- package/dist/esm/i18n/locales/locales/it-IT.json +33 -88
- package/dist/esm/i18n/locales/locales/ja-JP.json +33 -88
- package/dist/esm/i18n/locales/locales/ko-KR.json +33 -88
- package/dist/esm/i18n/locales/locales/pt-PT.json +33 -88
- package/dist/esm/i18n/locales/locales/zh-CN.json +34 -89
- package/dist/esm/i18n/locales/pt-PT.json +33 -88
- package/dist/esm/i18n/locales/zh-CN.json +34 -89
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.utility.js +138 -0
- package/dist/esm/server/securityHeaders.js +19 -1
- package/dist/esm/session/accountDialogController.js +37 -142
- package/dist/esm/utils/authWebUrl.js +6 -5
- package/dist/esm/utils/oauthPkce.js +5 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/webIdentityCarrier.d.ts +1 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/mixins/OxyServices.utility.d.ts +84 -1
- package/dist/types/server/requesterAssertion.d.ts +2 -2
- package/dist/types/server/securityHeaders.d.ts +9 -12
- package/dist/types/session/accountDialogController.d.ts +29 -98
- package/dist/types/utils/authWebUrl.d.ts +6 -5
- package/dist/types/utils/oauthPkce.d.ts +10 -0
- package/package.json +2 -2
- package/src/crypto/webIdentityCarrier.ts +1 -1
- package/src/i18n/locales/ar-SA.json +33 -88
- package/src/i18n/locales/ca-ES.json +35 -90
- package/src/i18n/locales/de-DE.json +33 -88
- package/src/i18n/locales/en-US.json +45 -141
- package/src/i18n/locales/es-ES.json +44 -140
- package/src/i18n/locales/fr-FR.json +33 -88
- package/src/i18n/locales/it-IT.json +33 -88
- package/src/i18n/locales/ja-JP.json +33 -88
- package/src/i18n/locales/ko-KR.json +33 -88
- package/src/i18n/locales/pt-PT.json +33 -88
- package/src/i18n/locales/zh-CN.json +34 -89
- package/src/index.ts +9 -3
- package/src/mixins/OxyServices.utility.ts +208 -0
- package/src/mixins/__tests__/accountEvents.test.ts +158 -0
- package/src/server/__tests__/securityHeaders.test.ts +17 -0
- package/src/server/securityHeaders.ts +29 -1
- package/src/session/__tests__/accountDialogController.test.ts +42 -184
- package/src/session/accountDialogController.ts +50 -185
- package/src/utils/__tests__/oauthPkce.test.ts +7 -0
- package/src/utils/authWebUrl.ts +6 -5
- package/src/utils/oauthPkce.ts +16 -1
|
@@ -313,6 +313,20 @@ const MAX_INLINE_SCRIPT_HASHES = 8;
|
|
|
313
313
|
* react-native-web's runtime stylesheet, and a style hash would silently switch
|
|
314
314
|
* that off and render every Oxy web app unstyled.)
|
|
315
315
|
*/
|
|
316
|
+
/** Passkeys for this origin itself; every other powerful feature off. */
|
|
317
|
+
const SENSITIVE_PERMISSIONS_POLICY = [
|
|
318
|
+
'publickey-credentials-get=(self)',
|
|
319
|
+
'publickey-credentials-create=(self)',
|
|
320
|
+
'camera=()',
|
|
321
|
+
'microphone=()',
|
|
322
|
+
'geolocation=()',
|
|
323
|
+
'payment=()',
|
|
324
|
+
'usb=()',
|
|
325
|
+
'serial=()',
|
|
326
|
+
'hid=()',
|
|
327
|
+
'bluetooth=()',
|
|
328
|
+
'display-capture=()',
|
|
329
|
+
].join(', ');
|
|
316
330
|
export function buildOxyPagesHeaders(options = {}) {
|
|
317
331
|
const hashes = [
|
|
318
332
|
...new Set((options.html ?? []).flatMap(extractInlineScripts).map(inlineScriptCspHash)),
|
|
@@ -328,6 +342,8 @@ export function buildOxyPagesHeaders(options = {}) {
|
|
|
328
342
|
for (const name of ['script-src', 'connect-src']) {
|
|
329
343
|
directives[name] = (directives[name] ?? []).filter((source) => !beacon.has(source));
|
|
330
344
|
}
|
|
345
|
+
directives['base-uri'] = ["'none'"];
|
|
346
|
+
directives['form-action'] = ["'none'"];
|
|
331
347
|
}
|
|
332
348
|
const csp = formatOxyCspPolicy(directives);
|
|
333
349
|
const lines = [
|
|
@@ -335,13 +351,15 @@ export function buildOxyPagesHeaders(options = {}) {
|
|
|
335
351
|
` Content-Security-Policy: ${csp}`,
|
|
336
352
|
' X-Frame-Options: DENY',
|
|
337
353
|
' X-Content-Type-Options: nosniff',
|
|
338
|
-
|
|
354
|
+
` Referrer-Policy: ${options.sensitive ? 'no-referrer' : 'strict-origin-when-cross-origin'}`,
|
|
339
355
|
];
|
|
340
356
|
if (options.hsts !== false) {
|
|
341
357
|
lines.push(' Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
|
|
342
358
|
}
|
|
343
359
|
if (options.sensitive) {
|
|
344
360
|
lines.push(' Cache-Control: public, max-age=0, must-revalidate, no-transform');
|
|
361
|
+
lines.push(` Permissions-Policy: ${SENSITIVE_PERMISSIONS_POLICY}`);
|
|
362
|
+
lines.push(' Cross-Origin-Resource-Policy: same-origin');
|
|
345
363
|
}
|
|
346
364
|
lines.push('');
|
|
347
365
|
return lines.join('\n');
|
|
@@ -36,12 +36,6 @@
|
|
|
36
36
|
* - `commonsAvailability` — whether Commons is installed on this device
|
|
37
37
|
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
38
38
|
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
39
|
-
* - `startPasskeyHubSignIn` — on a non-Oxy web origin (where a WebAuthn
|
|
40
|
-
* ceremony can't run locally, the RP-ID is bound to oxy.so), open a popup
|
|
41
|
-
* at the auth.oxy.so passkey hub instead, scoped to the SAME device-flow
|
|
42
|
-
* session `showQr` creates, and complete it via the SAME poll/socket/claim
|
|
43
|
-
* engine. Falls back to `showQr`'s plain QR rendering if the popup is
|
|
44
|
-
* blocked.
|
|
45
39
|
*
|
|
46
40
|
* Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
|
|
47
41
|
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
@@ -57,7 +51,6 @@
|
|
|
57
51
|
*/
|
|
58
52
|
import { logger } from '../logger/index.js';
|
|
59
53
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
60
|
-
import { IDENTITY_WEB_ORIGIN } from '../utils/authWebUrl.js';
|
|
61
54
|
import { getSocketIO } from './socketLoader.js';
|
|
62
55
|
import { resolveActiveContext } from './deviceDirectory.js';
|
|
63
56
|
import { pushTargetsFromDelivery, selectCommonsDelivery, } from '../utils/commonsDelivery.js';
|
|
@@ -125,8 +118,8 @@ const IDLE_SIGN_IN_FACTS = {
|
|
|
125
118
|
*/
|
|
126
119
|
const COMPLETED_SIGN_IN_FACTS = { ...IDLE_SIGN_IN_FACTS, phase: 'completed' };
|
|
127
120
|
/** The full flow state for `facts`, stamped with the attempt it belongs to. */
|
|
128
|
-
function buildSignIn(facts, attempt) {
|
|
129
|
-
return { ...facts, progress: deriveSignInProgress(facts), attempt };
|
|
121
|
+
function buildSignIn(facts, attempt, inline) {
|
|
122
|
+
return { ...facts, progress: deriveSignInProgress(facts), attempt, inline };
|
|
130
123
|
}
|
|
131
124
|
function errorMessage(error) {
|
|
132
125
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -158,7 +151,7 @@ export class AccountDialogController {
|
|
|
158
151
|
* alongside the three flags above.
|
|
159
152
|
*/
|
|
160
153
|
this.exclusiveMutationInFlight = false;
|
|
161
|
-
this.signIn = buildSignIn(IDLE_SIGN_IN_FACTS, 0);
|
|
154
|
+
this.signIn = buildSignIn(IDLE_SIGN_IN_FACTS, 0, false);
|
|
162
155
|
this.commonsAvailability = 'unknown';
|
|
163
156
|
// --- Sign-in device-flow bookkeeping ---
|
|
164
157
|
/**
|
|
@@ -187,10 +180,6 @@ export class AccountDialogController {
|
|
|
187
180
|
* ABANDONED request never blocks the next request's first poll.
|
|
188
181
|
*/
|
|
189
182
|
this.pollInFlightToken = null;
|
|
190
|
-
/** The popup opened by {@link startPasskeyHubSignIn}, or `null`. */
|
|
191
|
-
this.activePopup = null;
|
|
192
|
-
/** Watches {@link activePopup}'s `closed` state; see {@link watchPopup}. */
|
|
193
|
-
this.popupWatchTimer = null;
|
|
194
183
|
// --- Store plumbing ---
|
|
195
184
|
this.unsubscribeSession = null;
|
|
196
185
|
this.unsubscribeTokens = null;
|
|
@@ -207,8 +196,6 @@ export class AccountDialogController {
|
|
|
207
196
|
this.openUrl = options.openUrl;
|
|
208
197
|
this.canOpenApp = options.canOpenApp;
|
|
209
198
|
this.socketFactory = options.socketFactory;
|
|
210
|
-
this.openPopup = options.openPopup;
|
|
211
|
-
this.identityOrigin = options.identityOrigin ?? options.hubBaseUrl ?? IDENTITY_WEB_ORIGIN;
|
|
212
199
|
this.platform = options.platform ?? 'unknown';
|
|
213
200
|
this.snapshot = this.computeSnapshot();
|
|
214
201
|
}
|
|
@@ -286,7 +273,6 @@ export class AccountDialogController {
|
|
|
286
273
|
}
|
|
287
274
|
this.clearPollTimer();
|
|
288
275
|
this.closeAuthSessionSocket();
|
|
289
|
-
this.closeActivePopup();
|
|
290
276
|
this.signInToken = null;
|
|
291
277
|
this.listeners.clear();
|
|
292
278
|
}
|
|
@@ -351,12 +337,12 @@ export class AccountDialogController {
|
|
|
351
337
|
if (this.view === view)
|
|
352
338
|
return;
|
|
353
339
|
this.view = view;
|
|
354
|
-
// A `'completed'` flow owns no timers, socket,
|
|
340
|
+
// A `'completed'` flow owns no timers, socket, or token — it is only
|
|
355
341
|
// the terminal "Identity confirmed" the finished surface showed. Moving to
|
|
356
342
|
// another view is a NEW intention, so drop it; otherwise a later `add()`
|
|
357
343
|
// would open on the previous sign-in's terminal state.
|
|
358
344
|
if (this.signIn.phase === 'completed') {
|
|
359
|
-
this.signIn =
|
|
345
|
+
this.signIn = this.stampSignIn(IDLE_SIGN_IN_FACTS);
|
|
360
346
|
}
|
|
361
347
|
this.emit();
|
|
362
348
|
}
|
|
@@ -673,82 +659,33 @@ export class AccountDialogController {
|
|
|
673
659
|
await this.startDeviceFlowSession(attempt, { deliver: true });
|
|
674
660
|
}
|
|
675
661
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* passkey-hub attempt reopens the hub popup; a failed "Sign in with Oxy"
|
|
679
|
-
* retries that; an explicit QR request shows the QR.
|
|
662
|
+
* The sign-in entry's EMBEDDED QR: a request whose only route is the QR,
|
|
663
|
+
* started without leaving the current view.
|
|
680
664
|
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
665
|
+
* Unlike {@link showQr} it runs no delivery selection. The surface starts it
|
|
666
|
+
* by itself, on mount, so it must not push to the person's phone or open
|
|
667
|
+
* Commons — nobody asked for either. Any other sign-in the person then
|
|
668
|
+
* chooses supersedes it (and withdraws its request) like any new attempt.
|
|
669
|
+
*/
|
|
670
|
+
async startInlineQr() {
|
|
671
|
+
const attempt = this.beginSignInAttempt('inline-qr');
|
|
672
|
+
await this.startDeviceFlowSession(attempt, { deliver: false });
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Start a NEW attempt the same way the last one was started — "Try again"
|
|
676
|
+
* repeats the user's choice rather than silently switching method: a failed
|
|
677
|
+
* "Sign in with Oxy" retries that; an explicit QR request shows the QR.
|
|
684
678
|
*/
|
|
685
679
|
retrySignIn() {
|
|
686
680
|
switch (this.signInMethod) {
|
|
687
|
-
case 'passkey-hub':
|
|
688
|
-
return this.startPasskeyHubSignIn();
|
|
689
681
|
case 'qr':
|
|
690
682
|
return this.showQr();
|
|
683
|
+
case 'inline-qr':
|
|
684
|
+
return this.startInlineQr();
|
|
691
685
|
default:
|
|
692
686
|
return this.signInWithOxy();
|
|
693
687
|
}
|
|
694
688
|
}
|
|
695
|
-
/**
|
|
696
|
-
* Web-only: sign in (or create an account) with a passkey at the web identity
|
|
697
|
-
* carrier. The ceremony runs on `id.oxy.so`, never on the calling origin, for
|
|
698
|
-
* two reasons: a credential minted with `WEBAUTHN_RP_ID=oxy.so` can only be
|
|
699
|
-
* asserted from `oxy.so`/a subdomain/loopback (a browser-enforced boundary),
|
|
700
|
-
* and only the identity origin may unseal or create the account's identity
|
|
701
|
-
* (one identity, two carriers). Opens a popup at `<identityOrigin>/continue`,
|
|
702
|
-
* scoped to the SAME device-flow
|
|
703
|
-
* session {@link showQr} would create (same `authorizeCode`/`sessionToken`
|
|
704
|
-
* pair), and let the SAME poll/socket/claim engine complete it once the hub
|
|
705
|
-
* authorizes the session (`POST /auth/session/authorize-code/:authorizeCode`,
|
|
706
|
-
* bearer-authed, server-side — this method never sees or transmits the
|
|
707
|
-
* secret `sessionToken`).
|
|
708
|
-
*
|
|
709
|
-
* The popup MUST be opened synchronously, before any `await`, or the
|
|
710
|
-
* browser's popup blocker loses the click-gesture attribution and silently
|
|
711
|
-
* blocks it — so {@link openPopup} is invoked as the very first step, before
|
|
712
|
-
* the device-flow session (which needs an async call) even exists. If the
|
|
713
|
-
* popup is blocked (or no `openPopup` was injected), this falls back to
|
|
714
|
-
* `showQr`'s plain QR rendering — no dead end.
|
|
715
|
-
*/
|
|
716
|
-
async startPasskeyHubSignIn() {
|
|
717
|
-
const attempt = this.beginSignInAttempt('passkey-hub');
|
|
718
|
-
this.setView('qr');
|
|
719
|
-
const popup = this.openPopup?.() ?? null;
|
|
720
|
-
if (!popup) {
|
|
721
|
-
await this.startDeviceFlowSession(attempt, { deliver: true });
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
// Owned by the attempt from the moment it exists, so cancelling while the
|
|
725
|
-
// session is still being created closes it instead of leaving a blank
|
|
726
|
-
// window behind.
|
|
727
|
-
this.activePopup = popup;
|
|
728
|
-
// The hub popup IS the primary surface here, chosen explicitly by the user —
|
|
729
|
-
// so this flow does NOT run automatic Commons delivery (ringing the user's
|
|
730
|
-
// phone because they asked for a passkey would be exactly the "menu of
|
|
731
|
-
// methods" the one-primary-action rule forbids). The underlying request is
|
|
732
|
-
// the same `AuthSession`, and its Commons route stays the QR the view
|
|
733
|
-
// renders beneath the popup.
|
|
734
|
-
const handle = await this.startDeviceFlowSession(attempt, { deliver: false });
|
|
735
|
-
// `null` means the attempt failed (its teardown closed the popup) or was
|
|
736
|
-
// abandoned (the cancel closed it).
|
|
737
|
-
if (!handle)
|
|
738
|
-
return;
|
|
739
|
-
// The user may have closed the popup during the async session creation
|
|
740
|
-
// above, before any watcher was attached to catch it — guard rather than
|
|
741
|
-
// navigate a dead window (browsers vary on whether that throws).
|
|
742
|
-
if (popup.closed) {
|
|
743
|
-
const pendingCode = this.signIn.authorizeCode;
|
|
744
|
-
this.failSignIn('cancelled', 'Sign-in was cancelled.');
|
|
745
|
-
if (pendingCode)
|
|
746
|
-
void this.withdrawRequest(pendingCode);
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
popup.location.href = `${this.identityOrigin}/continue?code=${encodeURIComponent(handle.authorizeCode)}`;
|
|
750
|
-
this.watchPopup(popup);
|
|
751
|
-
}
|
|
752
689
|
/**
|
|
753
690
|
* End whatever attempt is running (withdrawing its request) and begin a new
|
|
754
691
|
* one. Returns the new attempt's identity for the caller to re-check after
|
|
@@ -764,9 +701,9 @@ export class AccountDialogController {
|
|
|
764
701
|
return attempt === this.signInAttempt;
|
|
765
702
|
}
|
|
766
703
|
/**
|
|
767
|
-
* Shared device-flow session creation for
|
|
768
|
-
*
|
|
769
|
-
*
|
|
704
|
+
* Shared device-flow session creation for {@link signInWithOxy},
|
|
705
|
+
* {@link showQr} and {@link startInlineQr} — the same `startCommonsSignIn` →
|
|
706
|
+
* poll/socket wiring every time. Returns the
|
|
770
707
|
* handle on success (already reflected in `signIn`), or `null` when the
|
|
771
708
|
* attempt failed (already set as `signIn.failure`) or was abandoned while the
|
|
772
709
|
* request was being created.
|
|
@@ -777,8 +714,8 @@ export class AccountDialogController {
|
|
|
777
714
|
* request, a socket, and a poll behind it.
|
|
778
715
|
* @param opts.deliver - Whether to run automatic Commons delivery selection
|
|
779
716
|
* ({@link resolveDeliveryRoute}). `true` for the normal one-primary-action
|
|
780
|
-
* entry; `false`
|
|
781
|
-
*
|
|
717
|
+
* entry; `false` for the embedded QR, which the screen starts by itself,
|
|
718
|
+
* where the request's Commons route is simply the QR.
|
|
782
719
|
*/
|
|
783
720
|
async startDeviceFlowSession(attempt, opts) {
|
|
784
721
|
if (!this.isCurrentAttempt(attempt))
|
|
@@ -821,9 +758,9 @@ export class AccountDialogController {
|
|
|
821
758
|
void this.openAuthSessionSocket(handle.sessionToken);
|
|
822
759
|
this.scheduleNextPoll(handle.sessionToken);
|
|
823
760
|
if (opts.deliver) {
|
|
824
|
-
// Non-blocking on purpose: the QR/authorizeCode are already renderable
|
|
825
|
-
//
|
|
826
|
-
//
|
|
761
|
+
// Non-blocking on purpose: the QR/authorizeCode are already renderable,
|
|
762
|
+
// while the route (a local probe plus at most one delivery round-trip)
|
|
763
|
+
// resolves behind it.
|
|
827
764
|
void this.resolveDeliveryRoute(handle);
|
|
828
765
|
}
|
|
829
766
|
return handle;
|
|
@@ -927,49 +864,6 @@ export class AccountDialogController {
|
|
|
927
864
|
return false;
|
|
928
865
|
}
|
|
929
866
|
}
|
|
930
|
-
/**
|
|
931
|
-
* Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
|
|
932
|
-
* popup without completing sign-in gets prompt feedback (there is no DOM
|
|
933
|
-
* event for a cross-origin popup closing — polling is the only mechanism).
|
|
934
|
-
* Cleared by {@link closeActivePopup}, which every sign-in exit path already
|
|
935
|
-
* calls.
|
|
936
|
-
*/
|
|
937
|
-
watchPopup(popup) {
|
|
938
|
-
this.clearPopupWatchTimer();
|
|
939
|
-
this.popupWatchTimer = setInterval(() => {
|
|
940
|
-
if (!popup.closed)
|
|
941
|
-
return;
|
|
942
|
-
this.clearPopupWatchTimer();
|
|
943
|
-
if (this.signIn.phase === 'starting' || this.signIn.phase === 'waiting') {
|
|
944
|
-
// Closing the surface cancels the REQUEST too, not just this listener.
|
|
945
|
-
const pendingCode = this.signIn.authorizeCode;
|
|
946
|
-
this.failSignIn('cancelled', 'Sign-in was cancelled.');
|
|
947
|
-
if (pendingCode) {
|
|
948
|
-
void this.withdrawRequest(pendingCode);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}, 1000);
|
|
952
|
-
}
|
|
953
|
-
clearPopupWatchTimer() {
|
|
954
|
-
if (this.popupWatchTimer !== null) {
|
|
955
|
-
clearInterval(this.popupWatchTimer);
|
|
956
|
-
this.popupWatchTimer = null;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/** Tear down the popup watch timer and close the popup, if any. Idempotent. */
|
|
960
|
-
closeActivePopup() {
|
|
961
|
-
this.clearPopupWatchTimer();
|
|
962
|
-
const popup = this.activePopup;
|
|
963
|
-
this.activePopup = null;
|
|
964
|
-
if (!popup || popup.closed)
|
|
965
|
-
return;
|
|
966
|
-
try {
|
|
967
|
-
popup.close();
|
|
968
|
-
}
|
|
969
|
-
catch (error) {
|
|
970
|
-
logger.debug('[AccountDialogController] popup close failed', { component: 'AccountDialogController' }, error);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
867
|
/**
|
|
974
868
|
* Resolve whether Commons is installed on this device via the injected
|
|
975
869
|
* `canOpenApp` probe, updating {@link commonsAvailability} as durable,
|
|
@@ -999,7 +893,7 @@ export class AccountDialogController {
|
|
|
999
893
|
this.emit();
|
|
1000
894
|
}
|
|
1001
895
|
/**
|
|
1002
|
-
* Tear down the active sign-in device flow (timers + socket +
|
|
896
|
+
* Tear down the active sign-in device flow (timers + socket + token),
|
|
1003
897
|
* WITHDRAW the request server-side, and reset to idle.
|
|
1004
898
|
*
|
|
1005
899
|
* Cancellation has to converge in both directions: the surface closing must
|
|
@@ -1022,7 +916,6 @@ export class AccountDialogController {
|
|
|
1022
916
|
: null;
|
|
1023
917
|
this.clearPollTimer();
|
|
1024
918
|
this.closeAuthSessionSocket();
|
|
1025
|
-
this.closeActivePopup();
|
|
1026
919
|
this.signInToken = null;
|
|
1027
920
|
if (this.signIn.phase !== 'idle') {
|
|
1028
921
|
this.setSignIn(IDLE_SIGN_IN_FACTS);
|
|
@@ -1221,10 +1114,9 @@ export class AccountDialogController {
|
|
|
1221
1114
|
this.signInToken = null;
|
|
1222
1115
|
this.clearPollTimer();
|
|
1223
1116
|
this.closeAuthSessionSocket();
|
|
1224
|
-
this.closeActivePopup();
|
|
1225
1117
|
// Terminal SUCCESS, not idle: the surface gets one honest frame to show
|
|
1226
1118
|
// "Identity confirmed" before it closes. Cleared on the next view change.
|
|
1227
|
-
this.signIn =
|
|
1119
|
+
this.signIn = this.stampSignIn(COMPLETED_SIGN_IN_FACTS);
|
|
1228
1120
|
this.view = 'accounts';
|
|
1229
1121
|
this.emit();
|
|
1230
1122
|
}
|
|
@@ -1251,7 +1143,6 @@ export class AccountDialogController {
|
|
|
1251
1143
|
failSignIn(failure, message) {
|
|
1252
1144
|
this.clearPollTimer();
|
|
1253
1145
|
this.closeAuthSessionSocket();
|
|
1254
|
-
this.closeActivePopup();
|
|
1255
1146
|
this.signInToken = null;
|
|
1256
1147
|
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'error', error: message, failure });
|
|
1257
1148
|
}
|
|
@@ -1343,9 +1234,13 @@ export class AccountDialogController {
|
|
|
1343
1234
|
* to advance without a fact behind it.
|
|
1344
1235
|
*/
|
|
1345
1236
|
setSignIn(facts) {
|
|
1346
|
-
this.signIn =
|
|
1237
|
+
this.signIn = this.stampSignIn(facts);
|
|
1347
1238
|
this.emit();
|
|
1348
1239
|
}
|
|
1240
|
+
/** `facts` as the current attempt's state. */
|
|
1241
|
+
stampSignIn(facts) {
|
|
1242
|
+
return buildSignIn(facts, this.signInAttempt, this.signInMethod === 'inline-qr');
|
|
1243
|
+
}
|
|
1349
1244
|
/** Update a subset of the device-flow facts, re-deriving `progress`. */
|
|
1350
1245
|
patchSignIn(patch) {
|
|
1351
1246
|
const { phase, authorizeCode, qrPayload, expiresAt, error, failure, route, routeFailed, pushSentAt, openedAt, } = this.signIn;
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export const CENTRAL_IDP_APEX = 'oxy.so';
|
|
17
17
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* The IdP's origin, `auth.oxy.so` — the OAuth authorize/consent surface AND the
|
|
19
|
+
* web identity carrier: where a person signs in or creates an account with a
|
|
20
|
+
* passkey from another origin's popup, and the one origin that may create,
|
|
21
|
+
* open, recover or move their self-custody identity (one identity, two
|
|
22
|
+
* carriers). Apps open it; they never handle the identity themselves.
|
|
22
23
|
*/
|
|
23
|
-
export const
|
|
24
|
+
export const AUTH_WEB_ORIGIN = `https://auth.${CENTRAL_IDP_APEX}`;
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* crypto already uses — so these helpers run identically on web, Node, and
|
|
16
16
|
* React Native. No `require()`, so the ESM build stays bundler-clean.
|
|
17
17
|
*/
|
|
18
|
+
import { AUTH_WEB_ORIGIN } from './authWebUrl.js';
|
|
18
19
|
import { isNodeJS, isReactNative, loadExpoCrypto, loadNodeCrypto, sha256 } from '@oxy.so/protocol';
|
|
19
20
|
import { logger } from '../logger/index.js';
|
|
20
21
|
/** The central Oxy IdP authorization endpoint used by default. */
|
|
21
|
-
export const OXY_AUTHORIZE_URL =
|
|
22
|
+
export const OXY_AUTHORIZE_URL = `${AUTH_WEB_ORIGIN}/authorize`;
|
|
22
23
|
/** Default OAuth scope requested for a "Sign in with Oxy" third-party flow. */
|
|
23
24
|
export const DEFAULT_OAUTH_SCOPE = 'openid profile';
|
|
24
25
|
/**
|
|
@@ -137,6 +138,9 @@ export function buildOAuthAuthorizeUrl(params) {
|
|
|
137
138
|
if (params.responseMode) {
|
|
138
139
|
url.searchParams.set('response_mode', params.responseMode);
|
|
139
140
|
}
|
|
141
|
+
if (params.screen) {
|
|
142
|
+
url.searchParams.set('screen', params.screen);
|
|
143
|
+
}
|
|
140
144
|
return url.toString();
|
|
141
145
|
}
|
|
142
146
|
/** `sessionStorage` key for the OAuth CSRF `state` across an authorize redirect. */
|