@oxyhq/auth 2.0.4 → 2.0.6

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