@oxy.so/core 1.13.0 → 1.18.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 (117) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/crypto/identityLink.js +18 -0
  3. package/dist/cjs/i18n/locales/ar-SA.json +105 -85
  4. package/dist/cjs/i18n/locales/ca-ES.json +107 -87
  5. package/dist/cjs/i18n/locales/de-DE.json +105 -85
  6. package/dist/cjs/i18n/locales/en-US.json +105 -164
  7. package/dist/cjs/i18n/locales/es-ES.json +104 -163
  8. package/dist/cjs/i18n/locales/fr-FR.json +105 -85
  9. package/dist/cjs/i18n/locales/it-IT.json +105 -85
  10. package/dist/cjs/i18n/locales/ja-JP.json +105 -85
  11. package/dist/cjs/i18n/locales/ko-KR.json +105 -85
  12. package/dist/cjs/i18n/locales/locales/ar-SA.json +105 -85
  13. package/dist/cjs/i18n/locales/locales/ca-ES.json +107 -87
  14. package/dist/cjs/i18n/locales/locales/de-DE.json +105 -85
  15. package/dist/cjs/i18n/locales/locales/en-US.json +105 -164
  16. package/dist/cjs/i18n/locales/locales/es-ES.json +104 -163
  17. package/dist/cjs/i18n/locales/locales/fr-FR.json +105 -85
  18. package/dist/cjs/i18n/locales/locales/it-IT.json +105 -85
  19. package/dist/cjs/i18n/locales/locales/ja-JP.json +105 -85
  20. package/dist/cjs/i18n/locales/locales/ko-KR.json +105 -85
  21. package/dist/cjs/i18n/locales/locales/pt-PT.json +105 -85
  22. package/dist/cjs/i18n/locales/locales/zh-CN.json +106 -86
  23. package/dist/cjs/i18n/locales/pt-PT.json +105 -85
  24. package/dist/cjs/i18n/locales/zh-CN.json +106 -86
  25. package/dist/cjs/index.js +10 -43
  26. package/dist/cjs/mixins/OxyServices.auth.js +54 -16
  27. package/dist/cjs/mixins/OxyServices.identity.js +128 -8
  28. package/dist/cjs/mixins/OxyServices.user.js +42 -7
  29. package/dist/cjs/server/securityHeaders.js +19 -1
  30. package/dist/cjs/session/accountDialogController.js +37 -142
  31. package/dist/cjs/utils/authWebUrl.js +6 -6
  32. package/dist/cjs/utils/oauthPkce.js +5 -1
  33. package/dist/esm/.tsbuildinfo +1 -1
  34. package/dist/esm/crypto/identityLink.js +15 -0
  35. package/dist/esm/i18n/locales/ar-SA.json +105 -85
  36. package/dist/esm/i18n/locales/ca-ES.json +107 -87
  37. package/dist/esm/i18n/locales/de-DE.json +105 -85
  38. package/dist/esm/i18n/locales/en-US.json +105 -164
  39. package/dist/esm/i18n/locales/es-ES.json +104 -163
  40. package/dist/esm/i18n/locales/fr-FR.json +105 -85
  41. package/dist/esm/i18n/locales/it-IT.json +105 -85
  42. package/dist/esm/i18n/locales/ja-JP.json +105 -85
  43. package/dist/esm/i18n/locales/ko-KR.json +105 -85
  44. package/dist/esm/i18n/locales/locales/ar-SA.json +105 -85
  45. package/dist/esm/i18n/locales/locales/ca-ES.json +107 -87
  46. package/dist/esm/i18n/locales/locales/de-DE.json +105 -85
  47. package/dist/esm/i18n/locales/locales/en-US.json +105 -164
  48. package/dist/esm/i18n/locales/locales/es-ES.json +104 -163
  49. package/dist/esm/i18n/locales/locales/fr-FR.json +105 -85
  50. package/dist/esm/i18n/locales/locales/it-IT.json +105 -85
  51. package/dist/esm/i18n/locales/locales/ja-JP.json +105 -85
  52. package/dist/esm/i18n/locales/locales/ko-KR.json +105 -85
  53. package/dist/esm/i18n/locales/locales/pt-PT.json +105 -85
  54. package/dist/esm/i18n/locales/locales/zh-CN.json +106 -86
  55. package/dist/esm/i18n/locales/pt-PT.json +105 -85
  56. package/dist/esm/i18n/locales/zh-CN.json +106 -86
  57. package/dist/esm/index.js +5 -5
  58. package/dist/esm/mixins/OxyServices.auth.js +55 -17
  59. package/dist/esm/mixins/OxyServices.identity.js +128 -8
  60. package/dist/esm/mixins/OxyServices.user.js +42 -7
  61. package/dist/esm/server/securityHeaders.js +19 -1
  62. package/dist/esm/session/accountDialogController.js +37 -142
  63. package/dist/esm/utils/authWebUrl.js +5 -5
  64. package/dist/esm/utils/oauthPkce.js +5 -1
  65. package/dist/types/.tsbuildinfo +1 -1
  66. package/dist/types/crypto/identityLink.d.ts +8 -0
  67. package/dist/types/index.d.ts +5 -8
  68. package/dist/types/mixins/OxyServices.auth.d.ts +35 -25
  69. package/dist/types/mixins/OxyServices.identity.d.ts +34 -34
  70. package/dist/types/mixins/OxyServices.user.d.ts +29 -6
  71. package/dist/types/models/interfaces.d.ts +46 -13
  72. package/dist/types/server/securityHeaders.d.ts +9 -12
  73. package/dist/types/session/accountDialogController.d.ts +29 -98
  74. package/dist/types/utils/authWebUrl.d.ts +5 -5
  75. package/dist/types/utils/oauthPkce.d.ts +10 -0
  76. package/package.json +3 -3
  77. package/src/crypto/__tests__/identityLink.test.ts +18 -0
  78. package/src/crypto/__tests__/identityProof.test.ts +6 -6
  79. package/src/crypto/identityLink.ts +18 -0
  80. package/src/i18n/locales/ar-SA.json +105 -85
  81. package/src/i18n/locales/ca-ES.json +107 -87
  82. package/src/i18n/locales/de-DE.json +105 -85
  83. package/src/i18n/locales/en-US.json +105 -164
  84. package/src/i18n/locales/es-ES.json +104 -163
  85. package/src/i18n/locales/fr-FR.json +105 -85
  86. package/src/i18n/locales/it-IT.json +105 -85
  87. package/src/i18n/locales/ja-JP.json +105 -85
  88. package/src/i18n/locales/ko-KR.json +105 -85
  89. package/src/i18n/locales/pt-PT.json +105 -85
  90. package/src/i18n/locales/zh-CN.json +106 -86
  91. package/src/index.ts +8 -51
  92. package/src/mixins/OxyServices.auth.ts +72 -24
  93. package/src/mixins/OxyServices.identity.ts +126 -8
  94. package/src/mixins/OxyServices.user.ts +51 -10
  95. package/src/mixins/__tests__/identityLink.test.ts +89 -0
  96. package/src/mixins/__tests__/notifications.read.test.ts +82 -0
  97. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +1 -1
  98. package/src/mixins/__tests__/webauthnAuth.test.ts +75 -1
  99. package/src/models/interfaces.ts +46 -12
  100. package/src/server/__tests__/securityHeaders.test.ts +17 -0
  101. package/src/server/securityHeaders.ts +29 -1
  102. package/src/session/__tests__/accountDialogController.test.ts +42 -184
  103. package/src/session/accountDialogController.ts +50 -185
  104. package/src/utils/__tests__/oauthPkce.test.ts +7 -0
  105. package/src/utils/authWebUrl.ts +5 -5
  106. package/src/utils/oauthPkce.ts +16 -1
  107. package/dist/cjs/crypto/identityMove.js +0 -142
  108. package/dist/cjs/crypto/webIdentityCarrier.js +0 -325
  109. package/dist/esm/crypto/identityMove.js +0 -128
  110. package/dist/esm/crypto/webIdentityCarrier.js +0 -303
  111. package/dist/types/crypto/identityMove.d.ts +0 -75
  112. package/dist/types/crypto/webIdentityCarrier.d.ts +0 -150
  113. package/src/crypto/__tests__/identityMove.test.ts +0 -146
  114. package/src/crypto/__tests__/webIdentityCarrier.test.ts +0 -169
  115. package/src/crypto/__tests__/webIdentityRecoveryMaterial.test.ts +0 -117
  116. package/src/crypto/identityMove.ts +0 -167
  117. package/src/crypto/webIdentityCarrier.ts +0 -394
@@ -535,6 +535,35 @@ function OxyServicesUserMixin(Base) {
535
535
  throw this.handleError(error);
536
536
  }
537
537
  }
538
+ /**
539
+ * WebAuthn request options to delete a PASSKEY account (ADR 0029 D3): its own
540
+ * passkeys, user verification required, and a challenge bound to it. Opaque —
541
+ * hand them to the browser's authentication ceremony, on auth.oxy.so. An
542
+ * account with a Commons key deletes with {@link deleteAccount} instead.
543
+ */
544
+ async getAccountDeletionOptions() {
545
+ try {
546
+ return await this.makeRequest('POST', '/users/me/delete/options', undefined, { cache: false });
547
+ }
548
+ catch (error) {
549
+ throw this.handleError(error);
550
+ }
551
+ }
552
+ /**
553
+ * Delete a PASSKEY account permanently, with an assertion by one of its
554
+ * passkeys over {@link getAccountDeletionOptions}' challenge.
555
+ *
556
+ * @param confirmText - Must equal the user's username (verified server-side)
557
+ * @param assertion - The ceremony's opaque `AuthenticationResponseJSON`
558
+ */
559
+ async deleteAccountWithPasskey(confirmText, assertion) {
560
+ try {
561
+ return await this.makeRequest('DELETE', '/users/me', { confirmText, assertion }, { cache: false });
562
+ }
563
+ catch (error) {
564
+ throw this.handleError(error);
565
+ }
566
+ }
538
567
  /**
539
568
  * Invalidate every cached read a follow/unfollow write invalidates.
540
569
  *
@@ -844,11 +873,13 @@ function OxyServicesUserMixin(Base) {
844
873
  }
845
874
  }
846
875
  /**
847
- * Get notifications
876
+ * One page of the signed-in user's Oxy notifications, newest first
877
+ * (`GET /notifications`), with the unread count across all pages.
848
878
  */
849
- async getNotifications() {
879
+ async getNotifications(params = {}) {
880
+ const query = (0, apiUtils_1.buildQueryParams)({ page: params.page, limit: params.limit });
850
881
  return this.withAuthRetry(async () => {
851
- return await this.makeRequest('GET', '/notifications', undefined, {
882
+ return await this.makeRequest('GET', '/notifications', query, {
852
883
  cache: false, // Don't cache notifications - always get fresh data
853
884
  });
854
885
  }, 'getNotifications');
@@ -861,7 +892,7 @@ function OxyServicesUserMixin(Base) {
861
892
  const res = await this.makeRequest('GET', '/notifications/unread-count', undefined, {
862
893
  cache: false, // Don't cache unread count - always get fresh data
863
894
  });
864
- return res.count;
895
+ return res.unreadCount;
865
896
  }
866
897
  catch (error) {
867
898
  throw this.handleError(error);
@@ -876,18 +907,22 @@ function OxyServicesUserMixin(Base) {
876
907
  */
877
908
  async createNotification(data) {
878
909
  try {
879
- return await this.makeRequest('POST', '/notifications', data, { cache: false });
910
+ const res = await this.makeRequest('POST', '/notifications', data, { cache: false });
911
+ return res.notification;
880
912
  }
881
913
  catch (error) {
882
914
  throw this.handleError(error);
883
915
  }
884
916
  }
885
917
  /**
886
- * Mark notification as read
918
+ * Mark one of the signed-in user's notifications read and return it as
919
+ * stored. Scoped to the recipient server-side, so it is also the
920
+ * authoritative read of a notification by id: a foreign or unknown id 404s.
887
921
  */
888
922
  async markNotificationAsRead(notificationId) {
889
923
  try {
890
- await this.makeRequest('PUT', `/notifications/${notificationId}/read`, undefined, { cache: false });
924
+ const res = await this.makeRequest('PUT', `/notifications/${encodeURIComponent(notificationId)}/read`, undefined, { cache: false });
925
+ return res.notification;
891
926
  }
892
927
  catch (error) {
893
928
  throw this.handleError(error);
@@ -326,6 +326,20 @@ const MAX_INLINE_SCRIPT_HASHES = 8;
326
326
  * react-native-web's runtime stylesheet, and a style hash would silently switch
327
327
  * that off and render every Oxy web app unstyled.)
328
328
  */
329
+ /** Passkeys for this origin itself; every other powerful feature off. */
330
+ const SENSITIVE_PERMISSIONS_POLICY = [
331
+ 'publickey-credentials-get=(self)',
332
+ 'publickey-credentials-create=(self)',
333
+ 'camera=()',
334
+ 'microphone=()',
335
+ 'geolocation=()',
336
+ 'payment=()',
337
+ 'usb=()',
338
+ 'serial=()',
339
+ 'hid=()',
340
+ 'bluetooth=()',
341
+ 'display-capture=()',
342
+ ].join(', ');
329
343
  function buildOxyPagesHeaders(options = {}) {
330
344
  const hashes = [
331
345
  ...new Set((options.html ?? []).flatMap(extractInlineScripts).map(inlineScriptCspHash)),
@@ -341,6 +355,8 @@ function buildOxyPagesHeaders(options = {}) {
341
355
  for (const name of ['script-src', 'connect-src']) {
342
356
  directives[name] = (directives[name] ?? []).filter((source) => !beacon.has(source));
343
357
  }
358
+ directives['base-uri'] = ["'none'"];
359
+ directives['form-action'] = ["'none'"];
344
360
  }
345
361
  const csp = formatOxyCspPolicy(directives);
346
362
  const lines = [
@@ -348,13 +364,15 @@ function buildOxyPagesHeaders(options = {}) {
348
364
  ` Content-Security-Policy: ${csp}`,
349
365
  ' X-Frame-Options: DENY',
350
366
  ' X-Content-Type-Options: nosniff',
351
- ' Referrer-Policy: strict-origin-when-cross-origin',
367
+ ` Referrer-Policy: ${options.sensitive ? 'no-referrer' : 'strict-origin-when-cross-origin'}`,
352
368
  ];
353
369
  if (options.hsts !== false) {
354
370
  lines.push(' Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
355
371
  }
356
372
  if (options.sensitive) {
357
373
  lines.push(' Cache-Control: public, max-age=0, must-revalidate, no-transform');
374
+ lines.push(` Permissions-Policy: ${SENSITIVE_PERMISSIONS_POLICY}`);
375
+ lines.push(' Cross-Origin-Resource-Policy: same-origin');
358
376
  }
359
377
  lines.push('');
360
378
  return lines.join('\n');
@@ -37,12 +37,6 @@
37
37
  * - `commonsAvailability` — whether Commons is installed on this device
38
38
  * (native only, via the injected `canOpenApp` probe), so the QR view can
39
39
  * offer a "Get Commons" fallback instead of a same-device dead end.
40
- * - `startPasskeyHubSignIn` — on a non-Oxy web origin (where a WebAuthn
41
- * ceremony can't run locally, the RP-ID is bound to oxy.so), open a popup
42
- * at the auth.oxy.so passkey hub instead, scoped to the SAME device-flow
43
- * session `showQr` creates, and complete it via the SAME poll/socket/claim
44
- * engine. Falls back to `showQr`'s plain QR rendering if the popup is
45
- * blocked.
46
40
  *
47
41
  * Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
48
42
  * password, social login, and 2FA were removed ecosystem-wide. Account
@@ -62,7 +56,6 @@ exports.backViewOf = backViewOf;
62
56
  exports.createAccountDialogController = createAccountDialogController;
63
57
  const logger_1 = require("../logger");
64
58
  const errorUtils_1 = require("../utils/errorUtils");
65
- const authWebUrl_1 = require("../utils/authWebUrl");
66
59
  const socketLoader_1 = require("./socketLoader");
67
60
  const deviceDirectory_1 = require("./deviceDirectory");
68
61
  const commonsDelivery_1 = require("../utils/commonsDelivery");
@@ -130,8 +123,8 @@ const IDLE_SIGN_IN_FACTS = {
130
123
  */
131
124
  const COMPLETED_SIGN_IN_FACTS = { ...IDLE_SIGN_IN_FACTS, phase: 'completed' };
132
125
  /** The full flow state for `facts`, stamped with the attempt it belongs to. */
133
- function buildSignIn(facts, attempt) {
134
- return { ...facts, progress: deriveSignInProgress(facts), attempt };
126
+ function buildSignIn(facts, attempt, inline) {
127
+ return { ...facts, progress: deriveSignInProgress(facts), attempt, inline };
135
128
  }
136
129
  function errorMessage(error) {
137
130
  return error instanceof Error ? error.message : String(error);
@@ -163,7 +156,7 @@ class AccountDialogController {
163
156
  * alongside the three flags above.
164
157
  */
165
158
  this.exclusiveMutationInFlight = false;
166
- this.signIn = buildSignIn(IDLE_SIGN_IN_FACTS, 0);
159
+ this.signIn = buildSignIn(IDLE_SIGN_IN_FACTS, 0, false);
167
160
  this.commonsAvailability = 'unknown';
168
161
  // --- Sign-in device-flow bookkeeping ---
169
162
  /**
@@ -192,10 +185,6 @@ class AccountDialogController {
192
185
  * ABANDONED request never blocks the next request's first poll.
193
186
  */
194
187
  this.pollInFlightToken = null;
195
- /** The popup opened by {@link startPasskeyHubSignIn}, or `null`. */
196
- this.activePopup = null;
197
- /** Watches {@link activePopup}'s `closed` state; see {@link watchPopup}. */
198
- this.popupWatchTimer = null;
199
188
  // --- Store plumbing ---
200
189
  this.unsubscribeSession = null;
201
190
  this.unsubscribeTokens = null;
@@ -212,8 +201,6 @@ class AccountDialogController {
212
201
  this.openUrl = options.openUrl;
213
202
  this.canOpenApp = options.canOpenApp;
214
203
  this.socketFactory = options.socketFactory;
215
- this.openPopup = options.openPopup;
216
- this.identityOrigin = options.identityOrigin ?? options.hubBaseUrl ?? authWebUrl_1.IDENTITY_WEB_ORIGIN;
217
204
  this.platform = options.platform ?? 'unknown';
218
205
  this.snapshot = this.computeSnapshot();
219
206
  }
@@ -291,7 +278,6 @@ class AccountDialogController {
291
278
  }
292
279
  this.clearPollTimer();
293
280
  this.closeAuthSessionSocket();
294
- this.closeActivePopup();
295
281
  this.signInToken = null;
296
282
  this.listeners.clear();
297
283
  }
@@ -356,12 +342,12 @@ class AccountDialogController {
356
342
  if (this.view === view)
357
343
  return;
358
344
  this.view = view;
359
- // A `'completed'` flow owns no timers, socket, popup, or token — it is only
345
+ // A `'completed'` flow owns no timers, socket, or token — it is only
360
346
  // the terminal "Identity confirmed" the finished surface showed. Moving to
361
347
  // another view is a NEW intention, so drop it; otherwise a later `add()`
362
348
  // would open on the previous sign-in's terminal state.
363
349
  if (this.signIn.phase === 'completed') {
364
- this.signIn = buildSignIn(IDLE_SIGN_IN_FACTS, this.signInAttempt);
350
+ this.signIn = this.stampSignIn(IDLE_SIGN_IN_FACTS);
365
351
  }
366
352
  this.emit();
367
353
  }
@@ -678,82 +664,33 @@ class AccountDialogController {
678
664
  await this.startDeviceFlowSession(attempt, { deliver: true });
679
665
  }
680
666
  /**
681
- * Start a NEW attempt the same way the last one was started — "Try again"
682
- * repeats the user's choice rather than silently switching method. A failed
683
- * passkey-hub attempt reopens the hub popup; a failed "Sign in with Oxy"
684
- * retries that; an explicit QR request shows the QR.
667
+ * The sign-in entry's EMBEDDED QR: a request whose only route is the QR,
668
+ * started without leaving the current view.
685
669
  *
686
- * Call it straight from the press handler: the hub route opens its popup
687
- * synchronously, before this method's first `await`, so the browser still
688
- * attributes it to the click.
670
+ * Unlike {@link showQr} it runs no delivery selection. The surface starts it
671
+ * by itself, on mount, so it must not push to the person's phone or open
672
+ * Commons — nobody asked for either. Any other sign-in the person then
673
+ * chooses supersedes it (and withdraws its request) like any new attempt.
674
+ */
675
+ async startInlineQr() {
676
+ const attempt = this.beginSignInAttempt('inline-qr');
677
+ await this.startDeviceFlowSession(attempt, { deliver: false });
678
+ }
679
+ /**
680
+ * Start a NEW attempt the same way the last one was started — "Try again"
681
+ * repeats the user's choice rather than silently switching method: a failed
682
+ * "Sign in with Oxy" retries that; an explicit QR request shows the QR.
689
683
  */
690
684
  retrySignIn() {
691
685
  switch (this.signInMethod) {
692
- case 'passkey-hub':
693
- return this.startPasskeyHubSignIn();
694
686
  case 'qr':
695
687
  return this.showQr();
688
+ case 'inline-qr':
689
+ return this.startInlineQr();
696
690
  default:
697
691
  return this.signInWithOxy();
698
692
  }
699
693
  }
700
- /**
701
- * Web-only: sign in (or create an account) with a passkey at the web identity
702
- * carrier. The ceremony runs on `id.oxy.so`, never on the calling origin, for
703
- * two reasons: a credential minted with `WEBAUTHN_RP_ID=oxy.so` can only be
704
- * asserted from `oxy.so`/a subdomain/loopback (a browser-enforced boundary),
705
- * and only the identity origin may unseal or create the account's identity
706
- * (one identity, two carriers). Opens a popup at `<identityOrigin>/continue`,
707
- * scoped to the SAME device-flow
708
- * session {@link showQr} would create (same `authorizeCode`/`sessionToken`
709
- * pair), and let the SAME poll/socket/claim engine complete it once the hub
710
- * authorizes the session (`POST /auth/session/authorize-code/:authorizeCode`,
711
- * bearer-authed, server-side — this method never sees or transmits the
712
- * secret `sessionToken`).
713
- *
714
- * The popup MUST be opened synchronously, before any `await`, or the
715
- * browser's popup blocker loses the click-gesture attribution and silently
716
- * blocks it — so {@link openPopup} is invoked as the very first step, before
717
- * the device-flow session (which needs an async call) even exists. If the
718
- * popup is blocked (or no `openPopup` was injected), this falls back to
719
- * `showQr`'s plain QR rendering — no dead end.
720
- */
721
- async startPasskeyHubSignIn() {
722
- const attempt = this.beginSignInAttempt('passkey-hub');
723
- this.setView('qr');
724
- const popup = this.openPopup?.() ?? null;
725
- if (!popup) {
726
- await this.startDeviceFlowSession(attempt, { deliver: true });
727
- return;
728
- }
729
- // Owned by the attempt from the moment it exists, so cancelling while the
730
- // session is still being created closes it instead of leaving a blank
731
- // window behind.
732
- this.activePopup = popup;
733
- // The hub popup IS the primary surface here, chosen explicitly by the user —
734
- // so this flow does NOT run automatic Commons delivery (ringing the user's
735
- // phone because they asked for a passkey would be exactly the "menu of
736
- // methods" the one-primary-action rule forbids). The underlying request is
737
- // the same `AuthSession`, and its Commons route stays the QR the view
738
- // renders beneath the popup.
739
- const handle = await this.startDeviceFlowSession(attempt, { deliver: false });
740
- // `null` means the attempt failed (its teardown closed the popup) or was
741
- // abandoned (the cancel closed it).
742
- if (!handle)
743
- return;
744
- // The user may have closed the popup during the async session creation
745
- // above, before any watcher was attached to catch it — guard rather than
746
- // navigate a dead window (browsers vary on whether that throws).
747
- if (popup.closed) {
748
- const pendingCode = this.signIn.authorizeCode;
749
- this.failSignIn('cancelled', 'Sign-in was cancelled.');
750
- if (pendingCode)
751
- void this.withdrawRequest(pendingCode);
752
- return;
753
- }
754
- popup.location.href = `${this.identityOrigin}/continue?code=${encodeURIComponent(handle.authorizeCode)}`;
755
- this.watchPopup(popup);
756
- }
757
694
  /**
758
695
  * End whatever attempt is running (withdrawing its request) and begin a new
759
696
  * one. Returns the new attempt's identity for the caller to re-check after
@@ -769,9 +706,9 @@ class AccountDialogController {
769
706
  return attempt === this.signInAttempt;
770
707
  }
771
708
  /**
772
- * Shared device-flow session creation for both {@link showQr} (renders the
773
- * QR) and {@link startPasskeyHubSignIn} (also opens the hub popup) — the
774
- * same `startCommonsSignIn` → poll/socket wiring either way. Returns the
709
+ * Shared device-flow session creation for {@link signInWithOxy},
710
+ * {@link showQr} and {@link startInlineQr} — the same `startCommonsSignIn` →
711
+ * poll/socket wiring every time. Returns the
775
712
  * handle on success (already reflected in `signIn`), or `null` when the
776
713
  * attempt failed (already set as `signIn.failure`) or was abandoned while the
777
714
  * request was being created.
@@ -782,8 +719,8 @@ class AccountDialogController {
782
719
  * request, a socket, and a poll behind it.
783
720
  * @param opts.deliver - Whether to run automatic Commons delivery selection
784
721
  * ({@link resolveDeliveryRoute}). `true` for the normal one-primary-action
785
- * entry; `false` when the caller already owns the primary surface (the
786
- * passkey hub popup), where the request's Commons route is simply the QR.
722
+ * entry; `false` for the embedded QR, which the screen starts by itself,
723
+ * where the request's Commons route is simply the QR.
787
724
  */
788
725
  async startDeviceFlowSession(attempt, opts) {
789
726
  if (!this.isCurrentAttempt(attempt))
@@ -826,9 +763,9 @@ class AccountDialogController {
826
763
  void this.openAuthSessionSocket(handle.sessionToken);
827
764
  this.scheduleNextPoll(handle.sessionToken);
828
765
  if (opts.deliver) {
829
- // Non-blocking on purpose: the QR/authorizeCode are already renderable
830
- // and the popup caller can navigate immediately, while the route (a
831
- // local probe plus at most one delivery round-trip) resolves behind it.
766
+ // Non-blocking on purpose: the QR/authorizeCode are already renderable,
767
+ // while the route (a local probe plus at most one delivery round-trip)
768
+ // resolves behind it.
832
769
  void this.resolveDeliveryRoute(handle);
833
770
  }
834
771
  return handle;
@@ -932,49 +869,6 @@ class AccountDialogController {
932
869
  return false;
933
870
  }
934
871
  }
935
- /**
936
- * Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
937
- * popup without completing sign-in gets prompt feedback (there is no DOM
938
- * event for a cross-origin popup closing — polling is the only mechanism).
939
- * Cleared by {@link closeActivePopup}, which every sign-in exit path already
940
- * calls.
941
- */
942
- watchPopup(popup) {
943
- this.clearPopupWatchTimer();
944
- this.popupWatchTimer = setInterval(() => {
945
- if (!popup.closed)
946
- return;
947
- this.clearPopupWatchTimer();
948
- if (this.signIn.phase === 'starting' || this.signIn.phase === 'waiting') {
949
- // Closing the surface cancels the REQUEST too, not just this listener.
950
- const pendingCode = this.signIn.authorizeCode;
951
- this.failSignIn('cancelled', 'Sign-in was cancelled.');
952
- if (pendingCode) {
953
- void this.withdrawRequest(pendingCode);
954
- }
955
- }
956
- }, 1000);
957
- }
958
- clearPopupWatchTimer() {
959
- if (this.popupWatchTimer !== null) {
960
- clearInterval(this.popupWatchTimer);
961
- this.popupWatchTimer = null;
962
- }
963
- }
964
- /** Tear down the popup watch timer and close the popup, if any. Idempotent. */
965
- closeActivePopup() {
966
- this.clearPopupWatchTimer();
967
- const popup = this.activePopup;
968
- this.activePopup = null;
969
- if (!popup || popup.closed)
970
- return;
971
- try {
972
- popup.close();
973
- }
974
- catch (error) {
975
- logger_1.logger.debug('[AccountDialogController] popup close failed', { component: 'AccountDialogController' }, error);
976
- }
977
- }
978
872
  /**
979
873
  * Resolve whether Commons is installed on this device via the injected
980
874
  * `canOpenApp` probe, updating {@link commonsAvailability} as durable,
@@ -1004,7 +898,7 @@ class AccountDialogController {
1004
898
  this.emit();
1005
899
  }
1006
900
  /**
1007
- * Tear down the active sign-in device flow (timers + socket + popup + token),
901
+ * Tear down the active sign-in device flow (timers + socket + token),
1008
902
  * WITHDRAW the request server-side, and reset to idle.
1009
903
  *
1010
904
  * Cancellation has to converge in both directions: the surface closing must
@@ -1027,7 +921,6 @@ class AccountDialogController {
1027
921
  : null;
1028
922
  this.clearPollTimer();
1029
923
  this.closeAuthSessionSocket();
1030
- this.closeActivePopup();
1031
924
  this.signInToken = null;
1032
925
  if (this.signIn.phase !== 'idle') {
1033
926
  this.setSignIn(IDLE_SIGN_IN_FACTS);
@@ -1226,10 +1119,9 @@ class AccountDialogController {
1226
1119
  this.signInToken = null;
1227
1120
  this.clearPollTimer();
1228
1121
  this.closeAuthSessionSocket();
1229
- this.closeActivePopup();
1230
1122
  // Terminal SUCCESS, not idle: the surface gets one honest frame to show
1231
1123
  // "Identity confirmed" before it closes. Cleared on the next view change.
1232
- this.signIn = buildSignIn(COMPLETED_SIGN_IN_FACTS, this.signInAttempt);
1124
+ this.signIn = this.stampSignIn(COMPLETED_SIGN_IN_FACTS);
1233
1125
  this.view = 'accounts';
1234
1126
  this.emit();
1235
1127
  }
@@ -1256,7 +1148,6 @@ class AccountDialogController {
1256
1148
  failSignIn(failure, message) {
1257
1149
  this.clearPollTimer();
1258
1150
  this.closeAuthSessionSocket();
1259
- this.closeActivePopup();
1260
1151
  this.signInToken = null;
1261
1152
  this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'error', error: message, failure });
1262
1153
  }
@@ -1348,9 +1239,13 @@ class AccountDialogController {
1348
1239
  * to advance without a fact behind it.
1349
1240
  */
1350
1241
  setSignIn(facts) {
1351
- this.signIn = buildSignIn(facts, this.signInAttempt);
1242
+ this.signIn = this.stampSignIn(facts);
1352
1243
  this.emit();
1353
1244
  }
1245
+ /** `facts` as the current attempt's state. */
1246
+ stampSignIn(facts) {
1247
+ return buildSignIn(facts, this.signInAttempt, this.signInMethod === 'inline-qr');
1248
+ }
1354
1249
  /** Update a subset of the device-flow facts, re-deriving `progress`. */
1355
1250
  patchSignIn(patch) {
1356
1251
  const { phase, authorizeCode, qrPayload, expiresAt, error, failure, route, routeFailed, pushSentAt, openedAt, } = this.signIn;
@@ -10,7 +10,7 @@
10
10
  * past the SSO/FedCM teardown.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.IDENTITY_WEB_ORIGIN = exports.CENTRAL_IDP_APEX = void 0;
13
+ exports.AUTH_WEB_ORIGIN = exports.CENTRAL_IDP_APEX = void 0;
14
14
  /**
15
15
  * The registrable apex (eTLD+1) of the Oxy ecosystem's central Identity
16
16
  * Provider, reachable at `auth.${CENTRAL_IDP_APEX}`. Single source of truth so
@@ -18,9 +18,9 @@ exports.IDENTITY_WEB_ORIGIN = exports.CENTRAL_IDP_APEX = void 0;
18
18
  */
19
19
  exports.CENTRAL_IDP_APEX = 'oxy.so';
20
20
  /**
21
- * The web identity carrier's origin — where a person signs in or creates an
22
- * account with a passkey, and where their self-custody identity is kept sealed
23
- * under that passkey (one identity, two carriers). Apps open it; they never
24
- * handle the identity themselves.
21
+ * The IdP's origin, `auth.oxy.so` — the OAuth authorize/consent surface and the
22
+ * one origin that asserts Oxy passkeys: where a person signs in, creates,
23
+ * recovers or deletes a web account, in the window an app opens (ADR 0029).
24
+ * Apps open it; they never run the passkey themselves.
25
25
  */
26
- exports.IDENTITY_WEB_ORIGIN = `https://id.${exports.CENTRAL_IDP_APEX}`;
26
+ exports.AUTH_WEB_ORIGIN = `https://auth.${exports.CENTRAL_IDP_APEX}`;
@@ -29,10 +29,11 @@ exports.consumeOAuthReturnPath = consumeOAuthReturnPath;
29
29
  exports.persistOAuthHandshake = persistOAuthHandshake;
30
30
  exports.readOAuthHandshake = readOAuthHandshake;
31
31
  exports.clearOAuthHandshake = clearOAuthHandshake;
32
+ const authWebUrl_1 = require("./authWebUrl");
32
33
  const protocol_1 = require("@oxy.so/protocol");
33
34
  const logger_1 = require("../logger");
34
35
  /** The central Oxy IdP authorization endpoint used by default. */
35
- exports.OXY_AUTHORIZE_URL = 'https://auth.oxy.so/authorize';
36
+ exports.OXY_AUTHORIZE_URL = `${authWebUrl_1.AUTH_WEB_ORIGIN}/authorize`;
36
37
  /** Default OAuth scope requested for a "Sign in with Oxy" third-party flow. */
37
38
  exports.DEFAULT_OAUTH_SCOPE = 'openid profile';
38
39
  /**
@@ -151,6 +152,9 @@ function buildOAuthAuthorizeUrl(params) {
151
152
  if (params.responseMode) {
152
153
  url.searchParams.set('response_mode', params.responseMode);
153
154
  }
155
+ if (params.screen) {
156
+ url.searchParams.set('screen', params.screen);
157
+ }
154
158
  return url.toString();
155
159
  }
156
160
  /** `sessionStorage` key for the OAuth CSRF `state` across an authorize redirect. */