@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,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;
@@ -1,11 +1,40 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
5
35
  Object.defineProperty(exports, "__esModule", { value: true });
6
36
  exports.useSessionSocket = useSessionSocket;
7
37
  const react_1 = require("react");
8
- const socket_io_client_1 = __importDefault(require("socket.io-client"));
9
38
  const sonner_1 = require("sonner");
10
39
  const core_1 = require("@oxyhq/core");
11
40
  const core_2 = require("@oxyhq/core");
@@ -14,6 +43,47 @@ const react_query_1 = require("@tanstack/react-query");
14
43
  const useServicesQueries_1 = require("./queries/useServicesQueries");
15
44
  const queryKeys_1 = require("./queries/queryKeys");
16
45
  const debug = (0, core_2.createDebugLogger)('SessionSocket');
46
+ /** localStorage key used by AuthManager for persisting access tokens. */
47
+ const LS_ACCESS_TOKEN_KEY = 'oxy_access_token';
48
+ /** Delay before retrying socket connection after an auth failure (ms). */
49
+ const AUTH_RETRY_DELAY_MS = 2000;
50
+ /** Maximum number of consecutive auth-failure retries. */
51
+ const MAX_AUTH_RETRIES = 3;
52
+ /**
53
+ * Read the access token from localStorage directly.
54
+ * Used as a fallback when the in-memory token is empty (e.g., during a
55
+ * cross-tab token refresh race).
56
+ */
57
+ function readTokenFromStorage() {
58
+ if (typeof window === 'undefined')
59
+ return null;
60
+ try {
61
+ return window.localStorage.getItem(LS_ACCESS_TOKEN_KEY);
62
+ }
63
+ catch (err) {
64
+ console.warn('[oxy.session-socket] localStorage read failed:', err);
65
+ return null;
66
+ }
67
+ }
68
+ let _io = null;
69
+ let _ioLoadAttempted = false;
70
+ async function getSocketIO() {
71
+ if (_io)
72
+ return _io;
73
+ if (_ioLoadAttempted)
74
+ return null;
75
+ _ioLoadAttempted = true;
76
+ try {
77
+ const mod = (await Promise.resolve().then(() => __importStar(require('socket.io-client'))));
78
+ _io = mod.io ?? mod.default ?? null;
79
+ return _io;
80
+ }
81
+ catch (err) {
82
+ console.warn('[oxy.session-socket] socket.io-client import failed:', err);
83
+ debug.warn('socket.io-client is not installed. useSessionSocket will be disabled. Install it with: bun add socket.io-client');
84
+ return null;
85
+ }
86
+ }
17
87
  function useSessionSocket(options) {
18
88
  const { user, activeSessionId, oxyServices, signOut, clearSessionState } = (0, WebOxyProvider_1.useWebOxy)();
19
89
  const queryClient = (0, react_query_1.useQueryClient)();
@@ -58,137 +128,202 @@ function useSessionSocket(options) {
58
128
  socketRef.current.disconnect();
59
129
  socketRef.current = null;
60
130
  }
61
- // Connect with auth token; use callback so reconnections get a fresh token
62
- socketRef.current = (0, socket_io_client_1.default)(baseURL, {
63
- transports: ['websocket'],
64
- auth: (cb) => {
65
- const token = oxyServices.getAccessToken();
66
- cb({ token: token ?? '' });
67
- },
68
- });
69
- const socket = socketRef.current;
70
- // Server auto-joins the user to `user:<userId>` room on connection
71
- const handleConnect = () => {
72
- debug.log('Socket connected:', socket.id);
73
- };
74
- const refreshSessions = () => {
75
- (0, queryKeys_1.invalidateSessionQueries)(queryClientRef.current);
76
- return Promise.resolve();
131
+ let cancelled = false;
132
+ let authRetryCount = 0;
133
+ let authRetryTimer = null;
134
+ /**
135
+ * Resolve the best available access token.
136
+ * Prefers the in-memory token from OxyServices; falls back to
137
+ * localStorage which may have been updated by another tab.
138
+ */
139
+ const resolveToken = () => {
140
+ return oxyServices.getAccessToken() || readTokenFromStorage();
77
141
  };
78
- const handleSessionUpdate = async (data) => {
79
- debug.log('Received session_update:', data);
80
- const currentActiveSessionId = activeSessionIdRef.current;
81
- const deviceId = currentDeviceIdRef.current;
82
- // Handle different event types
83
- if (data.type === 'session_removed') {
84
- // Track removed session
85
- if (data.sessionId && onSessionRemovedRef.current) {
86
- onSessionRemovedRef.current(data.sessionId);
87
- }
88
- // If the removed sessionId matches the current activeSessionId, immediately clear state
89
- if (data.sessionId === currentActiveSessionId) {
90
- if (onRemoteSignOutRef.current) {
91
- onRemoteSignOutRef.current();
92
- }
93
- else {
94
- sonner_1.toast.info('You have been signed out remotely.');
95
- }
96
- try {
97
- await clearSessionStateRef.current();
98
- }
99
- catch (error) {
100
- if (__DEV__) {
101
- core_1.logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
142
+ getSocketIO().then((ioFn) => {
143
+ if (cancelled || !ioFn)
144
+ return;
145
+ // Connect with auth token; use callback so reconnections get a fresh token.
146
+ // If no token is available at all, we skip the initial connect and let
147
+ // the storage listener or retry logic connect when a token appears.
148
+ const token = resolveToken();
149
+ const socket = ioFn(baseURL, {
150
+ transports: ['websocket'],
151
+ autoConnect: !!token, // don't auto-connect when there is no token
152
+ auth: (cb) => {
153
+ const resolved = resolveToken();
154
+ if (!resolved) {
155
+ // No token available -- disconnect gracefully instead of sending
156
+ // an empty string that the server will reject.
157
+ debug.warn('No access token available for socket auth; disconnecting.');
158
+ if (socketRef.current) {
159
+ socketRef.current.disconnect();
102
160
  }
161
+ return;
103
162
  }
163
+ cb({ token: resolved });
164
+ },
165
+ });
166
+ socketRef.current = socket;
167
+ // Server auto-joins the user to `user:<userId>` room on connection
168
+ const handleConnect = () => {
169
+ debug.log('Socket connected:', socket.id);
170
+ // Successful connection resets the auth retry counter.
171
+ authRetryCount = 0;
172
+ };
173
+ /**
174
+ * Handle socket disconnection. When the disconnect reason indicates an
175
+ * auth failure (server rejected the token), schedule a short retry so
176
+ * that an in-progress token refresh can complete before the next attempt.
177
+ */
178
+ const handleDisconnect = (reason) => {
179
+ debug.log('Socket disconnected:', reason);
180
+ // "io server disconnect" = server forcibly closed the connection (auth failure).
181
+ // "transport error" can also happen when the auth callback aborted.
182
+ if ((reason === 'io server disconnect' || reason === 'transport error') &&
183
+ authRetryCount < MAX_AUTH_RETRIES &&
184
+ !cancelled) {
185
+ authRetryCount++;
186
+ debug.log(`Auth-related disconnect; scheduling retry ${authRetryCount}/${MAX_AUTH_RETRIES} in ${AUTH_RETRY_DELAY_MS}ms`);
187
+ authRetryTimer = setTimeout(() => {
188
+ if (cancelled)
189
+ return;
190
+ const retryToken = resolveToken();
191
+ if (retryToken && socketRef.current) {
192
+ debug.log('Retrying socket connection with refreshed token');
193
+ socketRef.current.connect();
194
+ }
195
+ }, AUTH_RETRY_DELAY_MS);
196
+ }
197
+ };
198
+ const refreshSessions = () => {
199
+ (0, queryKeys_1.invalidateSessionQueries)(queryClientRef.current);
200
+ return Promise.resolve();
201
+ };
202
+ const triggerLocalSignOut = async (toastMessage, errorContext) => {
203
+ if (onRemoteSignOutRef.current) {
204
+ onRemoteSignOutRef.current();
104
205
  }
105
206
  else {
106
- refreshSessions();
207
+ sonner_1.toast.info(toastMessage);
107
208
  }
108
- }
109
- else if (data.type === 'device_removed') {
110
- // Track all removed sessions from this device
111
- if (data.sessionIds && onSessionRemovedRef.current) {
112
- for (const sessionId of data.sessionIds) {
113
- onSessionRemovedRef.current(sessionId);
114
- }
209
+ // Clear local state since the server has already removed the session.
210
+ // Await so storage cleanup completes before any subsequent navigation.
211
+ try {
212
+ await clearSessionStateRef.current();
115
213
  }
116
- // If the removed deviceId matches the current device, immediately clear state
117
- if (data.deviceId && data.deviceId === deviceId) {
118
- if (onRemoteSignOutRef.current) {
119
- onRemoteSignOutRef.current();
120
- }
121
- else {
122
- sonner_1.toast.info('This device has been removed. You have been signed out.');
123
- }
124
- try {
125
- await clearSessionStateRef.current();
126
- }
127
- catch (error) {
128
- if (__DEV__) {
129
- core_1.logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
130
- }
214
+ catch (error) {
215
+ if (process.env.NODE_ENV !== 'production') {
216
+ core_1.logger.error(`Failed to clear session state after ${errorContext}`, error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
131
217
  }
132
218
  }
133
- else {
134
- refreshSessions();
135
- }
136
- }
137
- else if (data.type === 'sessions_removed') {
138
- // Track all removed sessions
139
- if (data.sessionIds && onSessionRemovedRef.current) {
140
- for (const sessionId of data.sessionIds) {
141
- onSessionRemovedRef.current(sessionId);
219
+ };
220
+ const handleSessionUpdate = async (data) => {
221
+ debug.log('Received session_update:', data);
222
+ const currentActiveSessionId = activeSessionIdRef.current;
223
+ const deviceId = currentDeviceIdRef.current;
224
+ // Strict whitelist. Every event type that may sign the user out must
225
+ // appear in the switch. Anything unknown falls through to `default`,
226
+ // which only logs in dev. This guards against future server-side event
227
+ // additions (e.g. `session_created` after a successful sign-in)
228
+ // accidentally triggering sign-out via a fallback branch that compares
229
+ // `data.sessionId === currentActiveSessionId` — that branch would match
230
+ // the user's NEW session id and trigger an instant remote sign-out
231
+ // toast on every login.
232
+ switch (data.type) {
233
+ case 'session_removed': {
234
+ if (data.sessionId && onSessionRemovedRef.current) {
235
+ onSessionRemovedRef.current(data.sessionId);
236
+ }
237
+ if (data.sessionId && data.sessionId === currentActiveSessionId) {
238
+ await triggerLocalSignOut('You have been signed out remotely.', 'session_removed');
239
+ }
240
+ else {
241
+ refreshSessions();
242
+ }
243
+ break;
142
244
  }
143
- }
144
- // If the current activeSessionId is in the removed sessionIds list, immediately clear state
145
- if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
146
- if (onRemoteSignOutRef.current) {
147
- onRemoteSignOutRef.current();
245
+ case 'device_removed': {
246
+ if (data.sessionIds && onSessionRemovedRef.current) {
247
+ for (const sessionId of data.sessionIds) {
248
+ onSessionRemovedRef.current(sessionId);
249
+ }
250
+ }
251
+ if (data.deviceId && deviceId && data.deviceId === deviceId) {
252
+ await triggerLocalSignOut('This device has been removed. You have been signed out.', 'device_removed');
253
+ }
254
+ else {
255
+ refreshSessions();
256
+ }
257
+ break;
148
258
  }
149
- else {
150
- sonner_1.toast.info('You have been signed out remotely.');
259
+ case 'sessions_removed': {
260
+ if (data.sessionIds && onSessionRemovedRef.current) {
261
+ for (const sessionId of data.sessionIds) {
262
+ onSessionRemovedRef.current(sessionId);
263
+ }
264
+ }
265
+ if (data.sessionIds &&
266
+ currentActiveSessionId &&
267
+ data.sessionIds.includes(currentActiveSessionId)) {
268
+ await triggerLocalSignOut('You have been signed out remotely.', 'sessions_removed');
269
+ }
270
+ else {
271
+ refreshSessions();
272
+ }
273
+ break;
151
274
  }
152
- try {
153
- await clearSessionStateRef.current();
275
+ case 'session_created':
276
+ case 'session_update': {
277
+ // Lifecycle event for the current user. Just resync the sessions
278
+ // list — never sign out.
279
+ refreshSessions();
280
+ break;
154
281
  }
155
- catch (error) {
156
- if (__DEV__) {
157
- core_1.logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
282
+ default: {
283
+ if (process.env.NODE_ENV !== 'production') {
284
+ core_1.logger.warn('Unknown session socket event type', {
285
+ component: 'useSessionSocket',
286
+ type: data.type,
287
+ });
158
288
  }
289
+ break;
159
290
  }
160
291
  }
161
- else {
162
- refreshSessions();
292
+ };
293
+ socket.on('connect', handleConnect);
294
+ socket.on('disconnect', handleDisconnect);
295
+ socket.on('session_update', handleSessionUpdate);
296
+ // Listen for cross-tab token updates via the Storage event.
297
+ // When another tab writes a fresh access token to localStorage,
298
+ // reconnect this tab's socket if it was disconnected.
299
+ const handleStorageEvent = (e) => {
300
+ if (e.key === LS_ACCESS_TOKEN_KEY && e.newValue && socketRef.current?.disconnected) {
301
+ debug.log('Cross-tab token update detected; reconnecting socket');
302
+ authRetryCount = 0; // reset retries since we got a fresh token
303
+ socketRef.current.connect();
163
304
  }
305
+ };
306
+ if (typeof window !== 'undefined') {
307
+ window.addEventListener('storage', handleStorageEvent);
308
+ // Store the handler so cleanup can remove it
309
+ socket.__oxyStorageHandler = handleStorageEvent;
164
310
  }
165
- else {
166
- // For other event types (e.g., session_created), refresh sessions
167
- refreshSessions();
168
- // If the current session was logged out (legacy behavior), handle it specially
169
- if (data.sessionId === currentActiveSessionId) {
170
- if (onRemoteSignOutRef.current) {
171
- onRemoteSignOutRef.current();
172
- }
173
- else {
174
- sonner_1.toast.info('You have been signed out remotely.');
175
- }
176
- try {
177
- await clearSessionStateRef.current();
178
- }
179
- catch (error) {
180
- debug.error('Failed to clear session state after session_update:', error);
181
- }
311
+ });
312
+ return () => {
313
+ cancelled = true;
314
+ if (authRetryTimer) {
315
+ clearTimeout(authRetryTimer);
316
+ }
317
+ const currentSocket = socketRef.current;
318
+ if (currentSocket) {
319
+ // Remove cross-tab storage listener
320
+ const storageHandler = currentSocket.__oxyStorageHandler;
321
+ if (typeof window !== 'undefined' && storageHandler) {
322
+ window.removeEventListener('storage', storageHandler);
182
323
  }
324
+ currentSocket.disconnect();
325
+ socketRef.current = null;
183
326
  }
184
327
  };
185
- socket.on('connect', handleConnect);
186
- socket.on('session_update', handleSessionUpdate);
187
- return () => {
188
- socket.off('connect', handleConnect);
189
- socket.off('session_update', handleSessionUpdate);
190
- socket.disconnect();
191
- socketRef.current = null;
192
- };
193
328
  }, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
194
329
  }
package/dist/cjs/index.js CHANGED
@@ -24,8 +24,8 @@
24
24
  * ```
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.isInvalidSessionError = exports.handleAuthError = exports.useFileFiltering = exports.useFollowerCounts = exports.useFollow = exports.setOxyFileUrlInstance = exports.useFileDownloadUrl = exports.setOxyAssetInstance = exports.useAssets = exports.useSessionSocket = 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.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 = void 0;
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;
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; } });
@@ -45,6 +45,14 @@ Object.defineProperty(exports, "useAssetsByApp", { enumerable: true, get: functi
45
45
  Object.defineProperty(exports, "useAssetsByEntity", { enumerable: true, get: function () { return assetStore_1.useAssetsByEntity; } });
46
46
  Object.defineProperty(exports, "useAssetUsageCount", { enumerable: true, get: function () { return assetStore_1.useAssetUsageCount; } });
47
47
  Object.defineProperty(exports, "useIsAssetLinked", { enumerable: true, get: function () { return assetStore_1.useIsAssetLinked; } });
48
+ var accountStore_1 = require("./stores/accountStore");
49
+ Object.defineProperty(exports, "useAccountStore", { enumerable: true, get: function () { return accountStore_1.useAccountStore; } });
50
+ Object.defineProperty(exports, "useAccounts", { enumerable: true, get: function () { return accountStore_1.useAccounts; } });
51
+ Object.defineProperty(exports, "useAccountLoading", { enumerable: true, get: function () { return accountStore_1.useAccountLoading; } });
52
+ Object.defineProperty(exports, "useAccountError", { enumerable: true, get: function () { return accountStore_1.useAccountError; } });
53
+ Object.defineProperty(exports, "useAccountLoadingSession", { enumerable: true, get: function () { return accountStore_1.useAccountLoadingSession; } });
54
+ var followStore_1 = require("./stores/followStore");
55
+ Object.defineProperty(exports, "useFollowStore", { enumerable: true, get: function () { return followStore_1.useFollowStore; } });
48
56
  // --- Query Hooks ---
49
57
  var queries_1 = require("./hooks/queries");
50
58
  Object.defineProperty(exports, "useUserProfile", { enumerable: true, get: function () { return queries_1.useUserProfile; } });
@@ -77,6 +85,9 @@ var mutationFactory_1 = require("./hooks/mutations/mutationFactory");
77
85
  Object.defineProperty(exports, "createProfileMutation", { enumerable: true, get: function () { return mutationFactory_1.createProfileMutation; } });
78
86
  Object.defineProperty(exports, "createGenericMutation", { enumerable: true, get: function () { return mutationFactory_1.createGenericMutation; } });
79
87
  // --- Custom Hooks ---
88
+ var useWebSSO_1 = require("./hooks/useWebSSO");
89
+ Object.defineProperty(exports, "useWebSSO", { enumerable: true, get: function () { return useWebSSO_1.useWebSSO; } });
90
+ Object.defineProperty(exports, "isWebBrowser", { enumerable: true, get: function () { return useWebSSO_1.isWebBrowser; } });
80
91
  var useSessionSocket_1 = require("./hooks/useSessionSocket");
81
92
  Object.defineProperty(exports, "useSessionSocket", { enumerable: true, get: function () { return useSessionSocket_1.useSessionSocket; } });
82
93
  var useAssets_1 = require("./hooks/useAssets");
@@ -84,7 +95,6 @@ Object.defineProperty(exports, "useAssets", { enumerable: true, get: function ()
84
95
  Object.defineProperty(exports, "setOxyAssetInstance", { enumerable: true, get: function () { return useAssets_1.setOxyAssetInstance; } });
85
96
  var useFileDownloadUrl_1 = require("./hooks/useFileDownloadUrl");
86
97
  Object.defineProperty(exports, "useFileDownloadUrl", { enumerable: true, get: function () { return useFileDownloadUrl_1.useFileDownloadUrl; } });
87
- Object.defineProperty(exports, "setOxyFileUrlInstance", { enumerable: true, get: function () { return useFileDownloadUrl_1.setOxyFileUrlInstance; } });
88
98
  var useFollow_1 = require("./hooks/useFollow");
89
99
  Object.defineProperty(exports, "useFollow", { enumerable: true, get: function () { return useFollow_1.useFollow; } });
90
100
  Object.defineProperty(exports, "useFollowerCounts", { enumerable: true, get: function () { return useFollow_1.useFollowerCounts; } });
@@ -164,7 +164,7 @@ exports.useAccountStore = (0, zustand_1.create)((set, get) => ({
164
164
  }
165
165
  catch (error) {
166
166
  const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
167
- if (__DEV__) {
167
+ if (process.env.NODE_ENV !== 'production') {
168
168
  console.error('AccountStore: Failed to load accounts:', error);
169
169
  }
170
170
  set({ error: errorMessage });
@@ -175,7 +175,7 @@ exports.useAccountStore = (0, zustand_1.create)((set, get) => ({
175
175
  }
176
176
  catch (error) {
177
177
  const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
178
- if (__DEV__) {
178
+ if (process.env.NODE_ENV !== 'production') {
179
179
  console.error('AccountStore: Failed to load accounts:', error);
180
180
  }
181
181
  set({ error: errorMessage, loading: false });
@@ -25,7 +25,9 @@ const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId) => {
25
25
  };
26
26
  exports.mapSessionsToClient = mapSessionsToClient;
27
27
  /**
28
- * Fetch device sessions with fallback to the legacy session endpoint when needed.
28
+ * Fetch device sessions, falling back to the per-user session endpoint
29
+ * if the device endpoint is unavailable (older API versions or disabled
30
+ * device-grouping feature flag).
29
31
  *
30
32
  * @param oxyServices - Oxy service instance
31
33
  * @param sessionId - Session identifier to fetch
@@ -37,7 +39,7 @@ const fetchSessionsWithFallback = async (oxyServices, sessionId, { fallbackDevic
37
39
  return (0, exports.mapSessionsToClient)(deviceSessions, fallbackDeviceId, fallbackUserId);
38
40
  }
39
41
  catch (error) {
40
- if (__DEV__ && logger) {
42
+ if (process.env.NODE_ENV !== 'production' && logger) {
41
43
  logger('Failed to get device sessions, falling back to user sessions', error);
42
44
  }
43
45
  const userSessions = await oxyServices.getSessionsBySessionId(sessionId);