@oxyhq/auth 2.0.6 → 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.
@@ -11,6 +11,38 @@ 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 the provider's silent sign-in step.
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
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.
33
+ */
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}`;
45
+ }
14
46
  /**
15
47
  * Web-only Oxy Provider
16
48
  *
@@ -112,15 +144,24 @@ export function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChang
112
144
  await authManager.signOut();
113
145
  }
114
146
  }
115
- try {
116
- const session = await crossDomainAuth.silentSignIn();
117
- if (mounted && session?.user) {
118
- await handleAuthSuccess(session, 'fedcm');
119
- return;
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);
155
+ try {
156
+ const session = await crossDomainAuth.silentSignIn();
157
+ if (mounted && session?.user) {
158
+ await handleAuthSuccess(session, 'fedcm');
159
+ return;
160
+ }
161
+ }
162
+ catch {
163
+ // Silent sign-in failed — resolve to unauthenticated below.
120
164
  }
121
- }
122
- catch {
123
- // Silent sign-in failed
124
165
  }
125
166
  if (mounted)
126
167
  setIsLoading(false);
@@ -117,7 +117,14 @@ export function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onErr
117
117
  isCheckingRef.current = false;
118
118
  }
119
119
  }, [oxyServices, onSessionFound, onError, fedCMSupported]);
120
- // Auto-check SSO on mount (web only, FedCM only, not on auth domain)
120
+ // Auto-check SSO on mount (web only, FedCM only, not on auth domain).
121
+ //
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.
121
128
  useEffect(() => {
122
129
  if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
123
130
  if (isIdentityProvider()) {