@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.
Files changed (119) hide show
  1. package/README.md +56 -0
  2. package/dist/cjs/WebOxyProvider.js +287 -0
  3. package/dist/cjs/hooks/mutations/index.js +23 -0
  4. package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
  5. package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
  6. package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
  7. package/dist/cjs/hooks/queries/index.js +35 -0
  8. package/dist/cjs/hooks/queries/queryKeys.js +82 -0
  9. package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
  10. package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
  11. package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
  12. package/dist/cjs/hooks/queryClient.js +110 -0
  13. package/dist/cjs/hooks/useAssets.js +225 -0
  14. package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
  15. package/dist/cjs/hooks/useFileFiltering.js +81 -0
  16. package/dist/cjs/hooks/useFollow.js +159 -0
  17. package/dist/cjs/hooks/useFollow.types.js +4 -0
  18. package/dist/cjs/hooks/useQueryClient.js +16 -0
  19. package/dist/cjs/hooks/useSessionSocket.js +215 -0
  20. package/dist/cjs/hooks/useWebSSO.js +146 -0
  21. package/dist/cjs/index.js +115 -0
  22. package/dist/cjs/stores/accountStore.js +226 -0
  23. package/dist/cjs/stores/assetStore.js +192 -0
  24. package/dist/cjs/stores/authStore.js +47 -0
  25. package/dist/cjs/stores/followStore.js +154 -0
  26. package/dist/cjs/utils/authHelpers.js +154 -0
  27. package/dist/cjs/utils/avatarUtils.js +77 -0
  28. package/dist/cjs/utils/errorHandlers.js +128 -0
  29. package/dist/cjs/utils/sessionHelpers.js +90 -0
  30. package/dist/cjs/utils/storageHelpers.js +147 -0
  31. package/dist/esm/WebOxyProvider.js +282 -0
  32. package/dist/esm/hooks/mutations/index.js +10 -0
  33. package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
  34. package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
  35. package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
  36. package/dist/esm/hooks/queries/index.js +14 -0
  37. package/dist/esm/hooks/queries/queryKeys.js +76 -0
  38. package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
  39. package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
  40. package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
  41. package/dist/esm/hooks/queryClient.js +104 -0
  42. package/dist/esm/hooks/useAssets.js +220 -0
  43. package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
  44. package/dist/esm/hooks/useFileFiltering.js +78 -0
  45. package/dist/esm/hooks/useFollow.js +154 -0
  46. package/dist/esm/hooks/useFollow.types.js +3 -0
  47. package/dist/esm/hooks/useQueryClient.js +12 -0
  48. package/dist/esm/hooks/useSessionSocket.js +209 -0
  49. package/dist/esm/hooks/useWebSSO.js +143 -0
  50. package/dist/esm/index.js +48 -0
  51. package/dist/esm/stores/accountStore.js +219 -0
  52. package/dist/esm/stores/assetStore.js +180 -0
  53. package/dist/esm/stores/authStore.js +44 -0
  54. package/dist/esm/stores/followStore.js +151 -0
  55. package/dist/esm/utils/authHelpers.js +145 -0
  56. package/dist/esm/utils/avatarUtils.js +72 -0
  57. package/dist/esm/utils/errorHandlers.js +121 -0
  58. package/dist/esm/utils/sessionHelpers.js +84 -0
  59. package/dist/esm/utils/storageHelpers.js +108 -0
  60. package/dist/types/WebOxyProvider.d.ts +97 -0
  61. package/dist/types/hooks/mutations/index.d.ts +8 -0
  62. package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
  63. package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
  64. package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
  65. package/dist/types/hooks/queries/index.d.ts +10 -0
  66. package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
  67. package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
  68. package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
  69. package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
  70. package/dist/types/hooks/queryClient.d.ts +18 -0
  71. package/dist/types/hooks/useAssets.d.ts +34 -0
  72. package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
  73. package/dist/types/hooks/useFileFiltering.d.ts +28 -0
  74. package/dist/types/hooks/useFollow.d.ts +61 -0
  75. package/dist/types/hooks/useFollow.types.d.ts +32 -0
  76. package/dist/types/hooks/useQueryClient.d.ts +6 -0
  77. package/dist/types/hooks/useSessionSocket.d.ts +13 -0
  78. package/dist/types/hooks/useWebSSO.d.ts +57 -0
  79. package/dist/types/index.d.ts +46 -0
  80. package/dist/types/stores/accountStore.d.ts +33 -0
  81. package/dist/types/stores/assetStore.d.ts +53 -0
  82. package/dist/types/stores/authStore.d.ts +16 -0
  83. package/dist/types/stores/followStore.d.ts +24 -0
  84. package/dist/types/utils/authHelpers.d.ts +98 -0
  85. package/dist/types/utils/avatarUtils.d.ts +33 -0
  86. package/dist/types/utils/errorHandlers.d.ts +34 -0
  87. package/dist/types/utils/sessionHelpers.d.ts +63 -0
  88. package/dist/types/utils/storageHelpers.d.ts +27 -0
  89. package/package.json +71 -0
  90. package/src/WebOxyProvider.tsx +372 -0
  91. package/src/global.d.ts +1 -0
  92. package/src/hooks/mutations/index.ts +25 -0
  93. package/src/hooks/mutations/mutationFactory.ts +215 -0
  94. package/src/hooks/mutations/useAccountMutations.ts +344 -0
  95. package/src/hooks/mutations/useServicesMutations.ts +164 -0
  96. package/src/hooks/queries/index.ts +36 -0
  97. package/src/hooks/queries/queryKeys.ts +88 -0
  98. package/src/hooks/queries/useAccountQueries.ts +152 -0
  99. package/src/hooks/queries/useSecurityQueries.ts +64 -0
  100. package/src/hooks/queries/useServicesQueries.ts +126 -0
  101. package/src/hooks/queryClient.ts +112 -0
  102. package/src/hooks/useAssets.ts +291 -0
  103. package/src/hooks/useFileDownloadUrl.ts +118 -0
  104. package/src/hooks/useFileFiltering.ts +115 -0
  105. package/src/hooks/useFollow.ts +175 -0
  106. package/src/hooks/useFollow.types.ts +33 -0
  107. package/src/hooks/useQueryClient.ts +17 -0
  108. package/src/hooks/useSessionSocket.ts +233 -0
  109. package/src/hooks/useWebSSO.ts +187 -0
  110. package/src/index.ts +144 -0
  111. package/src/stores/accountStore.ts +296 -0
  112. package/src/stores/assetStore.ts +281 -0
  113. package/src/stores/authStore.ts +63 -0
  114. package/src/stores/followStore.ts +181 -0
  115. package/src/utils/authHelpers.ts +183 -0
  116. package/src/utils/avatarUtils.ts +103 -0
  117. package/src/utils/errorHandlers.ts +194 -0
  118. package/src/utils/sessionHelpers.ts +151 -0
  119. package/src/utils/storageHelpers.ts +130 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Authentication helper utilities to reduce code duplication across hooks and utilities.
3
+ * These functions handle common token validation and authentication error patterns.
4
+ */
5
+ /**
6
+ * Error thrown when session sync is required
7
+ */
8
+ export class SessionSyncRequiredError extends Error {
9
+ constructor(message = 'Session needs to be synced. Please try again.') {
10
+ super(message);
11
+ this.name = 'SessionSyncRequiredError';
12
+ }
13
+ }
14
+ /**
15
+ * Error thrown when authentication fails
16
+ */
17
+ export class AuthenticationFailedError extends Error {
18
+ constructor(message = 'Authentication failed. Please sign in again.') {
19
+ super(message);
20
+ this.name = 'AuthenticationFailedError';
21
+ }
22
+ }
23
+ /**
24
+ * Ensures a valid token exists before making authenticated API calls.
25
+ * If no valid token exists and an active session ID is available,
26
+ * attempts to refresh the token using the session.
27
+ *
28
+ * @param oxyServices - The OxyServices instance
29
+ * @param activeSessionId - The active session ID (if available)
30
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
31
+ * @throws {Error} If token refresh fails for other reasons
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // In a mutation or query function:
36
+ * await ensureValidToken(oxyServices, activeSessionId);
37
+ * return await oxyServices.updateProfile(updates);
38
+ * ```
39
+ */
40
+ export async function ensureValidToken(oxyServices, activeSessionId) {
41
+ if (oxyServices.hasValidToken() || !activeSessionId) {
42
+ return;
43
+ }
44
+ try {
45
+ await oxyServices.getTokenBySession(activeSessionId);
46
+ }
47
+ catch (tokenError) {
48
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
49
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
50
+ throw new SessionSyncRequiredError();
51
+ }
52
+ throw tokenError;
53
+ }
54
+ }
55
+ /**
56
+ * Checks if an error is an authentication error (401 or auth-related message)
57
+ *
58
+ * @param error - The error to check
59
+ * @returns True if the error is an authentication error
60
+ */
61
+ export function isAuthenticationError(error) {
62
+ if (!error || typeof error !== 'object') {
63
+ return false;
64
+ }
65
+ const errorObj = error;
66
+ const errorMessage = errorObj.message || '';
67
+ const status = errorObj.status || errorObj.response?.status;
68
+ return (status === 401 ||
69
+ errorMessage.includes('Authentication required') ||
70
+ errorMessage.includes('Invalid or missing authorization header'));
71
+ }
72
+ /**
73
+ * Wraps an API call with authentication error handling.
74
+ * If an authentication error occurs, it can optionally attempt to sync the session and retry.
75
+ *
76
+ * @param apiCall - The API call function to execute
77
+ * @param options - Optional error handling configuration
78
+ * @returns The result of the API call
79
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
80
+ * @throws {Error} If the API call fails for non-auth reasons
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * // Simple usage:
85
+ * const result = await withAuthErrorHandling(
86
+ * () => oxyServices.updateProfile(updates)
87
+ * );
88
+ *
89
+ * // With retry on auth failure:
90
+ * const result = await withAuthErrorHandling(
91
+ * () => oxyServices.updateProfile(updates),
92
+ * { syncSession, activeSessionId, oxyServices }
93
+ * );
94
+ * ```
95
+ */
96
+ export async function withAuthErrorHandling(apiCall, options) {
97
+ try {
98
+ return await apiCall();
99
+ }
100
+ catch (error) {
101
+ if (!isAuthenticationError(error)) {
102
+ throw error;
103
+ }
104
+ // If we have sync capabilities, try to recover
105
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
106
+ try {
107
+ await options.syncSession();
108
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
109
+ // Retry the API call after refreshing token
110
+ return await apiCall();
111
+ }
112
+ catch {
113
+ throw new AuthenticationFailedError();
114
+ }
115
+ }
116
+ throw new AuthenticationFailedError();
117
+ }
118
+ }
119
+ /**
120
+ * Combines token validation and auth error handling for a complete authenticated API call.
121
+ * This is the recommended helper for most authenticated API operations.
122
+ *
123
+ * @param oxyServices - The OxyServices instance
124
+ * @param activeSessionId - The active session ID
125
+ * @param apiCall - The API call function to execute
126
+ * @param syncSession - Optional callback to sync session on auth failure
127
+ * @returns The result of the API call
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * return await authenticatedApiCall(
132
+ * oxyServices,
133
+ * activeSessionId,
134
+ * () => oxyServices.updateProfile(updates)
135
+ * );
136
+ * ```
137
+ */
138
+ export async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
139
+ await ensureValidToken(oxyServices, activeSessionId);
140
+ return withAuthErrorHandling(apiCall, {
141
+ syncSession,
142
+ activeSessionId,
143
+ oxyServices,
144
+ });
145
+ }
@@ -0,0 +1,72 @@
1
+ import { useAccountStore } from '../stores/accountStore';
2
+ import { useAuthStore } from '../stores/authStore';
3
+ import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
4
+ import { authenticatedApiCall } from './authHelpers';
5
+ /**
6
+ * Updates file visibility to public for avatar use.
7
+ * Handles errors gracefully, only logging non-404 errors.
8
+ *
9
+ * @param fileId - The file ID to update visibility for
10
+ * @param oxyServices - OxyServices instance
11
+ * @param contextName - Optional context name for logging
12
+ * @returns Promise that resolves when visibility is updated (or skipped)
13
+ */
14
+ export async function updateAvatarVisibility(fileId, oxyServices, contextName = 'AvatarUtils') {
15
+ // Skip if temporary asset ID or no file ID
16
+ if (!fileId || fileId.startsWith('temp-')) {
17
+ return;
18
+ }
19
+ try {
20
+ await oxyServices.assetUpdateVisibility(fileId, 'public');
21
+ // Visibility update is logged by the API
22
+ }
23
+ catch (visError) {
24
+ // Silently handle errors - 404 means asset doesn't exist yet (which is OK)
25
+ // Other errors are logged by the API, so no need to log here
26
+ // Function continues gracefully regardless of visibility update success
27
+ }
28
+ }
29
+ /**
30
+ * Refreshes avatar in accountStore with cache-busted URL to force image reload.
31
+ *
32
+ * @param sessionId - The session ID for the account to update
33
+ * @param avatarFileId - The new avatar file ID
34
+ * @param oxyServices - OxyServices instance to generate download URL
35
+ */
36
+ export function refreshAvatarInStore(sessionId, avatarFileId, oxyServices) {
37
+ const { updateAccount } = useAccountStore.getState();
38
+ const cacheBustedUrl = oxyServices.getFileDownloadUrl(avatarFileId, 'thumb') + `?t=${Date.now()}`;
39
+ updateAccount(sessionId, {
40
+ avatar: avatarFileId,
41
+ avatarUrl: cacheBustedUrl,
42
+ });
43
+ }
44
+ /**
45
+ * Updates user profile with avatar and handles all side effects (query invalidation, accountStore update).
46
+ * This function can be used from within OxyContext provider without requiring useOxy hook.
47
+ *
48
+ * @param updates - Profile updates including avatar
49
+ * @param oxyServices - OxyServices instance
50
+ * @param activeSessionId - Active session ID
51
+ * @param queryClient - TanStack Query client
52
+ * @param syncSession - Optional function to sync session/refresh token when auth errors occur
53
+ * @returns Promise that resolves with updated user data
54
+ */
55
+ export async function updateProfileWithAvatar(updates, oxyServices, activeSessionId, queryClient, syncSession) {
56
+ const data = await authenticatedApiCall(oxyServices, activeSessionId, () => oxyServices.updateProfile(updates), syncSession);
57
+ // Update cache with server response
58
+ queryClient.setQueryData(queryKeys.accounts.current(), data);
59
+ if (activeSessionId) {
60
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
61
+ }
62
+ // Update authStore so frontend components see the changes immediately
63
+ useAuthStore.getState().setUser(data);
64
+ // If avatar was updated, refresh accountStore with cache-busted URL
65
+ if (updates.avatar && activeSessionId) {
66
+ refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
67
+ }
68
+ // Invalidate all related queries to refresh everywhere
69
+ invalidateUserQueries(queryClient);
70
+ invalidateAccountQueries(queryClient);
71
+ return data;
72
+ }
@@ -0,0 +1,121 @@
1
+ const DEFAULT_INVALID_SESSION_MESSAGES = [
2
+ 'Invalid or expired session',
3
+ 'Session is invalid',
4
+ 'Session not found',
5
+ 'Session expired',
6
+ ];
7
+ const isObject = (value) => typeof value === 'object' && value !== null;
8
+ const getResponseStatus = (error) => {
9
+ if (!isObject(error))
10
+ return undefined;
11
+ const response = error.response;
12
+ return response?.status;
13
+ };
14
+ /**
15
+ * Determine whether the error represents an invalid session condition.
16
+ * This centralizes 401 detection across different fetch clients.
17
+ */
18
+ export const isInvalidSessionError = (error) => {
19
+ const status = getResponseStatus(error);
20
+ if (status === 401) {
21
+ return true;
22
+ }
23
+ if (!isObject(error)) {
24
+ return false;
25
+ }
26
+ // Check error.status directly (HttpService sets this)
27
+ if (error.status === 401) {
28
+ return true;
29
+ }
30
+ const normalizedMessage = extractErrorMessage(error)?.toLowerCase();
31
+ if (!normalizedMessage) {
32
+ return false;
33
+ }
34
+ // Check for HTTP 401 in message (HttpService creates errors with "HTTP 401:" format)
35
+ if (normalizedMessage.includes('http 401') || normalizedMessage.includes('401')) {
36
+ return true;
37
+ }
38
+ return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) => normalizedMessage.includes(msg.toLowerCase()));
39
+ };
40
+ /**
41
+ * Determine whether the error represents a timeout or network error.
42
+ * These are expected when the device is offline or has poor connectivity.
43
+ */
44
+ export const isTimeoutOrNetworkError = (error) => {
45
+ if (!isObject(error) && !(error instanceof Error)) {
46
+ return false;
47
+ }
48
+ const message = extractErrorMessage(error, '').toLowerCase();
49
+ const errorCode = error.code;
50
+ // Check for timeout/cancelled messages
51
+ if (message.includes('timeout') ||
52
+ message.includes('cancelled') ||
53
+ message.includes('econnaborted') ||
54
+ message.includes('aborted') ||
55
+ message.includes('request timeout or cancelled')) {
56
+ return true;
57
+ }
58
+ // Check for timeout/network error codes
59
+ if (errorCode === 'TIMEOUT' || errorCode === 'NETWORK_ERROR' || errorCode === 'ECONNABORTED') {
60
+ return true;
61
+ }
62
+ // Check for AbortError
63
+ if (error instanceof Error && error.name === 'AbortError') {
64
+ return true;
65
+ }
66
+ // Check for network-related TypeErrors
67
+ if (error instanceof TypeError) {
68
+ const typeErrorMessage = error.message.toLowerCase();
69
+ if (typeErrorMessage.includes('fetch') ||
70
+ typeErrorMessage.includes('network') ||
71
+ typeErrorMessage.includes('failed to fetch')) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ };
77
+ /**
78
+ * Extract a consistent error message from unknown error shapes.
79
+ *
80
+ * @param error - The unknown error payload
81
+ * @param fallbackMessage - Message to return when no concrete message is available
82
+ */
83
+ export const extractErrorMessage = (error, fallbackMessage = 'Unexpected error') => {
84
+ if (typeof error === 'string' && error.trim().length > 0) {
85
+ return error;
86
+ }
87
+ if (!isObject(error)) {
88
+ return fallbackMessage;
89
+ }
90
+ const withMessage = error;
91
+ if (withMessage.message && withMessage.message.trim().length > 0) {
92
+ return withMessage.message;
93
+ }
94
+ const withResponse = error;
95
+ const responseMessage = withResponse.response?.data?.message ?? withResponse.response?.data?.error;
96
+ if (typeof responseMessage === 'string' && responseMessage.trim().length > 0) {
97
+ return responseMessage;
98
+ }
99
+ return fallbackMessage;
100
+ };
101
+ /**
102
+ * Centralized error handler for auth-related operations.
103
+ *
104
+ * @param error - Unknown error object
105
+ * @param options - Error handling configuration
106
+ * @returns Resolved error message
107
+ */
108
+ export const handleAuthError = (error, { defaultMessage, code, status, onError, setAuthError, logger, }) => {
109
+ const resolvedStatus = status ?? getResponseStatus(error) ?? (isInvalidSessionError(error) ? 401 : 500);
110
+ const message = extractErrorMessage(error, defaultMessage);
111
+ if (logger) {
112
+ logger(message, error);
113
+ }
114
+ setAuthError?.(message);
115
+ onError?.({
116
+ message,
117
+ code,
118
+ status: resolvedStatus,
119
+ });
120
+ return message;
121
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Normalize backend session payloads into `ClientSession` objects.
3
+ *
4
+ * @param sessions - Raw session array returned from the API
5
+ * @param fallbackDeviceId - Device identifier to use when missing from payload
6
+ * @param fallbackUserId - User identifier to use when missing from payload
7
+ */
8
+ export const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId) => {
9
+ const now = new Date();
10
+ return sessions.map((session) => ({
11
+ sessionId: session.sessionId,
12
+ deviceId: session.deviceId || fallbackDeviceId || '',
13
+ expiresAt: session.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
14
+ lastActive: session.lastActive || now.toISOString(),
15
+ userId: session.user?.id ||
16
+ session.userId ||
17
+ (session.user?._id ? session.user._id.toString() : undefined) ||
18
+ fallbackUserId ||
19
+ '',
20
+ isCurrent: Boolean(session.isCurrent),
21
+ }));
22
+ };
23
+ /**
24
+ * Fetch device sessions with fallback to the legacy session endpoint when needed.
25
+ *
26
+ * @param oxyServices - Oxy service instance
27
+ * @param sessionId - Session identifier to fetch
28
+ * @param options - Optional fallback options
29
+ */
30
+ export const fetchSessionsWithFallback = async (oxyServices, sessionId, { fallbackDeviceId, fallbackUserId, logger, } = {}) => {
31
+ try {
32
+ const deviceSessions = await oxyServices.getDeviceSessions(sessionId);
33
+ return mapSessionsToClient(deviceSessions, fallbackDeviceId, fallbackUserId);
34
+ }
35
+ catch (error) {
36
+ if (__DEV__ && logger) {
37
+ logger('Failed to get device sessions, falling back to user sessions', error);
38
+ }
39
+ const userSessions = await oxyServices.getSessionsBySessionId(sessionId);
40
+ return mapSessionsToClient(userSessions, fallbackDeviceId, fallbackUserId);
41
+ }
42
+ };
43
+ /**
44
+ * Validate multiple sessions concurrently with configurable concurrency.
45
+ *
46
+ * @param oxyServices - Oxy service instance
47
+ * @param sessionIds - Session identifiers to validate
48
+ * @param options - Validation options
49
+ */
50
+ export const validateSessionBatch = async (oxyServices, sessionIds, { useHeaderValidation = true, maxConcurrency = 5 } = {}) => {
51
+ if (!sessionIds.length) {
52
+ return [];
53
+ }
54
+ const uniqueSessionIds = Array.from(new Set(sessionIds));
55
+ const safeConcurrency = Math.max(1, Math.min(maxConcurrency, uniqueSessionIds.length));
56
+ const results = [];
57
+ let index = 0;
58
+ const worker = async () => {
59
+ while (index < uniqueSessionIds.length) {
60
+ const currentIndex = index;
61
+ index += 1;
62
+ const sessionId = uniqueSessionIds[currentIndex];
63
+ try {
64
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation });
65
+ const valid = Boolean(validation?.valid);
66
+ results.push({
67
+ sessionId,
68
+ valid,
69
+ user: validation?.user,
70
+ raw: validation,
71
+ });
72
+ }
73
+ catch (error) {
74
+ results.push({
75
+ sessionId,
76
+ valid: false,
77
+ error,
78
+ });
79
+ }
80
+ }
81
+ };
82
+ await Promise.all(Array.from({ length: safeConcurrency }, worker));
83
+ return results;
84
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Create an in-memory storage implementation used as a safe fallback.
3
+ */
4
+ const MEMORY_STORAGE = () => {
5
+ const store = new Map();
6
+ return {
7
+ async getItem(key) {
8
+ return store.has(key) ? store.get(key) : null;
9
+ },
10
+ async setItem(key, value) {
11
+ store.set(key, value);
12
+ },
13
+ async removeItem(key) {
14
+ store.delete(key);
15
+ },
16
+ async clear() {
17
+ store.clear();
18
+ },
19
+ };
20
+ };
21
+ /**
22
+ * Create a web storage implementation backed by `localStorage`.
23
+ * Falls back to in-memory storage when unavailable.
24
+ */
25
+ const createWebStorage = () => {
26
+ if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
27
+ return MEMORY_STORAGE();
28
+ }
29
+ return {
30
+ async getItem(key) {
31
+ try {
32
+ return window.localStorage.getItem(key);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ },
38
+ async setItem(key, value) {
39
+ try {
40
+ window.localStorage.setItem(key, value);
41
+ }
42
+ catch {
43
+ // Ignore quota or access issues for now.
44
+ }
45
+ },
46
+ async removeItem(key) {
47
+ try {
48
+ window.localStorage.removeItem(key);
49
+ }
50
+ catch {
51
+ // Ignore failures.
52
+ }
53
+ },
54
+ async clear() {
55
+ try {
56
+ window.localStorage.clear();
57
+ }
58
+ catch {
59
+ // Ignore failures.
60
+ }
61
+ },
62
+ };
63
+ };
64
+ let asyncStorageInstance = null;
65
+ /**
66
+ * Lazily import React Native AsyncStorage implementation.
67
+ */
68
+ const createNativeStorage = async () => {
69
+ if (asyncStorageInstance) {
70
+ return asyncStorageInstance;
71
+ }
72
+ try {
73
+ const asyncStorageModule = await import('@react-native-async-storage/async-storage');
74
+ asyncStorageInstance = asyncStorageModule.default;
75
+ return asyncStorageInstance;
76
+ }
77
+ catch (error) {
78
+ if (__DEV__) {
79
+ console.error('Failed to import AsyncStorage:', error);
80
+ }
81
+ throw new Error('AsyncStorage is required in React Native environment');
82
+ }
83
+ };
84
+ /**
85
+ * Detect whether the current runtime is React Native.
86
+ */
87
+ export const isReactNative = () => typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
88
+ /**
89
+ * Create a platform-appropriate storage implementation.
90
+ * Defaults to in-memory storage when no platform storage is available.
91
+ */
92
+ export const createPlatformStorage = async () => {
93
+ if (isReactNative()) {
94
+ return createNativeStorage();
95
+ }
96
+ return createWebStorage();
97
+ };
98
+ export const STORAGE_KEY_PREFIX = 'oxy_session';
99
+ /**
100
+ * Produce strongly typed storage key names for the supplied prefix.
101
+ *
102
+ * @param prefix - Storage key prefix
103
+ */
104
+ export const getStorageKeys = (prefix = STORAGE_KEY_PREFIX) => ({
105
+ activeSessionId: `${prefix}_active_session_id`,
106
+ sessionIds: `${prefix}_session_ids`,
107
+ language: `${prefix}_language`,
108
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @oxyhq/auth — Web Authentication Provider
3
+ *
4
+ * Clean implementation with ZERO React Native dependencies.
5
+ * Provides FedCM, popup, and redirect authentication methods.
6
+ * Uses centralized AuthManager for token and session management.
7
+ */
8
+ import { type ReactNode } from 'react';
9
+ import { OxyServices, CrossDomainAuth, AuthManager } from '@oxyhq/core';
10
+ import type { User, ClientSession } from '@oxyhq/core';
11
+ export interface WebAuthState {
12
+ user: User | null;
13
+ isAuthenticated: boolean;
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ activeSessionId: string | null;
17
+ sessions: ClientSession[];
18
+ }
19
+ export interface WebAuthActions {
20
+ signIn: () => Promise<void>;
21
+ signInWithFedCM: () => Promise<void>;
22
+ signInWithPopup: () => Promise<void>;
23
+ signInWithRedirect: () => void;
24
+ signOut: () => Promise<void>;
25
+ isFedCMSupported: () => boolean;
26
+ switchSession: (sessionId: string) => Promise<void>;
27
+ clearSessionState: () => Promise<void>;
28
+ }
29
+ export interface WebOxyContextValue extends WebAuthState, WebAuthActions {
30
+ oxyServices: OxyServices;
31
+ crossDomainAuth: CrossDomainAuth;
32
+ authManager: AuthManager;
33
+ }
34
+ export interface WebOxyProviderProps {
35
+ children: ReactNode;
36
+ baseURL: string;
37
+ authWebUrl?: string;
38
+ onAuthStateChange?: (user: User | null) => void;
39
+ onError?: (error: Error) => void;
40
+ preferredAuthMethod?: 'auto' | 'fedcm' | 'popup' | 'redirect';
41
+ skipAutoCheck?: boolean;
42
+ }
43
+ /**
44
+ * Web-only Oxy Provider
45
+ *
46
+ * Provides authentication context for pure web applications (React, Next.js, Vite).
47
+ * Supports FedCM, popup, and redirect authentication methods.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * import { WebOxyProvider, useAuth } from '@oxyhq/auth';
52
+ *
53
+ * function App() {
54
+ * return (
55
+ * <WebOxyProvider baseURL="https://api.oxy.so">
56
+ * <YourApp />
57
+ * </WebOxyProvider>
58
+ * );
59
+ * }
60
+ * ```
61
+ */
62
+ export declare function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onError, preferredAuthMethod, skipAutoCheck, }: WebOxyProviderProps): import("react/jsx-runtime").JSX.Element;
63
+ /**
64
+ * Hook to access the full Web Oxy context.
65
+ */
66
+ export declare function useWebOxy(): WebOxyContextValue;
67
+ /**
68
+ * Hook for authentication in web apps.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * function LoginPage() {
73
+ * const { user, isAuthenticated, signIn, signOut } = useAuth();
74
+ * if (!isAuthenticated) return <button onClick={signIn}>Sign in</button>;
75
+ * return <button onClick={signOut}>Sign out</button>;
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function useAuth(): {
80
+ user: User | null;
81
+ isAuthenticated: boolean;
82
+ isLoading: boolean;
83
+ isReady: boolean;
84
+ error: string | null;
85
+ activeSessionId: string | null;
86
+ sessions: ClientSession[];
87
+ signIn: () => Promise<void>;
88
+ signInWithFedCM: () => Promise<void>;
89
+ signInWithPopup: () => Promise<void>;
90
+ signInWithRedirect: () => void;
91
+ signOut: () => Promise<void>;
92
+ isFedCMSupported: () => boolean;
93
+ switchSession: (sessionId: string) => Promise<void>;
94
+ oxyServices: OxyServices;
95
+ authManager: AuthManager;
96
+ };
97
+ export default WebOxyProvider;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mutation Hooks
3
+ *
4
+ * TanStack Query mutation hooks for updating Oxy services data.
5
+ * All mutations handle authentication, error handling, and query invalidation.
6
+ */
7
+ export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdatePrivacySettings, useUploadFile, } from './useAccountMutations';
8
+ export { useSwitchSession, useLogoutSession, useLogoutAll, useUpdateDeviceName, useRemoveDevice, } from './useServicesMutations';