@oxyhq/auth 2.0.5 → 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.
Files changed (34) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/WebOxyProvider.js +30 -8
  3. package/dist/cjs/hooks/mutations/index.js +5 -1
  4. package/dist/cjs/hooks/mutations/useAppData.js +133 -0
  5. package/dist/cjs/hooks/queries/appDataQueryKeys.js +46 -0
  6. package/dist/cjs/hooks/queries/index.js +8 -1
  7. package/dist/cjs/hooks/queries/useAppData.js +87 -0
  8. package/dist/cjs/hooks/useWebSSO.js +46 -2
  9. package/dist/cjs/index.js +8 -2
  10. package/dist/esm/.tsbuildinfo +1 -1
  11. package/dist/esm/WebOxyProvider.js +30 -8
  12. package/dist/esm/hooks/mutations/index.js +2 -0
  13. package/dist/esm/hooks/mutations/useAppData.js +128 -0
  14. package/dist/esm/hooks/queries/appDataQueryKeys.js +42 -0
  15. package/dist/esm/hooks/queries/index.js +3 -0
  16. package/dist/esm/hooks/queries/useAppData.js +82 -0
  17. package/dist/esm/hooks/useWebSSO.js +46 -2
  18. package/dist/esm/index.js +2 -2
  19. package/dist/types/.tsbuildinfo +1 -1
  20. package/dist/types/hooks/mutations/index.d.ts +1 -0
  21. package/dist/types/hooks/mutations/useAppData.d.ts +47 -0
  22. package/dist/types/hooks/queries/appDataQueryKeys.d.ts +24 -0
  23. package/dist/types/hooks/queries/index.d.ts +2 -0
  24. package/dist/types/hooks/queries/useAppData.d.ts +46 -0
  25. package/dist/types/index.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/src/WebOxyProvider.tsx +31 -7
  28. package/src/hooks/mutations/index.ts +3 -0
  29. package/src/hooks/mutations/useAppData.ts +167 -0
  30. package/src/hooks/queries/appDataQueryKeys.ts +53 -0
  31. package/src/hooks/queries/index.ts +4 -0
  32. package/src/hooks/queries/useAppData.ts +105 -0
  33. package/src/hooks/useWebSSO.ts +48 -2
  34. package/src/index.ts +6 -0
@@ -16,6 +16,21 @@ const core_1 = require("@oxyhq/core");
16
16
  const react_query_1 = require("@tanstack/react-query");
17
17
  const queryClient_1 = require("./hooks/queryClient");
18
18
  const WebOxyContext = (0, react_1.createContext)(null);
19
+ /**
20
+ * Module-level run-once guard for FedCM silent sign-in.
21
+ *
22
+ * The init effect runs again whenever the provider remounts (route change,
23
+ * StrictMode double-invoke, error-boundary recovery). The redirect-callback
24
+ * and local-session-restore steps are cheap and idempotent, but the FedCM
25
+ * `silentSignIn()` step triggers `navigator.credentials.get`, which must fire
26
+ * AT MOST ONCE per page load — otherwise a remount storm becomes a credential
27
+ * request storm. Keyed by origin so the guard survives instance churn; never
28
+ * cleared because only a fresh page load can change the IdP session state.
29
+ */
30
+ const fedcmSilentSignInAttempted = new Set();
31
+ function silentSignInKey() {
32
+ return typeof window !== 'undefined' ? window.location.origin : 'no-origin';
33
+ }
19
34
  /**
20
35
  * Web-only Oxy Provider
21
36
  *
@@ -117,15 +132,22 @@ function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onEr
117
132
  await authManager.signOut();
118
133
  }
119
134
  }
120
- try {
121
- const session = await crossDomainAuth.silentSignIn();
122
- if (mounted && session?.user) {
123
- await handleAuthSuccess(session, 'fedcm');
124
- return;
135
+ // FedCM silent sign-in: run AT MOST ONCE per page load. A remount
136
+ // (route change / StrictMode / error recovery) must not re-trigger
137
+ // the browser credential request.
138
+ const ssoKey = silentSignInKey();
139
+ if (!fedcmSilentSignInAttempted.has(ssoKey)) {
140
+ fedcmSilentSignInAttempted.add(ssoKey);
141
+ try {
142
+ const session = await crossDomainAuth.silentSignIn();
143
+ if (mounted && session?.user) {
144
+ await handleAuthSuccess(session, 'fedcm');
145
+ return;
146
+ }
147
+ }
148
+ catch {
149
+ // Silent sign-in failed — resolve to unauthenticated below.
125
150
  }
126
- }
127
- catch {
128
- // Silent sign-in failed
129
151
  }
130
152
  if (mounted)
131
153
  setIsLoading(false);
@@ -6,7 +6,7 @@
6
6
  * All mutations handle authentication, error handling, and query invalidation.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = void 0;
9
+ exports.useDeleteAppData = exports.useSetAppData = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = void 0;
10
10
  // Account mutation hooks
11
11
  var useAccountMutations_1 = require("./useAccountMutations");
12
12
  Object.defineProperty(exports, "useUpdateProfile", { enumerable: true, get: function () { return useAccountMutations_1.useUpdateProfile; } });
@@ -21,3 +21,7 @@ Object.defineProperty(exports, "useLogoutSession", { enumerable: true, get: func
21
21
  Object.defineProperty(exports, "useLogoutAll", { enumerable: true, get: function () { return useServicesMutations_1.useLogoutAll; } });
22
22
  Object.defineProperty(exports, "useUpdateDeviceName", { enumerable: true, get: function () { return useServicesMutations_1.useUpdateDeviceName; } });
23
23
  Object.defineProperty(exports, "useRemoveDevice", { enumerable: true, get: function () { return useServicesMutations_1.useRemoveDevice; } });
24
+ // App-data KV store mutation hooks
25
+ var useAppData_1 = require("./useAppData");
26
+ Object.defineProperty(exports, "useSetAppData", { enumerable: true, get: function () { return useAppData_1.useSetAppData; } });
27
+ Object.defineProperty(exports, "useDeleteAppData", { enumerable: true, get: function () { return useAppData_1.useDeleteAppData; } });
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * App-Data Mutation Hooks
4
+ *
5
+ * Write side of the per-user JSON KV store. Both `useSetAppData` and
6
+ * `useDeleteAppData` apply optimistic updates against the two query keys
7
+ * that observe this data (`appDataQueryKeys.value` and the surrounding
8
+ * `appDataQueryKeys.namespace`) and roll back on error.
9
+ *
10
+ * When the underlying request fails because the endpoint isn't reachable
11
+ * (404 / network), the mutation still surfaces the error — write attempts
12
+ * are user-initiated and the caller may want to retry or fall back to
13
+ * local persistence. Reads are silent about missing endpoints; writes are
14
+ * not.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.useDeleteAppData = exports.useSetAppData = void 0;
18
+ const react_query_1 = require("@tanstack/react-query");
19
+ const core_1 = require("@oxyhq/core");
20
+ const WebOxyProvider_1 = require("../../WebOxyProvider");
21
+ const appDataQueryKeys_1 = require("../queries/appDataQueryKeys");
22
+ /**
23
+ * Upsert a per-user JSON value. Returns the value the server confirmed it
24
+ * stored — typically identical to the input but consumers should prefer the
25
+ * returned value (the server is the source of truth).
26
+ *
27
+ * Applies optimistic updates against both the single-value query key and
28
+ * the surrounding namespace query key, then rolls back on error.
29
+ */
30
+ const useSetAppData = () => {
31
+ const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
32
+ const queryClient = (0, react_query_1.useQueryClient)();
33
+ return (0, react_query_1.useMutation)({
34
+ mutationKey: ['appData', 'set'],
35
+ mutationFn: async ({ namespace, key, value }) => {
36
+ return (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.setAppData(namespace, key, value));
37
+ },
38
+ onMutate: async ({ namespace, key, value }) => {
39
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
40
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
41
+ await Promise.all([
42
+ queryClient.cancelQueries({ queryKey: valueKey }),
43
+ queryClient.cancelQueries({ queryKey: namespaceKey }),
44
+ ]);
45
+ const previousValue = queryClient.getQueryData(valueKey);
46
+ const previousNamespace = queryClient.getQueryData(namespaceKey);
47
+ queryClient.setQueryData(valueKey, value);
48
+ if (previousNamespace) {
49
+ queryClient.setQueryData(namespaceKey, {
50
+ ...previousNamespace,
51
+ [key]: value,
52
+ });
53
+ }
54
+ return { previousValue, previousNamespace };
55
+ },
56
+ onError: (_error, { namespace, key }, context) => {
57
+ if (!context)
58
+ return;
59
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
60
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
61
+ // Restore exactly the snapshots we captured in onMutate. Don't merge
62
+ // with whatever's currently in the cache — that could splice in writes
63
+ // from concurrent mutations and undo their state.
64
+ queryClient.setQueryData(valueKey, context.previousValue ?? null);
65
+ if (context.previousNamespace !== undefined) {
66
+ queryClient.setQueryData(namespaceKey, context.previousNamespace);
67
+ }
68
+ },
69
+ onSuccess: (data, { namespace, key }) => {
70
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
71
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
72
+ queryClient.setQueryData(valueKey, data);
73
+ const existingNamespace = queryClient.getQueryData(namespaceKey);
74
+ if (existingNamespace) {
75
+ queryClient.setQueryData(namespaceKey, {
76
+ ...existingNamespace,
77
+ [key]: data,
78
+ });
79
+ }
80
+ },
81
+ });
82
+ };
83
+ exports.useSetAppData = useSetAppData;
84
+ /**
85
+ * Delete a per-user JSON value. Optimistically removes the entry from the
86
+ * single-value cache and from the surrounding namespace map, then rolls back
87
+ * on error.
88
+ */
89
+ const useDeleteAppData = () => {
90
+ const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
91
+ const queryClient = (0, react_query_1.useQueryClient)();
92
+ return (0, react_query_1.useMutation)({
93
+ mutationKey: ['appData', 'delete'],
94
+ mutationFn: async ({ namespace, key }) => {
95
+ await (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.deleteAppData(namespace, key));
96
+ },
97
+ onMutate: async ({ namespace, key }) => {
98
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
99
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
100
+ await Promise.all([
101
+ queryClient.cancelQueries({ queryKey: valueKey }),
102
+ queryClient.cancelQueries({ queryKey: namespaceKey }),
103
+ ]);
104
+ const previousValue = queryClient.getQueryData(valueKey);
105
+ const previousNamespace = queryClient.getQueryData(namespaceKey);
106
+ queryClient.setQueryData(valueKey, null);
107
+ if (previousNamespace && key in previousNamespace) {
108
+ const next = { ...previousNamespace };
109
+ delete next[key];
110
+ queryClient.setQueryData(namespaceKey, next);
111
+ }
112
+ return { previousValue, previousNamespace };
113
+ },
114
+ onError: (_error, { namespace, key }, context) => {
115
+ if (!context)
116
+ return;
117
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
118
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
119
+ queryClient.setQueryData(valueKey, context.previousValue ?? null);
120
+ if (context.previousNamespace !== undefined) {
121
+ queryClient.setQueryData(namespaceKey, context.previousNamespace);
122
+ }
123
+ },
124
+ onSuccess: (_data, { namespace, key }) => {
125
+ queryClient.setQueryData(appDataQueryKeys_1.appDataQueryKeys.value(namespace, key), null);
126
+ // Confirm the value is gone from the namespace cache too. If the
127
+ // optimistic update wasn't applied (e.g. cache was empty), this is a
128
+ // no-op; if it was, we already removed it in onMutate, so this is also
129
+ // a no-op. The work happens in onMutate — onSuccess is the commit point.
130
+ },
131
+ });
132
+ };
133
+ exports.useDeleteAppData = useDeleteAppData;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * Query keys + error utilities for `useAppData` hooks.
4
+ *
5
+ * Lives next to the hook file so consumers can import the keys directly
6
+ * when they need to imperatively invalidate a value (e.g. after a non-React
7
+ * write through `oxyServices.setAppData`).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.appDataQueryKeys = void 0;
11
+ exports.isMissingAppDataEndpointError = isMissingAppDataEndpointError;
12
+ exports.appDataQueryKeys = {
13
+ all: ['appData'],
14
+ namespaces: () => [...exports.appDataQueryKeys.all, 'namespace'],
15
+ namespace: (namespace) => [...exports.appDataQueryKeys.namespaces(), namespace],
16
+ values: () => [...exports.appDataQueryKeys.all, 'value'],
17
+ value: (namespace, key) => [...exports.appDataQueryKeys.values(), namespace, key],
18
+ };
19
+ /**
20
+ * True when `error` indicates the app-data endpoint isn't reachable — either
21
+ * because the API deployment doesn't have it yet (404) or there's a network
22
+ * failure with no response. We treat these as "no value stored" so consumers
23
+ * fall back to local persistence without surfacing a user-facing error.
24
+ *
25
+ * Anything else (401, 403, 500) propagates normally — those are real bugs
26
+ * the auth or retry pipeline needs to see.
27
+ */
28
+ function isMissingAppDataEndpointError(error) {
29
+ if (!error || typeof error !== 'object') {
30
+ return false;
31
+ }
32
+ const candidate = error;
33
+ const status = candidate.status ?? candidate.statusCode ?? candidate.response?.status;
34
+ // 404: endpoint not deployed on this API instance yet.
35
+ if (status === 404)
36
+ return true;
37
+ // Network errors: no response received at all. Common during local dev
38
+ // when the API server is down, or when offline.
39
+ if (candidate.code === 'NETWORK_ERROR')
40
+ return true;
41
+ const message = typeof candidate.message === 'string' ? candidate.message : '';
42
+ if (message.includes('Network Error') || message.includes('Failed to fetch')) {
43
+ return true;
44
+ }
45
+ return false;
46
+ }
@@ -6,7 +6,7 @@
6
6
  * All hooks follow the same pattern with optional `enabled` parameter.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.invalidateSessionQueries = exports.invalidateUserQueries = exports.invalidateAccountQueries = exports.queryKeys = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = void 0;
9
+ exports.isMissingAppDataEndpointError = exports.appDataQueryKeys = exports.useAppDataNamespace = exports.useAppData = exports.invalidateSessionQueries = exports.invalidateUserQueries = exports.invalidateAccountQueries = exports.queryKeys = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = void 0;
10
10
  // Account and user query hooks
11
11
  var useAccountQueries_1 = require("./useAccountQueries");
12
12
  Object.defineProperty(exports, "useUserProfile", { enumerable: true, get: function () { return useAccountQueries_1.useUserProfile; } });
@@ -33,3 +33,10 @@ Object.defineProperty(exports, "queryKeys", { enumerable: true, get: function ()
33
33
  Object.defineProperty(exports, "invalidateAccountQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateAccountQueries; } });
34
34
  Object.defineProperty(exports, "invalidateUserQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateUserQueries; } });
35
35
  Object.defineProperty(exports, "invalidateSessionQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateSessionQueries; } });
36
+ // App-data KV store query hooks
37
+ var useAppData_1 = require("./useAppData");
38
+ Object.defineProperty(exports, "useAppData", { enumerable: true, get: function () { return useAppData_1.useAppData; } });
39
+ Object.defineProperty(exports, "useAppDataNamespace", { enumerable: true, get: function () { return useAppData_1.useAppDataNamespace; } });
40
+ var appDataQueryKeys_1 = require("./appDataQueryKeys");
41
+ Object.defineProperty(exports, "appDataQueryKeys", { enumerable: true, get: function () { return appDataQueryKeys_1.appDataQueryKeys; } });
42
+ Object.defineProperty(exports, "isMissingAppDataEndpointError", { enumerable: true, get: function () { return appDataQueryKeys_1.isMissingAppDataEndpointError; } });
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * App-Data Query Hooks
4
+ *
5
+ * Read side of the `/users/me/app-data/...` per-user JSON KV store. Gated on
6
+ * `isAuthenticated` — when signed out the query stays `enabled: false` and
7
+ * `data` is `null`, so consumers can fall back to localStorage without ever
8
+ * issuing a doomed request.
9
+ *
10
+ * Errors from the network (404 because the endpoint isn't deployed yet,
11
+ * 401 because the session lapsed, etc.) are not user-facing here. Hooks
12
+ * return `data: null` on error so the calling component renders the
13
+ * "nothing yet" state and the consuming app can quietly fall back to local
14
+ * persistence. Mutations still propagate errors so write attempts surface
15
+ * a toast — only reads are silent.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.useAppDataNamespace = exports.useAppData = void 0;
19
+ const react_query_1 = require("@tanstack/react-query");
20
+ const core_1 = require("@oxyhq/core");
21
+ const WebOxyProvider_1 = require("../../WebOxyProvider");
22
+ const appDataQueryKeys_1 = require("./appDataQueryKeys");
23
+ /**
24
+ * Read a single per-user JSON value.
25
+ *
26
+ * @param namespace - kebab/snake-case identifier (e.g. `"academy"`).
27
+ * @param key - kebab/snake-case identifier (e.g. course slug).
28
+ * @param options - optional `enabled`/`staleTime`/`gcTime` overrides.
29
+ *
30
+ * @returns A `useQuery` result with `data` of type `T | null`. The query
31
+ * stays disabled when the user is signed out; when enabled but the server
32
+ * has no stored value, `data` is `null`. Reads that fail because the
33
+ * endpoint isn't reachable also resolve to `null` so the consumer can
34
+ * fall back to local persistence.
35
+ */
36
+ const useAppData = (namespace, key, options) => {
37
+ const { oxyServices, activeSessionId, isAuthenticated } = (0, WebOxyProvider_1.useWebOxy)();
38
+ return (0, react_query_1.useQuery)({
39
+ queryKey: appDataQueryKeys_1.appDataQueryKeys.value(namespace, key),
40
+ queryFn: async () => {
41
+ try {
42
+ return await (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.getAppData(namespace, key));
43
+ }
44
+ catch (error) {
45
+ // Endpoint not deployed yet, no network, etc. — return null so the
46
+ // consumer falls back to localStorage rather than rendering a broken
47
+ // UI state. Authentication errors still bubble up so the auth retry
48
+ // pipeline can surface them at the provider level.
49
+ if ((0, appDataQueryKeys_1.isMissingAppDataEndpointError)(error)) {
50
+ return null;
51
+ }
52
+ throw error;
53
+ }
54
+ },
55
+ enabled: (options?.enabled !== false) && isAuthenticated,
56
+ staleTime: options?.staleTime ?? 60 * 1000,
57
+ gcTime: options?.gcTime ?? 30 * 60 * 1000,
58
+ });
59
+ };
60
+ exports.useAppData = useAppData;
61
+ /**
62
+ * Read every value in a namespace.
63
+ *
64
+ * @returns A `useQuery` result with `data` as a `Record<string, T>`. Empty
65
+ * object when the namespace contains nothing (or when fetching failed).
66
+ */
67
+ const useAppDataNamespace = (namespace, options) => {
68
+ const { oxyServices, activeSessionId, isAuthenticated } = (0, WebOxyProvider_1.useWebOxy)();
69
+ return (0, react_query_1.useQuery)({
70
+ queryKey: appDataQueryKeys_1.appDataQueryKeys.namespace(namespace),
71
+ queryFn: async () => {
72
+ try {
73
+ return await (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.listAppData(namespace));
74
+ }
75
+ catch (error) {
76
+ if ((0, appDataQueryKeys_1.isMissingAppDataEndpointError)(error)) {
77
+ return {};
78
+ }
79
+ throw error;
80
+ }
81
+ },
82
+ enabled: (options?.enabled !== false) && isAuthenticated,
83
+ staleTime: options?.staleTime ?? 60 * 1000,
84
+ gcTime: options?.gcTime ?? 30 * 60 * 1000,
85
+ });
86
+ };
87
+ exports.useAppDataNamespace = useAppDataNamespace;
@@ -19,6 +19,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.useWebSSO = useWebSSO;
20
20
  exports.isWebBrowser = isWebBrowser;
21
21
  const react_1 = require("react");
22
+ /**
23
+ * Module-level guard tracking which (origin + API) signatures have already
24
+ * had a silent SSO attempt this page load.
25
+ *
26
+ * A per-component `useRef` guard resets whenever the provider remounts (route
27
+ * churn, StrictMode double-invoke, error-boundary recovery), which previously
28
+ * allowed silent SSO to re-fire and — combined with a routing redirect loop —
29
+ * produced an accelerating `navigator.credentials.get` retry storm. Keying the
30
+ * guard on a stable signature instead of the component instance makes silent
31
+ * SSO fire EXACTLY ONCE per page load regardless of how many times the
32
+ * provider mounts. The set is intentionally never cleared: a fresh page load
33
+ * (the only thing that can change the answer) starts a fresh module scope.
34
+ */
35
+ const silentSSOAttempted = new Set();
36
+ /**
37
+ * Build a stable signature for the silent-SSO run-once guard. Two providers
38
+ * pointed at the same API from the same origin share one attempt.
39
+ */
40
+ function ssoSignature(oxyServices) {
41
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
42
+ let baseURL = '';
43
+ try {
44
+ baseURL = oxyServices.getBaseURL();
45
+ }
46
+ catch {
47
+ baseURL = '';
48
+ }
49
+ return `${origin}|${baseURL}`;
50
+ }
22
51
  /**
23
52
  * Check if we're running in a web browser environment (not React Native)
24
53
  */
@@ -121,7 +150,14 @@ function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onError, ena
121
150
  isCheckingRef.current = false;
122
151
  }
123
152
  }, [oxyServices, onSessionFound, onError, fedCMSupported]);
124
- // Auto-check SSO on mount (web only, FedCM only, not on auth domain)
153
+ // Auto-check SSO on mount (web only, FedCM only, not on auth domain).
154
+ //
155
+ // Run-once is enforced by TWO guards:
156
+ // 1. `hasCheckedRef` — cheap per-instance fast-path so effect re-runs
157
+ // (from changing deps) within one mount never re-fire.
158
+ // 2. `silentSSOAttempted` — module-level, survives remounts/StrictMode so
159
+ // silent SSO fires exactly once per page load even if the provider
160
+ // unmounts and remounts.
125
161
  (0, react_1.useEffect)(() => {
126
162
  if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
127
163
  if (isIdentityProvider()) {
@@ -129,14 +165,22 @@ function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onError, ena
129
165
  }
130
166
  return;
131
167
  }
168
+ const signature = ssoSignature(oxyServices);
169
+ if (silentSSOAttempted.has(signature)) {
170
+ // Already attempted this page load (e.g. before a remount) — do not
171
+ // re-fire. Mark the local fast-path too so subsequent re-renders skip.
172
+ hasCheckedRef.current = true;
173
+ return;
174
+ }
132
175
  hasCheckedRef.current = true;
176
+ silentSSOAttempted.add(signature);
133
177
  if (fedCMSupported) {
134
178
  checkSSO();
135
179
  }
136
180
  else {
137
181
  onSSOUnavailable?.();
138
182
  }
139
- }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable]);
183
+ }, [enabled, checkSSO, fedCMSupported, onSSOUnavailable, oxyServices]);
140
184
  return {
141
185
  checkSSO,
142
186
  signInWithFedCM,
package/dist/cjs/index.js CHANGED
@@ -24,8 +24,8 @@
24
24
  * ```
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.useAssets = exports.useSessionSocket = exports.isWebBrowser = exports.useWebSSO = exports.createGenericMutation = exports.createProfileMutation = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = exports.useFollowStore = exports.useAccountLoadingSession = exports.useAccountError = exports.useAccountLoading = exports.useAccounts = exports.useAccountStore = exports.useIsAssetLinked = exports.useAssetUsageCount = exports.useAssetsByEntity = exports.useAssetsByApp = exports.useAssetErrors = exports.useAssetLoading = exports.useUploadProgress = exports.useAsset = exports.useAssetsStore = exports.useAssetStore = exports.useAuthStore = exports.useAuth = exports.useWebOxy = exports.WebOxyProvider = void 0;
28
- exports.extractErrorMessage = exports.isTimeoutOrNetworkError = exports.isInvalidSessionError = exports.handleAuthError = exports.useFileFiltering = exports.useFollowerCounts = exports.useFollow = exports.useFileDownloadUrl = exports.setOxyAssetInstance = void 0;
27
+ exports.useDeleteAppData = exports.useSetAppData = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = exports.isMissingAppDataEndpointError = exports.appDataQueryKeys = exports.useAppDataNamespace = exports.useAppData = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = exports.useFollowStore = exports.useAccountLoadingSession = exports.useAccountError = exports.useAccountLoading = exports.useAccounts = exports.useAccountStore = exports.useIsAssetLinked = exports.useAssetUsageCount = exports.useAssetsByEntity = exports.useAssetsByApp = exports.useAssetErrors = exports.useAssetLoading = exports.useUploadProgress = exports.useAsset = exports.useAssetsStore = exports.useAssetStore = exports.useAuthStore = exports.useAuth = exports.useWebOxy = exports.WebOxyProvider = void 0;
28
+ exports.extractErrorMessage = exports.isTimeoutOrNetworkError = exports.isInvalidSessionError = exports.handleAuthError = exports.useFileFiltering = exports.useFollowerCounts = exports.useFollow = exports.useFileDownloadUrl = exports.setOxyAssetInstance = exports.useAssets = exports.useSessionSocket = exports.isWebBrowser = exports.useWebSSO = exports.createGenericMutation = exports.createProfileMutation = void 0;
29
29
  // --- Provider & Hooks ---
30
30
  var WebOxyProvider_1 = require("./WebOxyProvider");
31
31
  Object.defineProperty(exports, "WebOxyProvider", { enumerable: true, get: function () { return WebOxyProvider_1.WebOxyProvider; } });
@@ -69,6 +69,10 @@ Object.defineProperty(exports, "useUserDevices", { enumerable: true, get: functi
69
69
  Object.defineProperty(exports, "useSecurityInfo", { enumerable: true, get: function () { return queries_1.useSecurityInfo; } });
70
70
  Object.defineProperty(exports, "useSecurityActivity", { enumerable: true, get: function () { return queries_1.useSecurityActivity; } });
71
71
  Object.defineProperty(exports, "useRecentSecurityActivity", { enumerable: true, get: function () { return queries_1.useRecentSecurityActivity; } });
72
+ Object.defineProperty(exports, "useAppData", { enumerable: true, get: function () { return queries_1.useAppData; } });
73
+ Object.defineProperty(exports, "useAppDataNamespace", { enumerable: true, get: function () { return queries_1.useAppDataNamespace; } });
74
+ Object.defineProperty(exports, "appDataQueryKeys", { enumerable: true, get: function () { return queries_1.appDataQueryKeys; } });
75
+ Object.defineProperty(exports, "isMissingAppDataEndpointError", { enumerable: true, get: function () { return queries_1.isMissingAppDataEndpointError; } });
72
76
  // --- Mutation Hooks ---
73
77
  var mutations_1 = require("./hooks/mutations");
74
78
  Object.defineProperty(exports, "useUpdateProfile", { enumerable: true, get: function () { return mutations_1.useUpdateProfile; } });
@@ -81,6 +85,8 @@ Object.defineProperty(exports, "useLogoutSession", { enumerable: true, get: func
81
85
  Object.defineProperty(exports, "useLogoutAll", { enumerable: true, get: function () { return mutations_1.useLogoutAll; } });
82
86
  Object.defineProperty(exports, "useUpdateDeviceName", { enumerable: true, get: function () { return mutations_1.useUpdateDeviceName; } });
83
87
  Object.defineProperty(exports, "useRemoveDevice", { enumerable: true, get: function () { return mutations_1.useRemoveDevice; } });
88
+ Object.defineProperty(exports, "useSetAppData", { enumerable: true, get: function () { return mutations_1.useSetAppData; } });
89
+ Object.defineProperty(exports, "useDeleteAppData", { enumerable: true, get: function () { return mutations_1.useDeleteAppData; } });
84
90
  var mutationFactory_1 = require("./hooks/mutations/mutationFactory");
85
91
  Object.defineProperty(exports, "createProfileMutation", { enumerable: true, get: function () { return mutationFactory_1.createProfileMutation; } });
86
92
  Object.defineProperty(exports, "createGenericMutation", { enumerable: true, get: function () { return mutationFactory_1.createGenericMutation; } });