@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/auth",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "OxyHQ Web Authentication SDK — headless auth with React hooks for Next.js, Vite, and web apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -58,6 +58,39 @@ export interface WebOxyContextValue extends WebAuthState, WebAuthActions {
58
58
 
59
59
  const WebOxyContext = createContext<WebOxyContextValue | null>(null);
60
60
 
61
+ /**
62
+ * Module-level run-once guard for the provider's silent sign-in step.
63
+ *
64
+ * The init effect runs again whenever the provider remounts (route change,
65
+ * StrictMode double-invoke, error-boundary recovery). The redirect-callback
66
+ * and local-session-restore steps are cheap and idempotent, but
67
+ * `crossDomainAuth.silentSignIn()` is NOT: it first tries FedCM silent
68
+ * mediation (`navigator.credentials.get`) and, if that yields nothing, falls
69
+ * back to an iframe-based silent auth against `/auth/silent`.
70
+ *
71
+ * The FedCM leg is now centrally memoized inside `@oxyhq/core`'s
72
+ * `silentSignInWithFedCM` (at-most-once `navigator.credentials.get` per page
73
+ * load). The iframe fallback, however, is a SEPARATE mechanism the core guard
74
+ * does not cover — without this guard a remount storm would create a hidden
75
+ * iframe per remount. So this guard is intentionally retained to keep the
76
+ * provider's WHOLE silent step run-once. Keyed on `origin + baseURL` to match
77
+ * the core guard's keying (so the two stay in lockstep) and to survive
78
+ * instance churn; never cleared because only a fresh page load can change the
79
+ * IdP/iframe session state.
80
+ */
81
+ const silentSignInAttempted = new Set<string>();
82
+
83
+ function silentSignInKey(oxyServices: OxyServices): string {
84
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
85
+ let baseURL = '';
86
+ try {
87
+ baseURL = oxyServices.getBaseURL();
88
+ } catch {
89
+ baseURL = '';
90
+ }
91
+ return `${origin}|${baseURL}`;
92
+ }
93
+
61
94
  export interface WebOxyProviderProps {
62
95
  children: ReactNode;
63
96
  baseURL: string;
@@ -191,14 +224,23 @@ export function WebOxyProvider({
191
224
  }
192
225
  }
193
226
 
194
- try {
195
- const session = await crossDomainAuth.silentSignIn();
196
- if (mounted && session?.user) {
197
- await handleAuthSuccess(session, 'fedcm');
198
- return;
227
+ // Silent sign-in: run AT MOST ONCE per page load. A remount (route
228
+ // change / StrictMode / error recovery) must not re-trigger the
229
+ // browser credential request OR the iframe fallback. The FedCM leg is
230
+ // additionally memoized in core; this guard also covers the iframe
231
+ // fallback that core does not.
232
+ const ssoKey = silentSignInKey(oxyServices);
233
+ if (!silentSignInAttempted.has(ssoKey)) {
234
+ silentSignInAttempted.add(ssoKey);
235
+ try {
236
+ const session = await crossDomainAuth.silentSignIn();
237
+ if (mounted && session?.user) {
238
+ await handleAuthSuccess(session, 'fedcm');
239
+ return;
240
+ }
241
+ } catch {
242
+ // Silent sign-in failed — resolve to unauthenticated below.
199
243
  }
200
- } catch {
201
- // Silent sign-in failed
202
244
  }
203
245
 
204
246
  if (mounted) setIsLoading(false);
@@ -158,7 +158,14 @@ export function useWebSSO({
158
158
  }
159
159
  }, [oxyServices, onSessionFound, onError, fedCMSupported]);
160
160
 
161
- // Auto-check SSO on mount (web only, FedCM only, not on auth domain)
161
+ // Auto-check SSO on mount (web only, FedCM only, not on auth domain).
162
+ //
163
+ // `hasCheckedRef` is a cheap per-instance fast-path so effect re-runs (from
164
+ // changing deps) within one mount never re-fire. The page-load run-once
165
+ // guarantee — silent SSO invoking `navigator.credentials.get` AT MOST ONCE
166
+ // per page load across remounts / StrictMode / multiple consumers — lives in
167
+ // `@oxyhq/core`'s `silentSignInWithFedCM`, which memoizes the first silent
168
+ // attempt's result for this origin + API. The hook therefore calls it freely.
162
169
  useEffect(() => {
163
170
  if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
164
171
  if (isIdentityProvider()) {