@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/WebOxyProvider.js +37 -0
- package/dist/cjs/hooks/mutations/index.js +5 -1
- package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
- package/dist/cjs/hooks/mutations/useAppData.js +133 -0
- package/dist/cjs/hooks/queries/appDataQueryKeys.js +46 -0
- package/dist/cjs/hooks/queries/index.js +8 -1
- package/dist/cjs/hooks/queries/useAppData.js +87 -0
- package/dist/cjs/hooks/queryClient.js +136 -92
- package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
- package/dist/cjs/hooks/useSessionSocket.js +81 -94
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/utils/sessionHelpers.js +3 -1
- package/dist/cjs/utils/storageHelpers.js +36 -10
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/WebOxyProvider.js +38 -1
- package/dist/esm/hooks/mutations/index.js +2 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
- package/dist/esm/hooks/mutations/useAppData.js +128 -0
- package/dist/esm/hooks/queries/appDataQueryKeys.js +42 -0
- package/dist/esm/hooks/queries/index.js +3 -0
- package/dist/esm/hooks/queries/useAppData.js +82 -0
- package/dist/esm/hooks/queryClient.js +132 -89
- package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
- package/dist/esm/hooks/useSessionSocket.js +81 -94
- package/dist/esm/index.js +3 -3
- package/dist/esm/utils/sessionHelpers.js +3 -1
- package/dist/esm/utils/storageHelpers.js +36 -10
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/WebOxyProvider.d.ts +1 -1
- package/dist/types/hooks/mutations/index.d.ts +1 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
- package/dist/types/hooks/mutations/useAppData.d.ts +47 -0
- package/dist/types/hooks/queries/appDataQueryKeys.d.ts +24 -0
- package/dist/types/hooks/queries/index.d.ts +2 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
- package/dist/types/hooks/queries/useAppData.d.ts +46 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
- package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
- package/dist/types/hooks/queryClient.d.ts +24 -10
- package/dist/types/hooks/useAssets.d.ts +1 -1
- package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
- package/dist/types/index.d.ts +3 -3
- package/dist/types/utils/sessionHelpers.d.ts +3 -1
- package/package.json +22 -3
- package/src/WebOxyProvider.tsx +39 -1
- package/src/hooks/mutations/index.ts +3 -0
- package/src/hooks/mutations/useAccountMutations.ts +230 -57
- package/src/hooks/mutations/useAppData.ts +167 -0
- package/src/hooks/queries/appDataQueryKeys.ts +53 -0
- package/src/hooks/queries/index.ts +4 -0
- package/src/hooks/queries/useAppData.ts +105 -0
- package/src/hooks/queryClient.ts +140 -83
- package/src/hooks/useFileDownloadUrl.ts +15 -39
- package/src/hooks/useSessionSocket.ts +123 -91
- package/src/index.ts +7 -1
- package/src/utils/sessionHelpers.ts +3 -1
- package/src/utils/storageHelpers.ts +49 -10
|
@@ -1,104 +1,147 @@
|
|
|
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
|
+
*/
|
|
1
12
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
-
|
|
3
|
-
|
|
13
|
+
import { persistQueryClient, } from '@tanstack/react-query-persist-client';
|
|
14
|
+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
|
15
|
+
const QUERY_CACHE_KEY = 'oxy_auth_query_cache_v2';
|
|
16
|
+
const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const QUERY_PERSIST_THROTTLE_MS = 1000;
|
|
4
18
|
/**
|
|
5
|
-
*
|
|
19
|
+
* Query-key prefixes whose data is safe to restore across reloads.
|
|
20
|
+
* Web auth surfaces are session/profile heavy — lists and history are not
|
|
21
|
+
* persisted to keep the localStorage footprint small.
|
|
6
22
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const PERSISTED_QUERY_PREFIXES = [
|
|
24
|
+
'accounts',
|
|
25
|
+
'users',
|
|
26
|
+
'sessions',
|
|
27
|
+
'auth',
|
|
28
|
+
];
|
|
29
|
+
function shouldDehydrateQuery(query) {
|
|
30
|
+
if (query.state.status !== 'success')
|
|
31
|
+
return false;
|
|
32
|
+
const head = query.queryKey[0];
|
|
33
|
+
return typeof head === 'string' && PERSISTED_QUERY_PREFIXES.includes(head);
|
|
34
|
+
}
|
|
35
|
+
function shouldDehydrateMutation(_mutation) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Best-effort detection — works in browsers, Node SSR, and React Server
|
|
40
|
+
* Components. `localStorage` is gated behind `window` because Node and edge
|
|
41
|
+
* runtimes may polyfill `globalThis.localStorage` inconsistently.
|
|
42
|
+
*/
|
|
43
|
+
function getBrowserLocalStorage() {
|
|
44
|
+
if (typeof window === 'undefined')
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
if (!window.localStorage)
|
|
48
|
+
return null;
|
|
49
|
+
return window.localStorage;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Access blocked (Safari Private Mode, sandboxed iframe, etc.)
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const createPersistenceAdapter = (storage) => ({
|
|
57
|
+
persistClient: async (client) => {
|
|
58
|
+
try {
|
|
59
|
+
await storage.setItem(QUERY_CACHE_KEY, JSON.stringify(client));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
63
|
+
console.warn('[QueryClient] Failed to persist cache', error);
|
|
22
64
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
// Check if cache is too old (30 days)
|
|
37
|
-
const maxAge = 30 * 24 * 60 * 60 * 1000;
|
|
38
|
-
if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
|
|
39
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
return parsed.clientState;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
restoreClient: async () => {
|
|
68
|
+
try {
|
|
69
|
+
const cached = await storage.getItem(QUERY_CACHE_KEY);
|
|
70
|
+
return cached ? JSON.parse(cached) : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
74
|
+
console.warn('[QueryClient] Failed to restore cache', error);
|
|
43
75
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
removeClient: async () => {
|
|
80
|
+
try {
|
|
81
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
85
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
49
86
|
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
export const createQueryClient = () => new QueryClient({
|
|
91
|
+
defaultOptions: {
|
|
92
|
+
queries: {
|
|
93
|
+
staleTime: 5 * 60 * 1000,
|
|
94
|
+
gcTime: 30 * 60 * 1000,
|
|
95
|
+
retry: 3,
|
|
96
|
+
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
|
97
|
+
refetchOnReconnect: true,
|
|
98
|
+
refetchOnWindowFocus: false,
|
|
99
|
+
networkMode: 'offlineFirst',
|
|
50
100
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
57
|
-
console.warn('[QueryClient] Failed to remove cache:', error);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
101
|
+
mutations: {
|
|
102
|
+
retry: 1,
|
|
103
|
+
networkMode: 'offlineFirst',
|
|
60
104
|
},
|
|
61
|
-
}
|
|
62
|
-
};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
63
107
|
/**
|
|
64
|
-
*
|
|
108
|
+
* Wire `persistQueryClient` to browser `localStorage` (or a no-op when not
|
|
109
|
+
* in a browser). Returns the restore promise so consumers can `await` it
|
|
110
|
+
* before exposing the client to <Suspense> boundaries.
|
|
65
111
|
*/
|
|
66
|
-
export const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
mutations: {
|
|
85
|
-
// Retry once for mutations
|
|
86
|
-
retry: 1,
|
|
87
|
-
// Offline-first: queue mutations when offline
|
|
88
|
-
networkMode: 'offlineFirst',
|
|
89
|
-
},
|
|
112
|
+
export const attachQueryPersistence = (queryClient) => {
|
|
113
|
+
const localStorage = getBrowserLocalStorage();
|
|
114
|
+
if (!localStorage) {
|
|
115
|
+
return { restored: Promise.resolve(), unsubscribe: () => { } };
|
|
116
|
+
}
|
|
117
|
+
const persister = createSyncStoragePersister({
|
|
118
|
+
storage: localStorage,
|
|
119
|
+
key: QUERY_CACHE_KEY,
|
|
120
|
+
throttleTime: QUERY_PERSIST_THROTTLE_MS,
|
|
121
|
+
});
|
|
122
|
+
const [unsubscribe, restored] = persistQueryClient({
|
|
123
|
+
queryClient,
|
|
124
|
+
persister,
|
|
125
|
+
maxAge: QUERY_CACHE_MAX_AGE,
|
|
126
|
+
dehydrateOptions: {
|
|
127
|
+
shouldDehydrateQuery,
|
|
128
|
+
shouldDehydrateMutation,
|
|
90
129
|
},
|
|
91
130
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
restored.catch((error) => {
|
|
132
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
133
|
+
console.warn('[QueryClient] Failed to restore persisted cache', error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return { unsubscribe, restored };
|
|
97
137
|
};
|
|
98
|
-
/**
|
|
99
|
-
* Clear persisted query cache
|
|
100
|
-
*/
|
|
101
138
|
export const clearQueryCache = async (storage) => {
|
|
102
|
-
|
|
103
|
-
|
|
139
|
+
try {
|
|
140
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
144
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
104
147
|
};
|
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { OxyServices } from '@oxyhq/core';
|
|
3
|
-
let oxyInstance = null;
|
|
4
|
-
export const setOxyFileUrlInstance = (instance) => {
|
|
5
|
-
oxyInstance = instance;
|
|
6
|
-
};
|
|
7
2
|
/**
|
|
8
3
|
* Hook to resolve a file's download URL asynchronously.
|
|
9
4
|
*
|
|
10
|
-
* Prefers the provided `oxyServices` instance, falls back to the module-level
|
|
11
|
-
* singleton set via `setOxyFileUrlInstance`.
|
|
12
|
-
*
|
|
13
5
|
* Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
|
|
14
6
|
* `getFileDownloadUrl` if the async call fails.
|
|
15
7
|
*/
|
|
16
|
-
export const useFileDownloadUrl = (
|
|
17
|
-
// Support two call signatures:
|
|
18
|
-
// 1. useFileDownloadUrl(oxyServices, fileId, options) — preferred
|
|
19
|
-
// 2. useFileDownloadUrl(fileId, options) — legacy (uses singleton)
|
|
20
|
-
let services;
|
|
21
|
-
let fileId;
|
|
22
|
-
let options;
|
|
23
|
-
if (fileIdOrServices instanceof OxyServices) {
|
|
24
|
-
services = fileIdOrServices;
|
|
25
|
-
fileId = typeof fileIdOrOptions === 'string' ? fileIdOrOptions : null;
|
|
26
|
-
options = maybeOptions;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
services = oxyInstance;
|
|
30
|
-
fileId = typeof fileIdOrServices === 'string' ? fileIdOrServices : null;
|
|
31
|
-
options = typeof fileIdOrOptions === 'object' && fileIdOrOptions !== null ? fileIdOrOptions : undefined;
|
|
32
|
-
}
|
|
8
|
+
export const useFileDownloadUrl = (oxyServices, fileId, options) => {
|
|
33
9
|
const [url, setUrl] = useState(null);
|
|
34
10
|
const [loading, setLoading] = useState(false);
|
|
35
11
|
const [error, setError] = useState(null);
|
|
12
|
+
const variant = options?.variant;
|
|
13
|
+
const expiresIn = options?.expiresIn;
|
|
36
14
|
useEffect(() => {
|
|
37
15
|
if (!fileId) {
|
|
38
16
|
setUrl(null);
|
|
@@ -40,25 +18,25 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
40
18
|
setError(null);
|
|
41
19
|
return;
|
|
42
20
|
}
|
|
43
|
-
if (!
|
|
21
|
+
if (!oxyServices) {
|
|
44
22
|
setUrl(null);
|
|
45
23
|
setLoading(false);
|
|
46
24
|
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
47
25
|
return;
|
|
48
26
|
}
|
|
49
27
|
let cancelled = false;
|
|
50
|
-
const instance =
|
|
28
|
+
const instance = oxyServices;
|
|
29
|
+
const targetFileId = fileId;
|
|
51
30
|
const load = async () => {
|
|
52
31
|
setLoading(true);
|
|
53
32
|
setError(null);
|
|
54
33
|
try {
|
|
55
|
-
const { variant, expiresIn } = options || {};
|
|
56
34
|
let resolvedUrl = null;
|
|
57
35
|
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
58
|
-
resolvedUrl = await instance.getFileDownloadUrlAsync(
|
|
36
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(targetFileId, variant, expiresIn);
|
|
59
37
|
}
|
|
60
38
|
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
61
|
-
resolvedUrl = instance.getFileDownloadUrl(
|
|
39
|
+
resolvedUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
62
40
|
}
|
|
63
41
|
if (!cancelled) {
|
|
64
42
|
setUrl(resolvedUrl || null);
|
|
@@ -68,8 +46,7 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
68
46
|
// Fallback to sync URL on error where possible
|
|
69
47
|
try {
|
|
70
48
|
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
71
|
-
const
|
|
72
|
-
const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
49
|
+
const fallbackUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
73
50
|
if (!cancelled) {
|
|
74
51
|
setUrl(fallbackUrl || null);
|
|
75
52
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -78,7 +55,7 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
78
55
|
}
|
|
79
56
|
}
|
|
80
57
|
catch {
|
|
81
|
-
//
|
|
58
|
+
// Secondary failure: surface the original error below.
|
|
82
59
|
}
|
|
83
60
|
if (!cancelled) {
|
|
84
61
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -94,6 +71,6 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
94
71
|
return () => {
|
|
95
72
|
cancelled = true;
|
|
96
73
|
};
|
|
97
|
-
}, [fileId,
|
|
74
|
+
}, [fileId, oxyServices, variant, expiresIn]);
|
|
98
75
|
return { url, loading, error };
|
|
99
76
|
};
|
|
@@ -24,7 +24,8 @@ function readTokenFromStorage() {
|
|
|
24
24
|
try {
|
|
25
25
|
return window.localStorage.getItem(LS_ACCESS_TOKEN_KEY);
|
|
26
26
|
}
|
|
27
|
-
catch {
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn('[oxy.session-socket] localStorage read failed:', err);
|
|
28
29
|
return null;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -37,12 +38,12 @@ async function getSocketIO() {
|
|
|
37
38
|
return null;
|
|
38
39
|
_ioLoadAttempted = true;
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_io = (mod.io ?? mod.default);
|
|
41
|
+
const mod = (await import('socket.io-client'));
|
|
42
|
+
_io = mod.io ?? mod.default ?? null;
|
|
43
43
|
return _io;
|
|
44
44
|
}
|
|
45
|
-
catch {
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.warn('[oxy.session-socket] socket.io-client import failed:', err);
|
|
46
47
|
debug.warn('socket.io-client is not installed. useSessionSocket will be disabled. Install it with: bun add socket.io-client');
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
@@ -109,7 +110,7 @@ export function useSessionSocket(options) {
|
|
|
109
110
|
// If no token is available at all, we skip the initial connect and let
|
|
110
111
|
// the storage listener or retry logic connect when a token appears.
|
|
111
112
|
const token = resolveToken();
|
|
112
|
-
|
|
113
|
+
const socket = ioFn(baseURL, {
|
|
113
114
|
transports: ['websocket'],
|
|
114
115
|
autoConnect: !!token, // don't auto-connect when there is no token
|
|
115
116
|
auth: (cb) => {
|
|
@@ -126,7 +127,7 @@ export function useSessionSocket(options) {
|
|
|
126
127
|
cb({ token: resolved });
|
|
127
128
|
},
|
|
128
129
|
});
|
|
129
|
-
|
|
130
|
+
socketRef.current = socket;
|
|
130
131
|
// Server auto-joins the user to `user:<userId>` room on connection
|
|
131
132
|
const handleConnect = () => {
|
|
132
133
|
debug.log('Socket connected:', socket.id);
|
|
@@ -162,110 +163,94 @@ export function useSessionSocket(options) {
|
|
|
162
163
|
invalidateSessionQueries(queryClientRef.current);
|
|
163
164
|
return Promise.resolve();
|
|
164
165
|
};
|
|
166
|
+
const triggerLocalSignOut = async (toastMessage, errorContext) => {
|
|
167
|
+
if (onRemoteSignOutRef.current) {
|
|
168
|
+
onRemoteSignOutRef.current();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
toast.info(toastMessage);
|
|
172
|
+
}
|
|
173
|
+
// Clear local state since the server has already removed the session.
|
|
174
|
+
// Await so storage cleanup completes before any subsequent navigation.
|
|
175
|
+
try {
|
|
176
|
+
await clearSessionStateRef.current();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
180
|
+
logger.error(`Failed to clear session state after ${errorContext}`, error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
165
184
|
const handleSessionUpdate = async (data) => {
|
|
166
185
|
debug.log('Received session_update:', data);
|
|
167
186
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
168
187
|
const deviceId = currentDeviceIdRef.current;
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
toast.info('You have been signed out remotely.');
|
|
188
|
+
// Strict whitelist. Every event type that may sign the user out must
|
|
189
|
+
// appear in the switch. Anything unknown falls through to `default`,
|
|
190
|
+
// which only logs in dev. This guards against future server-side event
|
|
191
|
+
// additions (e.g. `session_created` after a successful sign-in)
|
|
192
|
+
// accidentally triggering sign-out via a fallback branch that compares
|
|
193
|
+
// `data.sessionId === currentActiveSessionId` — that branch would match
|
|
194
|
+
// the user's NEW session id and trigger an instant remote sign-out
|
|
195
|
+
// toast on every login.
|
|
196
|
+
switch (data.type) {
|
|
197
|
+
case 'session_removed': {
|
|
198
|
+
if (data.sessionId && onSessionRemovedRef.current) {
|
|
199
|
+
onSessionRemovedRef.current(data.sessionId);
|
|
182
200
|
}
|
|
183
|
-
|
|
184
|
-
await
|
|
201
|
+
if (data.sessionId && data.sessionId === currentActiveSessionId) {
|
|
202
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'session_removed');
|
|
185
203
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
189
|
-
}
|
|
204
|
+
else {
|
|
205
|
+
refreshSessions();
|
|
190
206
|
}
|
|
207
|
+
break;
|
|
191
208
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Track all removed sessions from this device
|
|
198
|
-
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
199
|
-
for (const sessionId of data.sessionIds) {
|
|
200
|
-
onSessionRemovedRef.current(sessionId);
|
|
209
|
+
case 'device_removed': {
|
|
210
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
211
|
+
for (const sessionId of data.sessionIds) {
|
|
212
|
+
onSessionRemovedRef.current(sessionId);
|
|
213
|
+
}
|
|
201
214
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (data.deviceId && data.deviceId === deviceId) {
|
|
205
|
-
if (onRemoteSignOutRef.current) {
|
|
206
|
-
onRemoteSignOutRef.current();
|
|
215
|
+
if (data.deviceId && deviceId && data.deviceId === deviceId) {
|
|
216
|
+
await triggerLocalSignOut('This device has been removed. You have been signed out.', 'device_removed');
|
|
207
217
|
}
|
|
208
218
|
else {
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
await clearSessionStateRef.current();
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
216
|
-
logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
217
|
-
}
|
|
219
|
+
refreshSessions();
|
|
218
220
|
}
|
|
221
|
+
break;
|
|
219
222
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Track all removed sessions
|
|
226
|
-
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
227
|
-
for (const sessionId of data.sessionIds) {
|
|
228
|
-
onSessionRemovedRef.current(sessionId);
|
|
223
|
+
case 'sessions_removed': {
|
|
224
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
225
|
+
for (const sessionId of data.sessionIds) {
|
|
226
|
+
onSessionRemovedRef.current(sessionId);
|
|
227
|
+
}
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
onRemoteSignOutRef.current();
|
|
229
|
+
if (data.sessionIds &&
|
|
230
|
+
currentActiveSessionId &&
|
|
231
|
+
data.sessionIds.includes(currentActiveSessionId)) {
|
|
232
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'sessions_removed');
|
|
235
233
|
}
|
|
236
234
|
else {
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
await clearSessionStateRef.current();
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
244
|
-
logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
245
|
-
}
|
|
235
|
+
refreshSessions();
|
|
246
236
|
}
|
|
237
|
+
break;
|
|
247
238
|
}
|
|
248
|
-
|
|
239
|
+
case 'session_created':
|
|
240
|
+
case 'session_update': {
|
|
241
|
+
// Lifecycle event for the current user. Just resync the sessions
|
|
242
|
+
// list — never sign out.
|
|
249
243
|
refreshSessions();
|
|
244
|
+
break;
|
|
250
245
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (onRemoteSignOutRef.current) {
|
|
258
|
-
onRemoteSignOutRef.current();
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
toast.info('You have been signed out remotely.');
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
await clearSessionStateRef.current();
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
debug.error('Failed to clear session state after session_update:', error);
|
|
246
|
+
default: {
|
|
247
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
248
|
+
logger.warn('Unknown session socket event type', {
|
|
249
|
+
component: 'useSessionSocket',
|
|
250
|
+
type: data.type,
|
|
251
|
+
});
|
|
268
252
|
}
|
|
253
|
+
break;
|
|
269
254
|
}
|
|
270
255
|
}
|
|
271
256
|
};
|
|
@@ -293,12 +278,14 @@ export function useSessionSocket(options) {
|
|
|
293
278
|
if (authRetryTimer) {
|
|
294
279
|
clearTimeout(authRetryTimer);
|
|
295
280
|
}
|
|
296
|
-
|
|
281
|
+
const currentSocket = socketRef.current;
|
|
282
|
+
if (currentSocket) {
|
|
297
283
|
// Remove cross-tab storage listener
|
|
298
|
-
|
|
299
|
-
|
|
284
|
+
const storageHandler = currentSocket.__oxyStorageHandler;
|
|
285
|
+
if (typeof window !== 'undefined' && storageHandler) {
|
|
286
|
+
window.removeEventListener('storage', storageHandler);
|
|
300
287
|
}
|
|
301
|
-
|
|
288
|
+
currentSocket.disconnect();
|
|
302
289
|
socketRef.current = null;
|
|
303
290
|
}
|
|
304
291
|
};
|
package/dist/esm/index.js
CHANGED
|
@@ -30,15 +30,15 @@ export { useAssetStore, useAssets as useAssetsStore, useAsset, useUploadProgress
|
|
|
30
30
|
export { useAccountStore, useAccounts, useAccountLoading, useAccountError, useAccountLoadingSession, } from './stores/accountStore';
|
|
31
31
|
export { useFollowStore, } from './stores/followStore';
|
|
32
32
|
// --- Query Hooks ---
|
|
33
|
-
export { useUserProfile, useUserProfiles, useCurrentUser, useUserById, useUserByUsername, useUsersBySessions, usePrivacySettings, useSessions, useSession, useDeviceSessions, useUserDevices, useSecurityInfo, useSecurityActivity, useRecentSecurityActivity, } from './hooks/queries';
|
|
33
|
+
export { useUserProfile, useUserProfiles, useCurrentUser, useUserById, useUserByUsername, useUsersBySessions, usePrivacySettings, useSessions, useSession, useDeviceSessions, useUserDevices, useSecurityInfo, useSecurityActivity, useRecentSecurityActivity, useAppData, useAppDataNamespace, appDataQueryKeys, isMissingAppDataEndpointError, } from './hooks/queries';
|
|
34
34
|
// --- Mutation Hooks ---
|
|
35
|
-
export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdatePrivacySettings, useUploadFile, useSwitchSession, useLogoutSession, useLogoutAll, useUpdateDeviceName, useRemoveDevice, } from './hooks/mutations';
|
|
35
|
+
export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdatePrivacySettings, useUploadFile, useSwitchSession, useLogoutSession, useLogoutAll, useUpdateDeviceName, useRemoveDevice, useSetAppData, useDeleteAppData, } from './hooks/mutations';
|
|
36
36
|
export { createProfileMutation, createGenericMutation, } from './hooks/mutations/mutationFactory';
|
|
37
37
|
// --- Custom Hooks ---
|
|
38
38
|
export { useWebSSO, isWebBrowser } from './hooks/useWebSSO';
|
|
39
39
|
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
40
40
|
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
41
|
-
export { useFileDownloadUrl
|
|
41
|
+
export { useFileDownloadUrl } from './hooks/useFileDownloadUrl';
|
|
42
42
|
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|
|
43
43
|
export { useFileFiltering } from './hooks/useFileFiltering';
|
|
44
44
|
// --- Error Handlers ---
|
|
@@ -21,7 +21,9 @@ export const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId)
|
|
|
21
21
|
}));
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Fetch device sessions
|
|
24
|
+
* Fetch device sessions, falling back to the per-user session endpoint
|
|
25
|
+
* if the device endpoint is unavailable (older API versions or disabled
|
|
26
|
+
* device-grouping feature flag).
|
|
25
27
|
*
|
|
26
28
|
* @param oxyServices - Oxy service instance
|
|
27
29
|
* @param sessionId - Session identifier to fetch
|