@oxyhq/auth 2.0.3 → 2.0.5

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 (42) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/WebOxyProvider.js +37 -0
  3. package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
  4. package/dist/cjs/hooks/queryClient.js +136 -92
  5. package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
  6. package/dist/cjs/hooks/useSessionSocket.js +250 -115
  7. package/dist/cjs/index.js +13 -3
  8. package/dist/cjs/stores/accountStore.js +2 -2
  9. package/dist/cjs/utils/sessionHelpers.js +4 -2
  10. package/dist/cjs/utils/storageHelpers.js +37 -11
  11. package/dist/esm/.tsbuildinfo +1 -1
  12. package/dist/esm/WebOxyProvider.js +38 -1
  13. package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
  14. package/dist/esm/hooks/queryClient.js +132 -89
  15. package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
  16. package/dist/esm/hooks/useSessionSocket.js +217 -112
  17. package/dist/esm/index.js +4 -1
  18. package/dist/esm/stores/accountStore.js +2 -2
  19. package/dist/esm/utils/sessionHelpers.js +4 -2
  20. package/dist/esm/utils/storageHelpers.js +37 -11
  21. package/dist/types/.tsbuildinfo +1 -1
  22. package/dist/types/WebOxyProvider.d.ts +1 -1
  23. package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
  24. package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
  25. package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
  26. package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
  27. package/dist/types/hooks/queryClient.d.ts +24 -10
  28. package/dist/types/hooks/useAssets.d.ts +1 -1
  29. package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
  30. package/dist/types/index.d.ts +5 -1
  31. package/dist/types/utils/sessionHelpers.d.ts +3 -1
  32. package/package.json +29 -5
  33. package/src/WebOxyProvider.tsx +39 -1
  34. package/src/hooks/mutations/useAccountMutations.ts +230 -57
  35. package/src/hooks/queryClient.ts +140 -83
  36. package/src/hooks/useFileDownloadUrl.ts +15 -39
  37. package/src/hooks/useSessionSocket.ts +273 -112
  38. package/src/index.ts +13 -1
  39. package/src/stores/accountStore.ts +2 -2
  40. package/src/utils/sessionHelpers.ts +4 -2
  41. package/src/utils/storageHelpers.ts +50 -11
  42. package/src/global.d.ts +0 -1
@@ -1,112 +1,169 @@
1
- import { QueryClient } from '@tanstack/react-query';
1
+ /**
2
+ * Web QueryClient with offline-first defaults + localStorage persistence.
3
+ *
4
+ * Mirrors the persistence behaviour in `@oxyhq/services/queryClient` so
5
+ * web auth apps (FedCM, popup, redirect flows) survive a page reload with
6
+ * cached identity + paused mutations intact.
7
+ *
8
+ * Persistence is opt-in via `attachQueryPersistence(...)` so SSR callers
9
+ * (Next.js getServerSideProps, Vite SSR, tests) can create a stateless
10
+ * client without touching `window`.
11
+ */
12
+
13
+ import { QueryClient, type Mutation, type Query } from '@tanstack/react-query';
14
+ import {
15
+ persistQueryClient,
16
+ type PersistedClient,
17
+ } from '@tanstack/react-query-persist-client';
18
+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
2
19
  import type { StorageInterface } from '../utils/storageHelpers';
3
20
 
4
- const QUERY_CACHE_KEY = 'oxy_query_cache';
5
- const QUERY_CACHE_VERSION = '1';
21
+ const QUERY_CACHE_KEY = 'oxy_auth_query_cache_v2';
22
+ const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
23
+ const QUERY_PERSIST_THROTTLE_MS = 1_000;
24
+
25
+ /**
26
+ * Query-key prefixes whose data is safe to restore across reloads.
27
+ * Web auth surfaces are session/profile heavy — lists and history are not
28
+ * persisted to keep the localStorage footprint small.
29
+ */
30
+ const PERSISTED_QUERY_PREFIXES: ReadonlyArray<string> = [
31
+ 'accounts',
32
+ 'users',
33
+ 'sessions',
34
+ 'auth',
35
+ ];
36
+
37
+ function shouldDehydrateQuery(query: Query): boolean {
38
+ if (query.state.status !== 'success') return false;
39
+ const head = query.queryKey[0];
40
+ return typeof head === 'string' && PERSISTED_QUERY_PREFIXES.includes(head);
41
+ }
42
+
43
+ function shouldDehydrateMutation(_mutation: Mutation): boolean {
44
+ return true;
45
+ }
6
46
 
7
47
  /**
8
- * Custom persistence adapter for TanStack Query using our StorageInterface
48
+ * Best-effort detection works in browsers, Node SSR, and React Server
49
+ * Components. `localStorage` is gated behind `window` because Node and edge
50
+ * runtimes may polyfill `globalThis.localStorage` inconsistently.
9
51
  */
10
- export const createPersistenceAdapter = (storage: StorageInterface) => {
11
- return {
12
- persistClient: async (client: any) => {
13
- try {
14
- const serialized = JSON.stringify({
15
- clientState: client,
16
- timestamp: Date.now(),
17
- version: QUERY_CACHE_VERSION,
18
- });
19
- await storage.setItem(QUERY_CACHE_KEY, serialized);
20
- } catch (error) {
21
- if (__DEV__) {
22
- console.warn('[QueryClient] Failed to persist cache:', error);
23
- }
52
+ function getBrowserLocalStorage(): Storage | null {
53
+ if (typeof window === 'undefined') return null;
54
+ try {
55
+ if (!window.localStorage) return null;
56
+ return window.localStorage;
57
+ } catch {
58
+ // Access blocked (Safari Private Mode, sandboxed iframe, etc.)
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export const createPersistenceAdapter = (storage: StorageInterface) => ({
64
+ persistClient: async (client: unknown): Promise<void> => {
65
+ try {
66
+ await storage.setItem(QUERY_CACHE_KEY, JSON.stringify(client));
67
+ } catch (error) {
68
+ if (process.env.NODE_ENV !== 'production') {
69
+ console.warn('[QueryClient] Failed to persist cache', error);
24
70
  }
25
- },
26
- restoreClient: async () => {
27
- try {
28
- const cached = await storage.getItem(QUERY_CACHE_KEY);
29
- if (!cached) return undefined;
30
-
31
- const parsed = JSON.parse(cached);
32
-
33
- // Check version compatibility
34
- if (parsed.version !== QUERY_CACHE_VERSION) {
35
- // Clear old cache on version mismatch
36
- await storage.removeItem(QUERY_CACHE_KEY);
37
- return undefined;
38
- }
39
-
40
- // Check if cache is too old (30 days)
41
- const maxAge = 30 * 24 * 60 * 60 * 1000;
42
- if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
43
- await storage.removeItem(QUERY_CACHE_KEY);
44
- return undefined;
45
- }
46
-
47
- return parsed.clientState;
48
- } catch (error) {
49
- if (__DEV__) {
50
- console.warn('[QueryClient] Failed to restore cache:', error);
51
- }
52
- return undefined;
71
+ }
72
+ },
73
+ restoreClient: async (): Promise<unknown> => {
74
+ try {
75
+ const cached = await storage.getItem(QUERY_CACHE_KEY);
76
+ return cached ? JSON.parse(cached) : undefined;
77
+ } catch (error) {
78
+ if (process.env.NODE_ENV !== 'production') {
79
+ console.warn('[QueryClient] Failed to restore cache', error);
53
80
  }
54
- },
55
- removeClient: async () => {
56
- try {
57
- await storage.removeItem(QUERY_CACHE_KEY);
58
- } catch (error) {
59
- if (__DEV__) {
60
- console.warn('[QueryClient] Failed to remove cache:', error);
61
- }
81
+ return undefined;
82
+ }
83
+ },
84
+ removeClient: async (): Promise<void> => {
85
+ try {
86
+ await storage.removeItem(QUERY_CACHE_KEY);
87
+ } catch (error) {
88
+ if (process.env.NODE_ENV !== 'production') {
89
+ console.warn('[QueryClient] Failed to remove cache', error);
62
90
  }
63
- },
64
- };
65
- };
91
+ }
92
+ },
93
+ });
66
94
 
67
- /**
68
- * Create a QueryClient with offline-first configuration
69
- */
70
- export const createQueryClient = (storage?: StorageInterface | null): QueryClient => {
71
- const client = new QueryClient({
95
+ export const createQueryClient = (): QueryClient =>
96
+ new QueryClient({
72
97
  defaultOptions: {
73
98
  queries: {
74
- // Data is fresh for 5 minutes
75
99
  staleTime: 5 * 60 * 1000,
76
- // Keep unused data in cache for 10 minutes
77
- gcTime: 10 * 60 * 1000,
78
- // Retry 3 times with exponential backoff
100
+ gcTime: 30 * 60 * 1000,
79
101
  retry: 3,
80
- retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
81
- // Refetch on reconnect
102
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
82
103
  refetchOnReconnect: true,
83
- // Don't refetch on window focus (better for mobile)
84
104
  refetchOnWindowFocus: false,
85
- // Offline-first: use cache when offline
86
105
  networkMode: 'offlineFirst',
87
106
  },
88
107
  mutations: {
89
- // Retry once for mutations
90
108
  retry: 1,
91
- // Offline-first: queue mutations when offline
92
109
  networkMode: 'offlineFirst',
93
110
  },
94
111
  },
95
112
  });
96
113
 
97
- // Note: Persistence is handled by TanStack Query's built-in persistence
98
- // For now, we rely on the query client's default behavior with networkMode: 'offlineFirst'
99
- // The cache will be available in memory and queries will use cached data when offline
100
- // Full persistence to AsyncStorage can be added later with @tanstack/react-query-persist-client if needed
101
-
102
- return client;
103
- };
114
+ export interface AttachPersistenceResult {
115
+ restored: Promise<void>;
116
+ unsubscribe: () => void;
117
+ }
104
118
 
105
119
  /**
106
- * Clear persisted query cache
120
+ * Wire `persistQueryClient` to browser `localStorage` (or a no-op when not
121
+ * in a browser). Returns the restore promise so consumers can `await` it
122
+ * before exposing the client to <Suspense> boundaries.
107
123
  */
108
- export const clearQueryCache = async (storage: StorageInterface): Promise<void> => {
109
- const adapter = createPersistenceAdapter(storage);
110
- await adapter.removeClient();
124
+ export const attachQueryPersistence = (
125
+ queryClient: QueryClient,
126
+ ): AttachPersistenceResult => {
127
+ const localStorage = getBrowserLocalStorage();
128
+ if (!localStorage) {
129
+ return { restored: Promise.resolve(), unsubscribe: () => {} };
130
+ }
131
+
132
+ const persister = createSyncStoragePersister({
133
+ storage: localStorage,
134
+ key: QUERY_CACHE_KEY,
135
+ throttleTime: QUERY_PERSIST_THROTTLE_MS,
136
+ });
137
+
138
+ const [unsubscribe, restored] = persistQueryClient({
139
+ queryClient,
140
+ persister,
141
+ maxAge: QUERY_CACHE_MAX_AGE,
142
+ dehydrateOptions: {
143
+ shouldDehydrateQuery,
144
+ shouldDehydrateMutation,
145
+ },
146
+ });
147
+
148
+ restored.catch((error) => {
149
+ if (process.env.NODE_ENV !== 'production') {
150
+ console.warn('[QueryClient] Failed to restore persisted cache', error);
151
+ }
152
+ });
153
+
154
+ return { unsubscribe, restored };
155
+ };
156
+
157
+ export const clearQueryCache = async (
158
+ storage: StorageInterface,
159
+ ): Promise<void> => {
160
+ try {
161
+ await storage.removeItem(QUERY_CACHE_KEY);
162
+ } catch (error) {
163
+ if (process.env.NODE_ENV !== 'production') {
164
+ console.warn('[QueryClient] Failed to remove cache', error);
165
+ }
166
+ }
111
167
  };
112
168
 
169
+ export type { PersistedClient };
@@ -1,11 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { OxyServices } from '@oxyhq/core';
3
-
4
- let oxyInstance: OxyServices | null = null;
5
-
6
- export const setOxyFileUrlInstance = (instance: OxyServices) => {
7
- oxyInstance = instance;
8
- };
2
+ import type { OxyServices } from '@oxyhq/core';
9
3
 
10
4
  export interface UseFileDownloadUrlOptions {
11
5
  variant?: string;
@@ -21,38 +15,21 @@ export interface UseFileDownloadUrlResult {
21
15
  /**
22
16
  * Hook to resolve a file's download URL asynchronously.
23
17
  *
24
- * Prefers the provided `oxyServices` instance, falls back to the module-level
25
- * singleton set via `setOxyFileUrlInstance`.
26
- *
27
18
  * Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
28
19
  * `getFileDownloadUrl` if the async call fails.
29
20
  */
30
21
  export const useFileDownloadUrl = (
31
- fileIdOrServices?: string | OxyServices | null,
32
- fileIdOrOptions?: string | UseFileDownloadUrlOptions | null,
33
- maybeOptions?: UseFileDownloadUrlOptions
22
+ oxyServices: OxyServices | null | undefined,
23
+ fileId: string | null | undefined,
24
+ options?: UseFileDownloadUrlOptions,
34
25
  ): UseFileDownloadUrlResult => {
35
- // Support two call signatures:
36
- // 1. useFileDownloadUrl(oxyServices, fileId, options) — preferred
37
- // 2. useFileDownloadUrl(fileId, options) — legacy (uses singleton)
38
- let services: OxyServices | null;
39
- let fileId: string | null | undefined;
40
- let options: UseFileDownloadUrlOptions | undefined;
41
-
42
- if (fileIdOrServices instanceof OxyServices) {
43
- services = fileIdOrServices;
44
- fileId = typeof fileIdOrOptions === 'string' ? fileIdOrOptions : null;
45
- options = maybeOptions;
46
- } else {
47
- services = oxyInstance;
48
- fileId = typeof fileIdOrServices === 'string' ? fileIdOrServices : null;
49
- options = typeof fileIdOrOptions === 'object' && fileIdOrOptions !== null ? fileIdOrOptions as UseFileDownloadUrlOptions : undefined;
50
- }
51
-
52
26
  const [url, setUrl] = useState<string | null>(null);
53
27
  const [loading, setLoading] = useState(false);
54
28
  const [error, setError] = useState<Error | null>(null);
55
29
 
30
+ const variant = options?.variant;
31
+ const expiresIn = options?.expiresIn;
32
+
56
33
  useEffect(() => {
57
34
  if (!fileId) {
58
35
  setUrl(null);
@@ -61,7 +38,7 @@ export const useFileDownloadUrl = (
61
38
  return;
62
39
  }
63
40
 
64
- if (!services) {
41
+ if (!oxyServices) {
65
42
  setUrl(null);
66
43
  setLoading(false);
67
44
  setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
@@ -69,22 +46,22 @@ export const useFileDownloadUrl = (
69
46
  }
70
47
 
71
48
  let cancelled = false;
72
- const instance = services;
49
+ const instance = oxyServices;
50
+ const targetFileId = fileId;
73
51
 
74
52
  const load = async () => {
75
53
  setLoading(true);
76
54
  setError(null);
77
55
 
78
56
  try {
79
- const { variant, expiresIn } = options || {};
80
57
  let resolvedUrl: string | null = null;
81
58
 
82
59
  if (typeof instance.getFileDownloadUrlAsync === 'function') {
83
- resolvedUrl = await instance.getFileDownloadUrlAsync(fileId!, variant, expiresIn);
60
+ resolvedUrl = await instance.getFileDownloadUrlAsync(targetFileId, variant, expiresIn);
84
61
  }
85
62
 
86
63
  if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
87
- resolvedUrl = instance.getFileDownloadUrl(fileId!, variant, expiresIn);
64
+ resolvedUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
88
65
  }
89
66
 
90
67
  if (!cancelled) {
@@ -94,8 +71,7 @@ export const useFileDownloadUrl = (
94
71
  // Fallback to sync URL on error where possible
95
72
  try {
96
73
  if (typeof instance.getFileDownloadUrl === 'function') {
97
- const { variant, expiresIn } = options || {};
98
- const fallbackUrl = instance.getFileDownloadUrl(fileId!, variant, expiresIn);
74
+ const fallbackUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
99
75
  if (!cancelled) {
100
76
  setUrl(fallbackUrl || null);
101
77
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -103,7 +79,7 @@ export const useFileDownloadUrl = (
103
79
  return;
104
80
  }
105
81
  } catch {
106
- // ignore secondary failure
82
+ // Secondary failure: surface the original error below.
107
83
  }
108
84
 
109
85
  if (!cancelled) {
@@ -121,7 +97,7 @@ export const useFileDownloadUrl = (
121
97
  return () => {
122
98
  cancelled = true;
123
99
  };
124
- }, [fileId, services, options?.variant, options?.expiresIn]);
100
+ }, [fileId, oxyServices, variant, expiresIn]);
125
101
 
126
102
  return { url, loading, error };
127
103
  };