@oxyhq/core 2.3.2 → 2.4.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.
@@ -733,9 +733,10 @@ class AuthManager {
733
733
  * Returns the active user on success, or `null` when neither path
734
734
  * restored a session.
735
735
  */
736
- async initialize() {
737
- // 1. Cookie path (preferred).
738
- const cookieResult = await this.restoreFromCookies();
736
+ async initialize(options = {}) {
737
+ // 1. Cookie path (preferred). Forward the optional cold-boot fail-fast
738
+ // timeout so a cross-domain stall cannot hang provider init.
739
+ const cookieResult = await this.restoreFromCookies(options);
739
740
  if (cookieResult.accounts.length > 0) {
740
741
  return this.currentUser;
741
742
  }
@@ -952,7 +953,7 @@ class AuthManager {
952
953
  * proceed unauthenticated. State is NOT cleared on failure; existing
953
954
  * accounts (if any) remain intact.
954
955
  */
955
- async restoreFromCookies() {
956
+ async restoreFromCookies(options = {}) {
956
957
  // Cross-tab cascade debounce. If we restored within the last
957
958
  // _RESTORE_DEBOUNCE_MS for the currently-active slot, skip the network
958
959
  // round-trip and return the cached registry verbatim. A burst of N
@@ -970,7 +971,9 @@ class AuthManager {
970
971
  }
971
972
  let snapshot;
972
973
  try {
973
- snapshot = await this.oxyServices.refreshAllSessions();
974
+ // Forward the optional cold-boot fail-fast timeout. Undefined (the warm
975
+ // cross-tab cascade default) preserves the wait-indefinitely behaviour.
976
+ snapshot = await this.oxyServices.refreshAllSessions({ timeout: options.timeout });
974
977
  }
975
978
  catch {
976
979
  return { accounts: [], activeAuthuser: null };
@@ -454,19 +454,42 @@ function OxyServicesAuthMixin(Base) {
454
454
  * tokens do. Each access token still needs to be planted via
455
455
  * `setTokens(...)` (or per-account in-memory storage) at the consumer.
456
456
  */
457
- async refreshAllSessions() {
457
+ async refreshAllSessions(options = {}) {
458
458
  const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/auth/refresh-all`;
459
+ // Optional bounded abort (see `RefreshAllOptions.timeout`). A positive
460
+ // timeout arms an `AbortController` that aborts the in-flight request; an
461
+ // abort is treated as "no signed-in accounts on this device" — the same
462
+ // outcome as a 401 — so a cross-domain stall falls through cleanly instead
463
+ // of hanging the cold boot.
464
+ const timeout = typeof options.timeout === 'number' && options.timeout > 0 ? options.timeout : undefined;
465
+ const controller = timeout !== undefined ? new AbortController() : undefined;
466
+ const timeoutId = timeout !== undefined && controller
467
+ ? setTimeout(() => controller.abort(), timeout)
468
+ : undefined;
459
469
  let response;
460
470
  try {
461
471
  response = await fetch(url, {
462
472
  method: 'POST',
463
473
  credentials: 'include',
464
474
  headers: { Accept: 'application/json' },
475
+ signal: controller?.signal,
465
476
  });
466
477
  }
467
478
  catch (error) {
479
+ // A bounded-timeout abort is the "not signed in / cross-domain stall"
480
+ // path, NOT an error. The browser raises a DOMException named
481
+ // 'AbortError' (some runtimes use a generic Error); match on the name so
482
+ // we never throw the timeout into the cold-boot error handler.
483
+ if (error instanceof Error && error.name === 'AbortError') {
484
+ return { accounts: [] };
485
+ }
468
486
  throw this.handleError(error);
469
487
  }
488
+ finally {
489
+ if (timeoutId !== undefined) {
490
+ clearTimeout(timeoutId);
491
+ }
492
+ }
470
493
  if (response.status === 401) {
471
494
  return { accounts: [] };
472
495
  }
@@ -294,6 +294,20 @@ function OxyServicesFedCMMixin(Base) {
294
294
  // Optional/interactive mediation should only happen when the user clicks "Sign In".
295
295
  let credential = null;
296
296
  const loginHint = this.getStoredLoginHint();
297
+ // Fast-skip: with no stored login hint this browser has never completed a
298
+ // FedCM sign-in for any Oxy account, so silent mediation cannot return a
299
+ // credential — the IdP has nothing to silently re-issue. Doing the full
300
+ // round-trip anyway (mint a nonce via `POST /fedcm/nonce`, then a
301
+ // `navigator.credentials.get` that aborts after `FEDCM_SILENT_TIMEOUT`) is
302
+ // pure latency in the cold-boot critical path. Return `null` immediately so
303
+ // the next cold-boot step (stored-session / iframe / bounce) runs without
304
+ // the wasted nonce mint + abort wait. A genuinely associated browser always
305
+ // has a hint (it is stored only after a real exchange), so this never skips
306
+ // a recoverable session.
307
+ if (!loginHint) {
308
+ debug.log('Silent SSO: No stored login hint — skipping silent mediation (no association on this browser)');
309
+ return null;
310
+ }
297
311
  try {
298
312
  // Server-minted, origin-bound nonce — required for `/fedcm/exchange`
299
313
  // to accept the resulting ID token (anti-replay binding).
@@ -741,12 +755,17 @@ function OxyServicesFedCMMixin(Base) {
741
755
  _a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
742
756
  _a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
743
757
  ,
744
- // Silent mediation runs on page load (e.g. re-signing-in a user whose stored
745
- // session was cleared after a cold-boot token fetch 401'd). The real silent
746
- // round-trip mint nonce navigator.credentials.get /fedcm/exchange was
747
- // measured to take more than 3s for live users, so a 3s budget timed out and
748
- // left them signed out on reload. 10s gives ample margin while staying bounded.
749
- _a.FEDCM_SILENT_TIMEOUT = 10000 // 10 seconds for silent mediation
758
+ // Silent mediation runs on page load as ONE step of the ordered cold-boot
759
+ // sequence (mint nonce navigator.credentials.get /fedcm/exchange). The
760
+ // real round-trip was measured at >3s for live users, so the budget must stay
761
+ // comfortably above 3s. It must ALSO be tight: on a logged-out browser this
762
+ // step never resolves a credential, and every millisecond it spends timing
763
+ // out is pure latency in front of the steps that actually hold the answer
764
+ // (stored-session bearer, the per-apex silent iframe, the /sso bounce). 4s is
765
+ // the floor that preserves the >3s success margin while bounding the dead
766
+ // wait — down from the previous 10s, which alone could account for most of a
767
+ // 20-30s cold-boot stall. Do NOT lower below 4s (it would clip live success).
768
+ _a.FEDCM_SILENT_TIMEOUT = 4000 // 4 seconds for silent mediation
750
769
  ,
751
770
  _a;
752
771
  }
@@ -411,8 +411,24 @@ function OxyServicesPopupAuthMixin(Base) {
411
411
  cleanup();
412
412
  resolve(session || null);
413
413
  };
414
+ // Fail-fast on a load failure. When the per-apex `/auth/silent` host is
415
+ // unreachable, blocked by CSP `frame-ancestors`/`X-Frame-Options`, or the
416
+ // network drops, the iframe never posts a message — without this handler
417
+ // the silent restore would block for the FULL `timeout` (dead latency in
418
+ // the cold-boot critical path). `onerror`/`onabort` fire on a failed load,
419
+ // so resolve `null` immediately and let the next cold-boot step run. The
420
+ // success path posts a message and is handled above; these only catch the
421
+ // no-message failure modes.
422
+ const failFast = () => {
423
+ cleanup();
424
+ resolve(null);
425
+ };
426
+ iframe.onerror = failFast;
427
+ iframe.onabort = failFast;
414
428
  const cleanup = () => {
415
429
  clearTimeout(timeoutId);
430
+ iframe.onerror = null;
431
+ iframe.onabort = null;
416
432
  window.removeEventListener('message', messageHandler);
417
433
  };
418
434
  window.addEventListener('message', messageHandler);