@oxyhq/auth 1.0.0
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/README.md +56 -0
- package/dist/cjs/WebOxyProvider.js +287 -0
- package/dist/cjs/hooks/mutations/index.js +23 -0
- package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
- package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
- package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
- package/dist/cjs/hooks/queries/index.js +35 -0
- package/dist/cjs/hooks/queries/queryKeys.js +82 -0
- package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
- package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
- package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
- package/dist/cjs/hooks/queryClient.js +110 -0
- package/dist/cjs/hooks/useAssets.js +225 -0
- package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
- package/dist/cjs/hooks/useFileFiltering.js +81 -0
- package/dist/cjs/hooks/useFollow.js +159 -0
- package/dist/cjs/hooks/useFollow.types.js +4 -0
- package/dist/cjs/hooks/useQueryClient.js +16 -0
- package/dist/cjs/hooks/useSessionSocket.js +215 -0
- package/dist/cjs/hooks/useWebSSO.js +146 -0
- package/dist/cjs/index.js +115 -0
- package/dist/cjs/stores/accountStore.js +226 -0
- package/dist/cjs/stores/assetStore.js +192 -0
- package/dist/cjs/stores/authStore.js +47 -0
- package/dist/cjs/stores/followStore.js +154 -0
- package/dist/cjs/utils/authHelpers.js +154 -0
- package/dist/cjs/utils/avatarUtils.js +77 -0
- package/dist/cjs/utils/errorHandlers.js +128 -0
- package/dist/cjs/utils/sessionHelpers.js +90 -0
- package/dist/cjs/utils/storageHelpers.js +147 -0
- package/dist/esm/WebOxyProvider.js +282 -0
- package/dist/esm/hooks/mutations/index.js +10 -0
- package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
- package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
- package/dist/esm/hooks/queries/index.js +14 -0
- package/dist/esm/hooks/queries/queryKeys.js +76 -0
- package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
- package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
- package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
- package/dist/esm/hooks/queryClient.js +104 -0
- package/dist/esm/hooks/useAssets.js +220 -0
- package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
- package/dist/esm/hooks/useFileFiltering.js +78 -0
- package/dist/esm/hooks/useFollow.js +154 -0
- package/dist/esm/hooks/useFollow.types.js +3 -0
- package/dist/esm/hooks/useQueryClient.js +12 -0
- package/dist/esm/hooks/useSessionSocket.js +209 -0
- package/dist/esm/hooks/useWebSSO.js +143 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/stores/accountStore.js +219 -0
- package/dist/esm/stores/assetStore.js +180 -0
- package/dist/esm/stores/authStore.js +44 -0
- package/dist/esm/stores/followStore.js +151 -0
- package/dist/esm/utils/authHelpers.js +145 -0
- package/dist/esm/utils/avatarUtils.js +72 -0
- package/dist/esm/utils/errorHandlers.js +121 -0
- package/dist/esm/utils/sessionHelpers.js +84 -0
- package/dist/esm/utils/storageHelpers.js +108 -0
- package/dist/types/WebOxyProvider.d.ts +97 -0
- package/dist/types/hooks/mutations/index.d.ts +8 -0
- package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
- package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
- package/dist/types/hooks/queries/index.d.ts +10 -0
- package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
- package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
- package/dist/types/hooks/queryClient.d.ts +18 -0
- package/dist/types/hooks/useAssets.d.ts +34 -0
- package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
- package/dist/types/hooks/useFileFiltering.d.ts +28 -0
- package/dist/types/hooks/useFollow.d.ts +61 -0
- package/dist/types/hooks/useFollow.types.d.ts +32 -0
- package/dist/types/hooks/useQueryClient.d.ts +6 -0
- package/dist/types/hooks/useSessionSocket.d.ts +13 -0
- package/dist/types/hooks/useWebSSO.d.ts +57 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/stores/accountStore.d.ts +33 -0
- package/dist/types/stores/assetStore.d.ts +53 -0
- package/dist/types/stores/authStore.d.ts +16 -0
- package/dist/types/stores/followStore.d.ts +24 -0
- package/dist/types/utils/authHelpers.d.ts +98 -0
- package/dist/types/utils/avatarUtils.d.ts +33 -0
- package/dist/types/utils/errorHandlers.d.ts +34 -0
- package/dist/types/utils/sessionHelpers.d.ts +63 -0
- package/dist/types/utils/storageHelpers.d.ts +27 -0
- package/package.json +71 -0
- package/src/WebOxyProvider.tsx +372 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/mutations/index.ts +25 -0
- package/src/hooks/mutations/mutationFactory.ts +215 -0
- package/src/hooks/mutations/useAccountMutations.ts +344 -0
- package/src/hooks/mutations/useServicesMutations.ts +164 -0
- package/src/hooks/queries/index.ts +36 -0
- package/src/hooks/queries/queryKeys.ts +88 -0
- package/src/hooks/queries/useAccountQueries.ts +152 -0
- package/src/hooks/queries/useSecurityQueries.ts +64 -0
- package/src/hooks/queries/useServicesQueries.ts +126 -0
- package/src/hooks/queryClient.ts +112 -0
- package/src/hooks/useAssets.ts +291 -0
- package/src/hooks/useFileDownloadUrl.ts +118 -0
- package/src/hooks/useFileFiltering.ts +115 -0
- package/src/hooks/useFollow.ts +175 -0
- package/src/hooks/useFollow.types.ts +33 -0
- package/src/hooks/useQueryClient.ts +17 -0
- package/src/hooks/useSessionSocket.ts +233 -0
- package/src/hooks/useWebSSO.ts +187 -0
- package/src/index.ts +144 -0
- package/src/stores/accountStore.ts +296 -0
- package/src/stores/assetStore.ts +281 -0
- package/src/stores/authStore.ts +63 -0
- package/src/stores/followStore.ts +181 -0
- package/src/utils/authHelpers.ts +183 -0
- package/src/utils/avatarUtils.ts +103 -0
- package/src/utils/errorHandlers.ts +194 -0
- package/src/utils/sessionHelpers.ts +151 -0
- package/src/utils/storageHelpers.ts +130 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Type-only definition for the useFollow hook to allow context exposure without runtime import cycles.
|
|
2
|
+
// Expand this as needed to better reflect the real return type.
|
|
3
|
+
|
|
4
|
+
export type SingleFollowResult = {
|
|
5
|
+
isFollowing: boolean;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
toggleFollow: () => Promise<void>;
|
|
9
|
+
setFollowStatus: (following: boolean) => void;
|
|
10
|
+
fetchStatus: () => Promise<void>;
|
|
11
|
+
clearError: () => void;
|
|
12
|
+
followerCount: number | null;
|
|
13
|
+
followingCount: number | null;
|
|
14
|
+
isLoadingCounts: boolean;
|
|
15
|
+
fetchUserCounts: () => Promise<void>;
|
|
16
|
+
setFollowerCount: (count: number) => void;
|
|
17
|
+
setFollowingCount: (count: number) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MultiFollowResult = {
|
|
21
|
+
followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }>;
|
|
22
|
+
toggleFollowForUser: (userId: string) => Promise<void>;
|
|
23
|
+
setFollowStatusForUser: (userId: string, following: boolean) => void;
|
|
24
|
+
fetchStatusForUser: (userId: string) => Promise<void>;
|
|
25
|
+
fetchAllStatuses: () => Promise<void>;
|
|
26
|
+
clearErrorForUser: (userId: string) => void;
|
|
27
|
+
isAnyLoading: boolean;
|
|
28
|
+
hasAnyError: boolean;
|
|
29
|
+
allFollowing: boolean;
|
|
30
|
+
allNotFollowing: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type UseFollowHook = (userId?: string | string[]) => SingleFollowResult | MultiFollowResult;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useQueryClient as useTanStackQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to access the QueryClient
|
|
6
|
+
* Provides type safety and ensures client is available
|
|
7
|
+
*/
|
|
8
|
+
export const useQueryClient = (): QueryClient => {
|
|
9
|
+
const queryClient = useTanStackQueryClient();
|
|
10
|
+
|
|
11
|
+
if (!queryClient) {
|
|
12
|
+
throw new Error('QueryClient is not available. Make sure OxyProvider is wrapping your app.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return queryClient;
|
|
16
|
+
};
|
|
17
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import io from 'socket.io-client';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { logger } from '@oxyhq/core';
|
|
5
|
+
import { createDebugLogger } from '@oxyhq/core';
|
|
6
|
+
|
|
7
|
+
const debug = createDebugLogger('SessionSocket');
|
|
8
|
+
|
|
9
|
+
interface UseSessionSocketProps {
|
|
10
|
+
userId: string | null | undefined;
|
|
11
|
+
activeSessionId: string | null | undefined;
|
|
12
|
+
currentDeviceId: string | null | undefined;
|
|
13
|
+
refreshSessions: () => Promise<void>;
|
|
14
|
+
logout: () => Promise<void>;
|
|
15
|
+
clearSessionState: () => Promise<void>;
|
|
16
|
+
baseURL: string;
|
|
17
|
+
onRemoteSignOut?: () => void;
|
|
18
|
+
onSessionRemoved?: (sessionId: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
|
|
22
|
+
const socketRef = useRef<any>(null);
|
|
23
|
+
const joinedRoomRef = useRef<string | null>(null);
|
|
24
|
+
|
|
25
|
+
// Store callbacks in refs to avoid re-joining when they change
|
|
26
|
+
const refreshSessionsRef = useRef(refreshSessions);
|
|
27
|
+
const logoutRef = useRef(logout);
|
|
28
|
+
const clearSessionStateRef = useRef(clearSessionState);
|
|
29
|
+
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
30
|
+
const onSessionRemovedRef = useRef(onSessionRemoved);
|
|
31
|
+
const activeSessionIdRef = useRef(activeSessionId);
|
|
32
|
+
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
33
|
+
|
|
34
|
+
// Update refs when callbacks change
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
refreshSessionsRef.current = refreshSessions;
|
|
37
|
+
logoutRef.current = logout;
|
|
38
|
+
clearSessionStateRef.current = clearSessionState;
|
|
39
|
+
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
40
|
+
onSessionRemovedRef.current = onSessionRemoved;
|
|
41
|
+
activeSessionIdRef.current = activeSessionId;
|
|
42
|
+
currentDeviceIdRef.current = currentDeviceId;
|
|
43
|
+
}, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!userId || !baseURL) {
|
|
47
|
+
// Clean up if userId or baseURL becomes invalid
|
|
48
|
+
if (socketRef.current && joinedRoomRef.current) {
|
|
49
|
+
socketRef.current.emit('leave', { userId: joinedRoomRef.current });
|
|
50
|
+
joinedRoomRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const roomId = `user:${userId}`;
|
|
56
|
+
|
|
57
|
+
// Only create socket if it doesn't exist
|
|
58
|
+
if (!socketRef.current) {
|
|
59
|
+
socketRef.current = io(baseURL, {
|
|
60
|
+
transports: ['websocket'],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const socket = socketRef.current;
|
|
64
|
+
|
|
65
|
+
// Only join if we haven't already joined this room
|
|
66
|
+
if (joinedRoomRef.current !== roomId) {
|
|
67
|
+
// Leave previous room if switching users
|
|
68
|
+
if (joinedRoomRef.current) {
|
|
69
|
+
socket.emit('leave', { userId: joinedRoomRef.current });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
socket.emit('join', { userId: roomId });
|
|
73
|
+
joinedRoomRef.current = roomId;
|
|
74
|
+
|
|
75
|
+
debug.log('Emitting join for room:', roomId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set up event handlers (only once per socket instance)
|
|
79
|
+
const handleConnect = () => {
|
|
80
|
+
debug.log('Socket connected:', socket.id);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleSessionUpdate = async (data: {
|
|
84
|
+
type: string;
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
deviceId?: string;
|
|
87
|
+
sessionIds?: string[]
|
|
88
|
+
}) => {
|
|
89
|
+
debug.log('Received session_update:', data);
|
|
90
|
+
|
|
91
|
+
const currentActiveSessionId = activeSessionIdRef.current;
|
|
92
|
+
const currentDeviceId = currentDeviceIdRef.current;
|
|
93
|
+
|
|
94
|
+
// Handle different event types
|
|
95
|
+
if (data.type === 'session_removed') {
|
|
96
|
+
// Track removed session
|
|
97
|
+
if (data.sessionId && onSessionRemovedRef.current) {
|
|
98
|
+
onSessionRemovedRef.current(data.sessionId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If the removed sessionId matches the current activeSessionId, immediately clear state
|
|
102
|
+
if (data.sessionId === currentActiveSessionId) {
|
|
103
|
+
if (onRemoteSignOutRef.current) {
|
|
104
|
+
onRemoteSignOutRef.current();
|
|
105
|
+
} else {
|
|
106
|
+
toast.info('You have been signed out remotely.');
|
|
107
|
+
}
|
|
108
|
+
// Use clearSessionState since session was already removed server-side
|
|
109
|
+
// Await to ensure storage cleanup completes before continuing
|
|
110
|
+
try {
|
|
111
|
+
await clearSessionStateRef.current();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Otherwise, just refresh the sessions list (with error handling)
|
|
119
|
+
refreshSessionsRef.current().catch((error) => {
|
|
120
|
+
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
121
|
+
if (__DEV__) {
|
|
122
|
+
logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} else if (data.type === 'device_removed') {
|
|
127
|
+
// Track all removed sessions from this device
|
|
128
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
129
|
+
for (const sessionId of data.sessionIds) {
|
|
130
|
+
onSessionRemovedRef.current(sessionId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If the removed deviceId matches the current device, immediately clear state
|
|
135
|
+
if (data.deviceId && data.deviceId === currentDeviceId) {
|
|
136
|
+
if (onRemoteSignOutRef.current) {
|
|
137
|
+
onRemoteSignOutRef.current();
|
|
138
|
+
} else {
|
|
139
|
+
toast.info('This device has been removed. You have been signed out.');
|
|
140
|
+
}
|
|
141
|
+
// Use clearSessionState since sessions were already removed server-side
|
|
142
|
+
// Await to ensure storage cleanup completes before continuing
|
|
143
|
+
try {
|
|
144
|
+
await clearSessionStateRef.current();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (__DEV__) {
|
|
147
|
+
logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Otherwise, refresh sessions and device list (with error handling)
|
|
152
|
+
refreshSessionsRef.current().catch((error) => {
|
|
153
|
+
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
154
|
+
if (__DEV__) {
|
|
155
|
+
logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} else if (data.type === 'sessions_removed') {
|
|
160
|
+
// Track all removed sessions
|
|
161
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
162
|
+
for (const sessionId of data.sessionIds) {
|
|
163
|
+
onSessionRemovedRef.current(sessionId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If the current activeSessionId is in the removed sessionIds list, immediately clear state
|
|
168
|
+
if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
|
|
169
|
+
if (onRemoteSignOutRef.current) {
|
|
170
|
+
onRemoteSignOutRef.current();
|
|
171
|
+
} else {
|
|
172
|
+
toast.info('You have been signed out remotely.');
|
|
173
|
+
}
|
|
174
|
+
// Use clearSessionState since sessions were already removed server-side
|
|
175
|
+
// Await to ensure storage cleanup completes before continuing
|
|
176
|
+
try {
|
|
177
|
+
await clearSessionStateRef.current();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (__DEV__) {
|
|
180
|
+
logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Otherwise, refresh sessions list (with error handling)
|
|
185
|
+
refreshSessionsRef.current().catch((error) => {
|
|
186
|
+
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
187
|
+
if (__DEV__) {
|
|
188
|
+
logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// For other event types (e.g., session_created), refresh sessions (with error handling)
|
|
194
|
+
refreshSessionsRef.current().catch((error) => {
|
|
195
|
+
// Log but don't throw - refresh errors shouldn't break the socket handler
|
|
196
|
+
if (__DEV__) {
|
|
197
|
+
logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error as unknown);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// If the current session was logged out (legacy behavior), handle it specially
|
|
202
|
+
if (data.sessionId === currentActiveSessionId) {
|
|
203
|
+
if (onRemoteSignOutRef.current) {
|
|
204
|
+
onRemoteSignOutRef.current();
|
|
205
|
+
} else {
|
|
206
|
+
toast.info('You have been signed out remotely.');
|
|
207
|
+
}
|
|
208
|
+
// Use clearSessionState since session was already removed server-side
|
|
209
|
+
// Await to ensure storage cleanup completes before continuing
|
|
210
|
+
try {
|
|
211
|
+
await clearSessionStateRef.current();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
debug.error('Failed to clear session state after session_update:', error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
socket.on('connect', handleConnect);
|
|
220
|
+
socket.on('session_update', handleSessionUpdate);
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
socket.off('connect', handleConnect);
|
|
224
|
+
socket.off('session_update', handleSessionUpdate);
|
|
225
|
+
|
|
226
|
+
// Only leave on unmount if we're still in this room
|
|
227
|
+
if (joinedRoomRef.current === roomId) {
|
|
228
|
+
socket.emit('leave', { userId: roomId });
|
|
229
|
+
joinedRoomRef.current = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
|
|
233
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web SSO Hook
|
|
3
|
+
*
|
|
4
|
+
* Handles cross-domain SSO for web apps using FedCM (Federated Credential Management).
|
|
5
|
+
*
|
|
6
|
+
* FedCM is the modern, privacy-preserving standard for cross-domain identity federation.
|
|
7
|
+
* It works across completely different TLDs (alia.onl, mention.earth, homiio.com, etc.)
|
|
8
|
+
* without relying on third-party cookies.
|
|
9
|
+
*
|
|
10
|
+
* For browsers without FedCM support, users will need to click a sign-in button
|
|
11
|
+
* which triggers a popup-based authentication flow.
|
|
12
|
+
*
|
|
13
|
+
* This is called automatically by OxyContext on web platforms.
|
|
14
|
+
*
|
|
15
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
19
|
+
import type { OxyServices } from '@oxyhq/core';
|
|
20
|
+
import type { SessionLoginResponse } from '@oxyhq/core';
|
|
21
|
+
|
|
22
|
+
interface UseWebSSOOptions {
|
|
23
|
+
oxyServices: OxyServices;
|
|
24
|
+
onSessionFound: (session: SessionLoginResponse) => Promise<void>;
|
|
25
|
+
onSSOUnavailable?: () => void;
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UseWebSSOResult {
|
|
31
|
+
/** Manually trigger SSO check */
|
|
32
|
+
checkSSO: () => Promise<SessionLoginResponse | null>;
|
|
33
|
+
/** Trigger interactive FedCM sign-in (shows browser UI) */
|
|
34
|
+
signInWithFedCM: () => Promise<SessionLoginResponse | null>;
|
|
35
|
+
/** Whether SSO check is in progress */
|
|
36
|
+
isChecking: boolean;
|
|
37
|
+
/** Whether FedCM is supported in this browser */
|
|
38
|
+
isFedCMSupported: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if we're running in a web browser environment (not React Native)
|
|
43
|
+
*/
|
|
44
|
+
function isWebBrowser(): boolean {
|
|
45
|
+
return typeof window !== 'undefined' &&
|
|
46
|
+
typeof document !== 'undefined' &&
|
|
47
|
+
typeof document.documentElement !== 'undefined';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if we're on the identity provider domain (where FedCM would authenticate against itself)
|
|
52
|
+
* Only auth.oxy.so is the IdP - accounts.oxy.so is a client app like any other
|
|
53
|
+
*/
|
|
54
|
+
function isIdentityProvider(): boolean {
|
|
55
|
+
if (!isWebBrowser()) return false;
|
|
56
|
+
const hostname = window.location.hostname;
|
|
57
|
+
return hostname === 'auth.oxy.so';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook for automatic cross-domain web SSO
|
|
62
|
+
*
|
|
63
|
+
* Uses FedCM (Federated Credential Management) - the modern browser-native
|
|
64
|
+
* identity federation API. This is the same technology that powers
|
|
65
|
+
* Google's cross-domain SSO (YouTube, Gmail, Maps, etc.).
|
|
66
|
+
*
|
|
67
|
+
* Key benefits:
|
|
68
|
+
* - Works across different TLDs (alia.onl ↔ mention.earth ↔ homiio.com)
|
|
69
|
+
* - No third-party cookies required
|
|
70
|
+
* - Privacy-preserving (browser mediates identity, IdP can't track)
|
|
71
|
+
* - Automatic silent sign-in after initial authentication
|
|
72
|
+
*
|
|
73
|
+
* For browsers without FedCM (Firefox, older browsers), automatic SSO
|
|
74
|
+
* is not possible. Users will see a sign-in button instead.
|
|
75
|
+
*/
|
|
76
|
+
export function useWebSSO({
|
|
77
|
+
oxyServices,
|
|
78
|
+
onSessionFound,
|
|
79
|
+
onSSOUnavailable,
|
|
80
|
+
onError,
|
|
81
|
+
enabled = true,
|
|
82
|
+
}: UseWebSSOOptions): UseWebSSOResult {
|
|
83
|
+
const isCheckingRef = useRef(false);
|
|
84
|
+
const hasCheckedRef = useRef(false);
|
|
85
|
+
|
|
86
|
+
// Check FedCM support once
|
|
87
|
+
const fedCMSupported = isWebBrowser() && (oxyServices as any).isFedCMSupported?.();
|
|
88
|
+
|
|
89
|
+
const checkSSO = useCallback(async (): Promise<SessionLoginResponse | null> => {
|
|
90
|
+
if (!isWebBrowser() || isCheckingRef.current) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Don't use FedCM on the auth domain itself - it would authenticate against itself
|
|
95
|
+
if (isIdentityProvider()) {
|
|
96
|
+
onSSOUnavailable?.();
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// FedCM is the only reliable cross-domain SSO mechanism
|
|
101
|
+
if (!fedCMSupported) {
|
|
102
|
+
onSSOUnavailable?.();
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
isCheckingRef.current = true;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const session = await (oxyServices as any).silentSignInWithFedCM?.();
|
|
110
|
+
|
|
111
|
+
if (session) {
|
|
112
|
+
await onSessionFound(session);
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onSSOUnavailable?.();
|
|
117
|
+
return null;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
onSSOUnavailable?.();
|
|
120
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
121
|
+
return null;
|
|
122
|
+
} finally {
|
|
123
|
+
isCheckingRef.current = false;
|
|
124
|
+
}
|
|
125
|
+
}, [oxyServices, onSessionFound, onSSOUnavailable, onError, fedCMSupported]);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Trigger interactive FedCM sign-in
|
|
129
|
+
* This shows the browser's native "Sign in with Oxy" prompt.
|
|
130
|
+
* Use this when silent mediation fails (user hasn't previously consented).
|
|
131
|
+
*/
|
|
132
|
+
const signInWithFedCM = useCallback(async (): Promise<SessionLoginResponse | null> => {
|
|
133
|
+
if (!isWebBrowser() || isCheckingRef.current) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!fedCMSupported) {
|
|
138
|
+
onError?.(new Error('FedCM is not supported in this browser'));
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isCheckingRef.current = true;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const session = await (oxyServices as any).signInWithFedCM?.();
|
|
146
|
+
|
|
147
|
+
if (session) {
|
|
148
|
+
await onSessionFound(session);
|
|
149
|
+
return session;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
155
|
+
return null;
|
|
156
|
+
} finally {
|
|
157
|
+
isCheckingRef.current = false;
|
|
158
|
+
}
|
|
159
|
+
}, [oxyServices, onSessionFound, onError, fedCMSupported]);
|
|
160
|
+
|
|
161
|
+
// Auto-check SSO on mount (web only, FedCM only, not on auth domain)
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider()) {
|
|
164
|
+
if (isIdentityProvider()) {
|
|
165
|
+
onSSOUnavailable?.();
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
hasCheckedRef.current = true;
|
|
171
|
+
|
|
172
|
+
if (fedCMSupported) {
|
|
173
|
+
checkSSO();
|
|
174
|
+
} else {
|
|
175
|
+
onSSOUnavailable?.();
|
|
176
|
+
}
|
|
177
|
+
}, [enabled, checkSSO, fedCMSupported, onSSOUnavailable]);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
checkSSO,
|
|
181
|
+
signInWithFedCM,
|
|
182
|
+
isChecking: isCheckingRef.current,
|
|
183
|
+
isFedCMSupported: fedCMSupported,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { isWebBrowser };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @oxyhq/auth — OxyHQ Web Authentication SDK
|
|
3
|
+
*
|
|
4
|
+
* Headless authentication for React web apps (Next.js, Vite, CRA).
|
|
5
|
+
* Zero React Native / Expo dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { WebOxyProvider, useAuth } from '@oxyhq/auth';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <WebOxyProvider baseURL="https://api.oxy.so">
|
|
14
|
+
* <YourApp />
|
|
15
|
+
* </WebOxyProvider>
|
|
16
|
+
* );
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* function YourApp() {
|
|
20
|
+
* const { user, isAuthenticated, signIn, signOut } = useAuth();
|
|
21
|
+
* // ...
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// --- Provider & Hooks ---
|
|
27
|
+
export { WebOxyProvider, useWebOxy, useAuth } from './WebOxyProvider';
|
|
28
|
+
export type {
|
|
29
|
+
WebOxyProviderProps,
|
|
30
|
+
WebAuthState,
|
|
31
|
+
WebAuthActions,
|
|
32
|
+
WebOxyContextValue,
|
|
33
|
+
} from './WebOxyProvider';
|
|
34
|
+
|
|
35
|
+
// --- Stores ---
|
|
36
|
+
export { useAuthStore } from './stores/authStore';
|
|
37
|
+
export {
|
|
38
|
+
useAssetStore,
|
|
39
|
+
useAssets as useAssetsStore,
|
|
40
|
+
useAsset,
|
|
41
|
+
useUploadProgress,
|
|
42
|
+
useAssetLoading,
|
|
43
|
+
useAssetErrors,
|
|
44
|
+
useAssetsByApp,
|
|
45
|
+
useAssetsByEntity,
|
|
46
|
+
useAssetUsageCount,
|
|
47
|
+
useIsAssetLinked,
|
|
48
|
+
} from './stores/assetStore';
|
|
49
|
+
|
|
50
|
+
// --- Query Hooks ---
|
|
51
|
+
export {
|
|
52
|
+
useUserProfile,
|
|
53
|
+
useUserProfiles,
|
|
54
|
+
useCurrentUser,
|
|
55
|
+
useUserById,
|
|
56
|
+
useUserByUsername,
|
|
57
|
+
useUsersBySessions,
|
|
58
|
+
usePrivacySettings,
|
|
59
|
+
useSessions,
|
|
60
|
+
useSession,
|
|
61
|
+
useDeviceSessions,
|
|
62
|
+
useUserDevices,
|
|
63
|
+
useSecurityInfo,
|
|
64
|
+
useSecurityActivity,
|
|
65
|
+
useRecentSecurityActivity,
|
|
66
|
+
} from './hooks/queries';
|
|
67
|
+
|
|
68
|
+
// --- Mutation Hooks ---
|
|
69
|
+
export {
|
|
70
|
+
useUpdateProfile,
|
|
71
|
+
useUploadAvatar,
|
|
72
|
+
useUpdateAccountSettings,
|
|
73
|
+
useUpdatePrivacySettings,
|
|
74
|
+
useUploadFile,
|
|
75
|
+
useSwitchSession,
|
|
76
|
+
useLogoutSession,
|
|
77
|
+
useLogoutAll,
|
|
78
|
+
useUpdateDeviceName,
|
|
79
|
+
useRemoveDevice,
|
|
80
|
+
} from './hooks/mutations';
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
createProfileMutation,
|
|
84
|
+
createGenericMutation,
|
|
85
|
+
} from './hooks/mutations/mutationFactory';
|
|
86
|
+
export type {
|
|
87
|
+
ProfileMutationConfig,
|
|
88
|
+
GenericMutationConfig,
|
|
89
|
+
} from './hooks/mutations/mutationFactory';
|
|
90
|
+
|
|
91
|
+
// --- Custom Hooks ---
|
|
92
|
+
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
93
|
+
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
94
|
+
export { useFileDownloadUrl, setOxyFileUrlInstance } from './hooks/useFileDownloadUrl';
|
|
95
|
+
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|
|
96
|
+
export { useFileFiltering } from './hooks/useFileFiltering';
|
|
97
|
+
export type { ViewMode, SortBy, SortOrder } from './hooks/useFileFiltering';
|
|
98
|
+
|
|
99
|
+
// --- Auth Helpers ---
|
|
100
|
+
export {
|
|
101
|
+
ensureValidToken,
|
|
102
|
+
withAuthErrorHandling,
|
|
103
|
+
authenticatedApiCall,
|
|
104
|
+
isAuthenticationError,
|
|
105
|
+
SessionSyncRequiredError,
|
|
106
|
+
AuthenticationFailedError,
|
|
107
|
+
} from './utils/authHelpers';
|
|
108
|
+
export type { HandleApiErrorOptions } from './utils/authHelpers';
|
|
109
|
+
|
|
110
|
+
// --- Error Handlers ---
|
|
111
|
+
export {
|
|
112
|
+
handleAuthError,
|
|
113
|
+
isInvalidSessionError,
|
|
114
|
+
isTimeoutOrNetworkError,
|
|
115
|
+
extractErrorMessage,
|
|
116
|
+
} from './utils/errorHandlers';
|
|
117
|
+
export type { HandleAuthErrorOptions } from './utils/errorHandlers';
|
|
118
|
+
|
|
119
|
+
// Re-export core for convenience
|
|
120
|
+
export {
|
|
121
|
+
OxyServices,
|
|
122
|
+
CrossDomainAuth,
|
|
123
|
+
AuthManager,
|
|
124
|
+
createAuthManager,
|
|
125
|
+
createCrossDomainAuth,
|
|
126
|
+
} from '@oxyhq/core';
|
|
127
|
+
|
|
128
|
+
export type {
|
|
129
|
+
User,
|
|
130
|
+
LoginResponse,
|
|
131
|
+
ApiError,
|
|
132
|
+
SessionLoginResponse,
|
|
133
|
+
ClientSession,
|
|
134
|
+
MinimalUserData,
|
|
135
|
+
OxyConfig,
|
|
136
|
+
StorageAdapter,
|
|
137
|
+
AuthStateChangeCallback,
|
|
138
|
+
AuthMethod,
|
|
139
|
+
AuthManagerConfig,
|
|
140
|
+
CrossDomainAuthOptions,
|
|
141
|
+
} from '@oxyhq/core';
|
|
142
|
+
|
|
143
|
+
import { WebOxyProvider as _WebOxyProvider } from './WebOxyProvider';
|
|
144
|
+
export default _WebOxyProvider;
|