@oxyhq/auth 2.0.7 → 2.0.8

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.
@@ -12,19 +12,36 @@ import { QueryClientProvider } from '@tanstack/react-query';
12
12
  import { attachQueryPersistence, createQueryClient } from './hooks/queryClient';
13
13
  const WebOxyContext = createContext(null);
14
14
  /**
15
- * Module-level run-once guard for FedCM silent sign-in.
15
+ * Module-level run-once guard for the provider's silent sign-in step.
16
16
  *
17
17
  * The init effect runs again whenever the provider remounts (route change,
18
18
  * StrictMode double-invoke, error-boundary recovery). The redirect-callback
19
- * and local-session-restore steps are cheap and idempotent, but the FedCM
20
- * `silentSignIn()` step triggers `navigator.credentials.get`, which must fire
21
- * AT MOST ONCE per page load otherwise a remount storm becomes a credential
22
- * request storm. Keyed by origin so the guard survives instance churn; never
23
- * cleared because only a fresh page load can change the IdP session state.
19
+ * and local-session-restore steps are cheap and idempotent, but
20
+ * `crossDomainAuth.silentSignIn()` is NOT: it first tries FedCM silent
21
+ * mediation (`navigator.credentials.get`) and, if that yields nothing, falls
22
+ * back to an iframe-based silent auth against `/auth/silent`.
23
+ *
24
+ * The FedCM leg is now centrally memoized inside `@oxyhq/core`'s
25
+ * `silentSignInWithFedCM` (at-most-once `navigator.credentials.get` per page
26
+ * load). The iframe fallback, however, is a SEPARATE mechanism the core guard
27
+ * does not cover — without this guard a remount storm would create a hidden
28
+ * iframe per remount. So this guard is intentionally retained to keep the
29
+ * provider's WHOLE silent step run-once. Keyed on `origin + baseURL` to match
30
+ * the core guard's keying (so the two stay in lockstep) and to survive
31
+ * instance churn; never cleared because only a fresh page load can change the
32
+ * IdP/iframe session state.
24
33
  */
25
- const fedcmSilentSignInAttempted = new Set();
26
- function silentSignInKey() {
27
- return typeof window !== 'undefined' ? window.location.origin : 'no-origin';
34
+ const silentSignInAttempted = new Set();
35
+ function silentSignInKey(oxyServices) {
36
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
37
+ let baseURL = '';
38
+ try {
39
+ baseURL = oxyServices.getBaseURL();
40
+ }
41
+ catch {
42
+ baseURL = '';
43
+ }
44
+ return `${origin}|${baseURL}`;
28
45
  }
29
46
  /**
30
47
  * Web-only Oxy Provider
@@ -127,12 +144,14 @@ export function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChang
127
144
  await authManager.signOut();
128
145
  }
129
146
  }
130
- // FedCM silent sign-in: run AT MOST ONCE per page load. A remount
131
- // (route change / StrictMode / error recovery) must not re-trigger
132
- // the browser credential request.
133
- const ssoKey = silentSignInKey();
134
- if (!fedcmSilentSignInAttempted.has(ssoKey)) {
135
- fedcmSilentSignInAttempted.add(ssoKey);
147
+ // Silent sign-in: run AT MOST ONCE per page load. A remount (route
148
+ // change / StrictMode / error recovery) must not re-trigger the
149
+ // browser credential request OR the iframe fallback. The FedCM leg is
150
+ // additionally memoized in core; this guard also covers the iframe
151
+ // fallback that core does not.
152
+ const ssoKey = silentSignInKey(oxyServices);
153
+ if (!silentSignInAttempted.has(ssoKey)) {
154
+ silentSignInAttempted.add(ssoKey);
136
155
  try {
137
156
  const session = await crossDomainAuth.silentSignIn();
138
157
  if (mounted && session?.user) {
@@ -15,35 +15,6 @@
15
15
  * @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
16
16
  */
17
17
  import { useEffect, useRef, useCallback } from 'react';
18
- /**
19
- * Module-level guard tracking which (origin + API) signatures have already
20
- * had a silent SSO attempt this page load.
21
- *
22
- * A per-component `useRef` guard resets whenever the provider remounts (route
23
- * churn, StrictMode double-invoke, error-boundary recovery), which previously
24
- * allowed silent SSO to re-fire and — combined with a routing redirect loop —
25
- * produced an accelerating `navigator.credentials.get` retry storm. Keying the
26
- * guard on a stable signature instead of the component instance makes silent
27
- * SSO fire EXACTLY ONCE per page load regardless of how many times the
28
- * provider mounts. The set is intentionally never cleared: a fresh page load
29
- * (the only thing that can change the answer) starts a fresh module scope.
30
- */
31
- const silentSSOAttempted = new Set();
32
- /**
33
- * Build a stable signature for the silent-SSO run-once guard. Two providers
34
- * pointed at the same API from the same origin share one attempt.
35
- */
36
- function ssoSignature(oxyServices) {
37
- const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
38
- let baseURL = '';
39
- try {
40
- baseURL = oxyServices.getBaseURL();
41
- }
42
- catch {
43
- baseURL = '';
44
- }
45
- return `${origin}|${baseURL}`;
46
- }
47
18
  /**
48
19
  * Check if we're running in a web browser environment (not React Native)
49
20
  */
@@ -148,12 +119,12 @@ export function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onErr
148
119
  }, [oxyServices, onSessionFound, onError, fedCMSupported]);
149
120
  // Auto-check SSO on mount (web only, FedCM only, not on auth domain).
150
121
  //
151
- // Run-once is enforced by TWO guards:
152
- // 1. `hasCheckedRef` cheap per-instance fast-path so effect re-runs
153
- // (from changing deps) within one mount never re-fire.
154
- // 2. `silentSSOAttempted` module-level, survives remounts/StrictMode so
155
- // silent SSO fires exactly once per page load even if the provider
156
- // unmounts and remounts.
122
+ // `hasCheckedRef` is a cheap per-instance fast-path so effect re-runs (from
123
+ // changing deps) within one mount never re-fire. The page-load run-once
124
+ // guarantee silent SSO invoking `navigator.credentials.get` AT MOST ONCE
125
+ // per page load across remounts / StrictMode / multiple consumers — lives in
126
+ // `@oxyhq/core`'s `silentSignInWithFedCM`, which memoizes the first silent
127
+ // attempt's result for this origin + API. The hook therefore calls it freely.
157
128
  useEffect(() => {
158
129
  if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
159
130
  if (isIdentityProvider()) {
@@ -161,22 +132,14 @@ export function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onErr
161
132
  }
162
133
  return;
163
134
  }
164
- const signature = ssoSignature(oxyServices);
165
- if (silentSSOAttempted.has(signature)) {
166
- // Already attempted this page load (e.g. before a remount) — do not
167
- // re-fire. Mark the local fast-path too so subsequent re-renders skip.
168
- hasCheckedRef.current = true;
169
- return;
170
- }
171
135
  hasCheckedRef.current = true;
172
- silentSSOAttempted.add(signature);
173
136
  if (fedCMSupported) {
174
137
  checkSSO();
175
138
  }
176
139
  else {
177
140
  onSSOUnavailable?.();
178
141
  }
179
- }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable, oxyServices]);
142
+ }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable]);
180
143
  return {
181
144
  checkSSO,
182
145
  signInWithFedCM,