@oxyhq/auth 2.0.6 → 2.0.7

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.
@@ -11,6 +11,21 @@ import { OxyServices, CrossDomainAuth, createAuthManager, } from '@oxyhq/core';
11
11
  import { QueryClientProvider } from '@tanstack/react-query';
12
12
  import { attachQueryPersistence, createQueryClient } from './hooks/queryClient';
13
13
  const WebOxyContext = createContext(null);
14
+ /**
15
+ * Module-level run-once guard for FedCM silent sign-in.
16
+ *
17
+ * The init effect runs again whenever the provider remounts (route change,
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.
24
+ */
25
+ const fedcmSilentSignInAttempted = new Set();
26
+ function silentSignInKey() {
27
+ return typeof window !== 'undefined' ? window.location.origin : 'no-origin';
28
+ }
14
29
  /**
15
30
  * Web-only Oxy Provider
16
31
  *
@@ -112,15 +127,22 @@ export function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChang
112
127
  await authManager.signOut();
113
128
  }
114
129
  }
115
- try {
116
- const session = await crossDomainAuth.silentSignIn();
117
- if (mounted && session?.user) {
118
- await handleAuthSuccess(session, 'fedcm');
119
- return;
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);
136
+ try {
137
+ const session = await crossDomainAuth.silentSignIn();
138
+ if (mounted && session?.user) {
139
+ await handleAuthSuccess(session, 'fedcm');
140
+ return;
141
+ }
142
+ }
143
+ catch {
144
+ // Silent sign-in failed — resolve to unauthenticated below.
120
145
  }
121
- }
122
- catch {
123
- // Silent sign-in failed
124
146
  }
125
147
  if (mounted)
126
148
  setIsLoading(false);
@@ -15,6 +15,35 @@
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
+ }
18
47
  /**
19
48
  * Check if we're running in a web browser environment (not React Native)
20
49
  */
@@ -117,7 +146,14 @@ export function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onErr
117
146
  isCheckingRef.current = false;
118
147
  }
119
148
  }, [oxyServices, onSessionFound, onError, fedCMSupported]);
120
- // Auto-check SSO on mount (web only, FedCM only, not on auth domain)
149
+ // Auto-check SSO on mount (web only, FedCM only, not on auth domain).
150
+ //
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.
121
157
  useEffect(() => {
122
158
  if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
123
159
  if (isIdentityProvider()) {
@@ -125,14 +161,22 @@ export function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onErr
125
161
  }
126
162
  return;
127
163
  }
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
+ }
128
171
  hasCheckedRef.current = true;
172
+ silentSSOAttempted.add(signature);
129
173
  if (fedCMSupported) {
130
174
  checkSSO();
131
175
  }
132
176
  else {
133
177
  onSSOUnavailable?.();
134
178
  }
135
- }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable]);
179
+ }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable, oxyServices]);
136
180
  return {
137
181
  checkSSO,
138
182
  signInWithFedCM,