@oxyhq/services 5.16.12 → 5.16.14

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 (89) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.security.js +54 -0
  2. package/lib/commonjs/core/mixins/OxyServices.security.js.map +1 -0
  3. package/lib/commonjs/core/mixins/index.js +3 -2
  4. package/lib/commonjs/core/mixins/index.js.map +1 -1
  5. package/lib/commonjs/crypto/keyManager.js +87 -22
  6. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  7. package/lib/commonjs/index.js.map +1 -1
  8. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js +3 -1
  9. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js.map +1 -1
  10. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js +3 -1
  11. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  12. package/lib/commonjs/ui/context/OxyContext.js +28 -36
  13. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  14. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +1 -1
  15. package/lib/commonjs/ui/context/utils/errorHandlers.js +10 -0
  16. package/lib/commonjs/ui/context/utils/errorHandlers.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +16 -2
  18. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/queries/index.js +13 -0
  20. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/queries/queryKeys.js +6 -0
  22. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/queries/useSecurityQueries.js +58 -0
  24. package/lib/commonjs/ui/hooks/queries/useSecurityQueries.js.map +1 -0
  25. package/lib/commonjs/ui/utils/avatarUtils.js +8 -5
  26. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  27. package/lib/module/core/mixins/OxyServices.security.js +50 -0
  28. package/lib/module/core/mixins/OxyServices.security.js.map +1 -0
  29. package/lib/module/core/mixins/index.js +2 -1
  30. package/lib/module/core/mixins/index.js.map +1 -1
  31. package/lib/module/crypto/keyManager.js +87 -22
  32. package/lib/module/crypto/keyManager.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/components/TextField/TextFieldFlat.js +3 -1
  35. package/lib/module/ui/components/TextField/TextFieldFlat.js.map +1 -1
  36. package/lib/module/ui/components/TextField/TextFieldOutlined.js +3 -1
  37. package/lib/module/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  38. package/lib/module/ui/context/OxyContext.js +23 -30
  39. package/lib/module/ui/context/OxyContext.js.map +1 -1
  40. package/lib/module/ui/context/hooks/useSessionManagement.js +1 -1
  41. package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
  42. package/lib/module/ui/context/utils/errorHandlers.js +10 -0
  43. package/lib/module/ui/context/utils/errorHandlers.js.map +1 -1
  44. package/lib/module/ui/hooks/mutations/useAccountMutations.js +16 -2
  45. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  46. package/lib/module/ui/hooks/queries/index.js +3 -0
  47. package/lib/module/ui/hooks/queries/index.js.map +1 -1
  48. package/lib/module/ui/hooks/queries/queryKeys.js +6 -0
  49. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  50. package/lib/module/ui/hooks/queries/useSecurityQueries.js +52 -0
  51. package/lib/module/ui/hooks/queries/useSecurityQueries.js.map +1 -0
  52. package/lib/module/ui/utils/avatarUtils.js +8 -5
  53. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  54. package/lib/typescript/core/mixins/OxyServices.security.d.ts +67 -0
  55. package/lib/typescript/core/mixins/OxyServices.security.d.ts.map +1 -0
  56. package/lib/typescript/core/mixins/index.d.ts +48 -0
  57. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  58. package/lib/typescript/crypto/keyManager.d.ts +9 -2
  59. package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
  60. package/lib/typescript/index.d.ts +1 -1
  61. package/lib/typescript/index.d.ts.map +1 -1
  62. package/lib/typescript/models/interfaces.d.ts +34 -4
  63. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  64. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  65. package/lib/typescript/ui/context/utils/errorHandlers.d.ts.map +1 -1
  66. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  67. package/lib/typescript/ui/hooks/queries/index.d.ts +1 -0
  68. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
  69. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +5 -0
  70. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  71. package/lib/typescript/ui/hooks/queries/useSecurityQueries.d.ts +15 -0
  72. package/lib/typescript/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -0
  73. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  74. package/package.json +4 -3
  75. package/src/core/mixins/OxyServices.security.ts +59 -0
  76. package/src/core/mixins/index.ts +14 -11
  77. package/src/crypto/keyManager.ts +92 -16
  78. package/src/index.ts +6 -1
  79. package/src/models/interfaces.ts +48 -5
  80. package/src/ui/components/TextField/TextFieldFlat.tsx +1 -1
  81. package/src/ui/components/TextField/TextFieldOutlined.tsx +1 -1
  82. package/src/ui/context/OxyContext.tsx +26 -28
  83. package/src/ui/context/hooks/useSessionManagement.ts +1 -1
  84. package/src/ui/context/utils/errorHandlers.ts +10 -0
  85. package/src/ui/hooks/mutations/useAccountMutations.ts +17 -2
  86. package/src/ui/hooks/queries/index.ts +6 -0
  87. package/src/ui/hooks/queries/queryKeys.ts +9 -0
  88. package/src/ui/hooks/queries/useSecurityQueries.ts +64 -0
  89. package/src/ui/utils/avatarUtils.ts +8 -5
@@ -28,7 +28,15 @@ const STORAGE_KEYS = {
28
28
  */
29
29
  async function initSecureStore(): Promise<typeof import('expo-secure-store')> {
30
30
  if (!SecureStore) {
31
- SecureStore = await import('expo-secure-store');
31
+ try {
32
+ SecureStore = await import('expo-secure-store');
33
+ } catch (error) {
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
36
+ }
37
+ }
38
+ if (!SecureStore) {
39
+ throw new Error('expo-secure-store module is not available');
32
40
  }
33
41
  return SecureStore;
34
42
  }
@@ -95,6 +103,19 @@ export interface KeyPair {
95
103
  }
96
104
 
97
105
  export class KeyManager {
106
+ // In-memory cache for identity state (invalidated on identity changes)
107
+ private static cachedPublicKey: string | null = null;
108
+ private static cachedHasIdentity: boolean | null = null;
109
+
110
+ /**
111
+ * Invalidate cached identity state
112
+ * Called internally when identity is created/deleted/imported
113
+ */
114
+ private static invalidateCache(): void {
115
+ KeyManager.cachedPublicKey = null;
116
+ KeyManager.cachedHasIdentity = null;
117
+ }
118
+
98
119
  /**
99
120
  * Generate a new ECDSA secp256k1 key pair
100
121
  * Returns the keys in hexadecimal format
@@ -129,14 +150,16 @@ export class KeyManager {
129
150
  const store = await initSecureStore();
130
151
  const { privateKey, publicKey } = await KeyManager.generateKeyPair();
131
152
 
132
- // Store private key securely
133
153
  await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, privateKey, {
134
154
  keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
135
155
  });
136
156
 
137
- // Store public key (for quick access without deriving)
138
157
  await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, publicKey);
139
158
 
159
+ // Update cache
160
+ KeyManager.cachedPublicKey = publicKey;
161
+ KeyManager.cachedHasIdentity = true;
162
+
140
163
  return publicKey;
141
164
  }
142
165
 
@@ -146,16 +169,18 @@ export class KeyManager {
146
169
  static async importKeyPair(privateKey: string): Promise<string> {
147
170
  const store = await initSecureStore();
148
171
 
149
- // Derive public key from private key
150
172
  const keyPair = ec.keyFromPrivate(privateKey);
151
173
  const publicKey = keyPair.getPublic('hex');
152
174
 
153
- // Store both keys
154
175
  await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, privateKey, {
155
176
  keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
156
177
  });
157
178
  await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, publicKey);
158
179
 
180
+ // Update cache
181
+ KeyManager.cachedPublicKey = publicKey;
182
+ KeyManager.cachedHasIdentity = true;
183
+
159
184
  return publicKey;
160
185
  }
161
186
 
@@ -164,24 +189,71 @@ export class KeyManager {
164
189
  * WARNING: Only use this for signing operations within the app
165
190
  */
166
191
  static async getPrivateKey(): Promise<string | null> {
167
- const store = await initSecureStore();
168
- return store.getItemAsync(STORAGE_KEYS.PRIVATE_KEY);
192
+ try {
193
+ const store = await initSecureStore();
194
+ return await store.getItemAsync(STORAGE_KEYS.PRIVATE_KEY);
195
+ } catch (error) {
196
+ // If secure store is not available, return null (no identity)
197
+ // This allows the app to continue functioning even if secure store fails to load
198
+ if (__DEV__) {
199
+ console.warn('[KeyManager] Failed to access secure store:', error);
200
+ }
201
+ return null;
202
+ }
169
203
  }
170
204
 
171
205
  /**
172
- * Get the stored public key
206
+ * Get the stored public key (cached for performance)
173
207
  */
174
208
  static async getPublicKey(): Promise<string | null> {
175
- const store = await initSecureStore();
176
- return store.getItemAsync(STORAGE_KEYS.PUBLIC_KEY);
209
+ if (KeyManager.cachedPublicKey !== null) {
210
+ return KeyManager.cachedPublicKey;
211
+ }
212
+
213
+ try {
214
+ const store = await initSecureStore();
215
+ const publicKey = await store.getItemAsync(STORAGE_KEYS.PUBLIC_KEY);
216
+
217
+ // Cache result (null is a valid cache value meaning no identity)
218
+ KeyManager.cachedPublicKey = publicKey;
219
+
220
+ return publicKey;
221
+ } catch (error) {
222
+ // If secure store is not available, return null (no identity)
223
+ // Cache null to avoid repeated failed attempts
224
+ KeyManager.cachedPublicKey = null;
225
+ if (__DEV__) {
226
+ console.warn('[KeyManager] Failed to access secure store:', error);
227
+ }
228
+ return null;
229
+ }
177
230
  }
178
231
 
179
232
  /**
180
- * Check if an identity (key pair) exists on this device
233
+ * Check if an identity (key pair) exists on this device (cached for performance)
181
234
  */
182
235
  static async hasIdentity(): Promise<boolean> {
183
- const privateKey = await KeyManager.getPrivateKey();
184
- return privateKey !== null;
236
+ if (KeyManager.cachedHasIdentity !== null) {
237
+ return KeyManager.cachedHasIdentity;
238
+ }
239
+
240
+ try {
241
+ const privateKey = await KeyManager.getPrivateKey();
242
+ const hasIdentity = privateKey !== null;
243
+
244
+ // Cache result
245
+ KeyManager.cachedHasIdentity = hasIdentity;
246
+
247
+ return hasIdentity;
248
+ } catch (error) {
249
+ // If we can't check, assume no identity (safer default)
250
+ // Cache false to avoid repeated failed attempts
251
+ KeyManager.cachedHasIdentity = false;
252
+ if (__DEV__) {
253
+ console.warn('[KeyManager] Failed to check identity:', error);
254
+ }
255
+ return false;
256
+ }
185
257
  }
186
258
 
187
259
  /**
@@ -228,6 +300,9 @@ export class KeyManager {
228
300
  await store.deleteItemAsync(STORAGE_KEYS.PRIVATE_KEY);
229
301
  await store.deleteItemAsync(STORAGE_KEYS.PUBLIC_KEY);
230
302
 
303
+ // Invalidate cache
304
+ KeyManager.invalidateCache();
305
+
231
306
  // Also clear backup if force deletion
232
307
  if (force) {
233
308
  try {
@@ -343,16 +418,17 @@ export class KeyManager {
343
418
  return false; // Backup keys don't match
344
419
  }
345
420
 
346
- // Restore from backup
347
421
  await store.setItemAsync(STORAGE_KEYS.PRIVATE_KEY, backupPrivateKey, {
348
422
  keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
349
423
  });
350
424
  await store.setItemAsync(STORAGE_KEYS.PUBLIC_KEY, backupPublicKey);
351
425
 
352
- // Verify restoration was successful
353
426
  const restored = await KeyManager.verifyIdentityIntegrity();
354
427
  if (restored) {
355
- // Update backup timestamp
428
+ // Update cache
429
+ KeyManager.cachedPublicKey = backupPublicKey;
430
+ KeyManager.cachedHasIdentity = true;
431
+
356
432
  await store.setItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString());
357
433
  return true;
358
434
  }
package/src/index.ts CHANGED
@@ -103,7 +103,12 @@ export type {
103
103
  AssetUpdateVisibilityResponse,
104
104
  // Account storage usage
105
105
  AccountStorageCategoryUsage,
106
- AccountStorageUsageResponse
106
+ AccountStorageUsageResponse,
107
+ // Security activity
108
+ SecurityEventType,
109
+ SecurityEventSeverity,
110
+ SecurityActivity,
111
+ SecurityActivityResponse
107
112
  } from './models/interfaces';
108
113
 
109
114
  export type {
@@ -50,15 +50,11 @@ export interface User {
50
50
  image?: string;
51
51
  link: string;
52
52
  }>;
53
- // Social counts - can be returned by API in different formats
53
+ // Social counts
54
54
  _count?: {
55
55
  followers?: number;
56
56
  following?: number;
57
57
  };
58
- stats?: {
59
- followers?: number;
60
- following?: number;
61
- };
62
58
  accountExpiresAfterInactivityDays?: number | null; // Days of inactivity before account expires (null = never expire)
63
59
  [key: string]: unknown;
64
60
  }
@@ -407,6 +403,53 @@ export interface AccountStorageUsageResponse {
407
403
  updatedAt: string;
408
404
  }
409
405
 
406
+ /**
407
+ * Security activity event types
408
+ */
409
+ export type SecurityEventType =
410
+ | 'sign_in'
411
+ | 'sign_out'
412
+ | 'email_changed'
413
+ | 'profile_updated'
414
+ | 'device_added'
415
+ | 'device_removed'
416
+ | 'account_recovery'
417
+ | 'security_settings_changed'
418
+ | 'suspicious_activity';
419
+
420
+ /**
421
+ * Security event severity levels
422
+ */
423
+ export type SecurityEventSeverity = 'low' | 'medium' | 'high' | 'critical';
424
+
425
+ /**
426
+ * Security activity event
427
+ */
428
+ export interface SecurityActivity {
429
+ id: string;
430
+ userId: string;
431
+ eventType: SecurityEventType;
432
+ eventDescription: string;
433
+ metadata?: Record<string, any>;
434
+ ipAddress?: string;
435
+ userAgent?: string;
436
+ deviceId?: string;
437
+ timestamp: string;
438
+ severity: SecurityEventSeverity;
439
+ createdAt: string;
440
+ }
441
+
442
+ /**
443
+ * Security activity response with pagination
444
+ */
445
+ export interface SecurityActivityResponse {
446
+ data: SecurityActivity[];
447
+ total: number;
448
+ limit: number;
449
+ offset: number;
450
+ hasMore: boolean;
451
+ }
452
+
410
453
  export interface AssetUploadProgress {
411
454
  fileId: string;
412
455
  uploaded: number;
@@ -438,7 +438,7 @@ const TextInputFlat = ({
438
438
  flexShrink: 1,
439
439
  minWidth: 0,
440
440
  },
441
- Platform.OS === 'web' ? { outline: 'none' } : undefined,
441
+ Platform.OS === 'web' ? { outline: 'none', outlineWidth: 0, outlineStyle: 'none' } : undefined,
442
442
  adornmentStyleAdjustmentForNativeInput,
443
443
  contentStyle,
444
444
  ],
@@ -427,7 +427,7 @@ const TextInputOutlined = ({
427
427
  flexShrink: 1,
428
428
  minWidth: 0,
429
429
  },
430
- Platform.OS === 'web' ? { outline: 'none' } : undefined,
430
+ Platform.OS === 'web' ? { outline: 'none', outlineWidth: 0, outlineStyle: 'none' } : undefined,
431
431
  adornmentStyleAdjustmentForNativeInput,
432
432
  contentStyle,
433
433
  ],
@@ -201,7 +201,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
201
201
 
202
202
  const checkAndRestoreIdentity = async () => {
203
203
  try {
204
- const { KeyManager } = await import('../../crypto/index.js');
205
204
  // Check if identity exists and verify integrity
206
205
  const hasIdentity = await KeyManager.hasIdentity();
207
206
  if (hasIdentity) {
@@ -209,14 +208,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
209
208
  if (!isValid) {
210
209
  // Try to restore from backup
211
210
  const restored = await KeyManager.restoreIdentityFromBackup();
212
- if (restored) {
213
- if (__DEV__) {
214
- logger('Identity restored from backup successfully');
215
- }
216
- } else {
217
- if (__DEV__) {
218
- logger('Identity integrity check failed - user may need to restore from recovery phrase');
219
- }
211
+ if (__DEV__) {
212
+ logger(restored
213
+ ? 'Identity restored from backup successfully'
214
+ : 'Identity integrity check failed - user may need to restore from recovery phrase'
215
+ );
220
216
  }
221
217
  } else {
222
218
  // Identity is valid - ensure backup is up to date
@@ -317,9 +313,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
317
313
  });
318
314
 
319
315
  // syncIdentity - TanStack Query handles offline mutations automatically
320
- const syncIdentity = useCallback(async () => {
321
- return await syncIdentityBase();
322
- }, [syncIdentityBase]);
316
+ const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
323
317
 
324
318
  // Clear all account data when identity is lost (for accounts app)
325
319
  // In accounts app, identity = account, so losing identity means losing everything
@@ -332,9 +326,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
332
326
  try {
333
327
  await clearQueryCache(storage);
334
328
  } catch (error) {
335
- if (logger) {
336
- logger('Failed to clear persisted query cache', error);
337
- }
329
+ logger('Failed to clear persisted query cache', error);
338
330
  }
339
331
  }
340
332
 
@@ -346,9 +338,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
346
338
  try {
347
339
  await storage.removeItem('oxy_identity_synced');
348
340
  } catch (error) {
349
- if (logger) {
350
- logger('Failed to clear identity sync state', error);
351
- }
341
+ logger('Failed to clear identity sync state', error);
352
342
  }
353
343
  }
354
344
 
@@ -419,20 +409,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
419
409
 
420
410
  // If we were offline and now we're online, sync identity if needed
421
411
  if (wasOffline) {
422
- if (__DEV__ && logger) {
423
- logger('Network reconnected, checking identity sync...');
424
- }
412
+ logger('Network reconnected, checking identity sync...');
425
413
 
426
414
  // Sync identity first (if not synced)
427
415
  try {
428
- const isSynced = await storage.getItem('oxy_identity_synced');
429
- if (isSynced === 'false') {
430
- await syncIdentity();
416
+ const hasIdentityValue = await hasIdentity();
417
+ if (hasIdentityValue) {
418
+ // Check sync status directly - sync if not explicitly 'true'
419
+ // undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
420
+ const syncStatus = await storage.getItem('oxy_identity_synced');
421
+ if (syncStatus !== 'true') {
422
+ await syncIdentity();
423
+ }
431
424
  }
432
425
  } catch (syncError) {
433
- if (__DEV__ && logger) {
434
- logger('Error syncing identity on reconnect', syncError);
435
- }
426
+ logger('Error syncing identity on reconnect', syncError);
436
427
  }
437
428
 
438
429
  // TanStack Query will automatically retry pending mutations
@@ -515,7 +506,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
515
506
  });
516
507
  }
517
508
  } catch (validationError) {
518
- logger('Session validation failed during init', validationError);
509
+ // Silently handle expected 401 errors (expired/invalid sessions) during restoration
510
+ // Only log unexpected errors
511
+ if (!isInvalidSessionError(validationError)) {
512
+ logger('Session validation failed during init', validationError);
513
+ }
519
514
  }
520
515
  }
521
516
 
@@ -528,13 +523,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
528
523
  try {
529
524
  await switchSession(storedActiveSessionId);
530
525
  } catch (switchError) {
526
+ // Silently handle expected 401 errors (expired/invalid active session)
531
527
  if (isInvalidSessionError(switchError)) {
532
528
  await storage.removeItem(storageKeys.activeSessionId);
533
529
  updateSessions(
534
530
  validSessions.filter((session) => session.sessionId !== storedActiveSessionId),
535
531
  { merge: false },
536
532
  );
533
+ // Don't log expected session errors during restoration
537
534
  } else {
535
+ // Only log unexpected errors
538
536
  logger('Active session validation error', switchError);
539
537
  }
540
538
  }
@@ -7,7 +7,7 @@ import { getStorageKeys, type StorageInterface } from '../utils/storageHelpers';
7
7
  import { handleAuthError, isInvalidSessionError } from '../utils/errorHandlers';
8
8
  import type { OxyServices } from '../../../core';
9
9
  import type { QueryClient } from '@tanstack/react-query';
10
- import { clearQueryCache } from '../../hooks/queryClient.js';
10
+ import { clearQueryCache } from '../../hooks/queryClient';
11
11
 
12
12
  export interface UseSessionManagementOptions {
13
13
  oxyServices: OxyServices;
@@ -53,11 +53,21 @@ export const isInvalidSessionError = (error: unknown): boolean => {
53
53
  return false;
54
54
  }
55
55
 
56
+ // Check error.status directly (HttpService sets this)
57
+ if ((error as any).status === 401) {
58
+ return true;
59
+ }
60
+
56
61
  const normalizedMessage = extractErrorMessage(error)?.toLowerCase();
57
62
  if (!normalizedMessage) {
58
63
  return false;
59
64
  }
60
65
 
66
+ // Check for HTTP 401 in message (HttpService creates errors with "HTTP 401:" format)
67
+ if (normalizedMessage.includes('http 401') || normalizedMessage.includes('401')) {
68
+ return true;
69
+ }
70
+
61
71
  return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) =>
62
72
  normalizedMessage.includes(msg.toLowerCase()),
63
73
  );
@@ -4,6 +4,7 @@ import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../q
4
4
  import { useOxy } from '../../context/OxyContext';
5
5
  import { toast } from '../../../lib/sonner';
6
6
  import { refreshAvatarInStore } from '../../utils/avatarUtils';
7
+ import { useAuthStore } from '../../stores/authStore';
7
8
 
8
9
  /**
9
10
  * Update user profile with optimistic updates and offline queue support
@@ -107,6 +108,9 @@ export const useUpdateProfile = () => {
107
108
  queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
108
109
  }
109
110
 
111
+ // Update authStore so frontend components see the changes immediately
112
+ useAuthStore.getState().setUser(data);
113
+
110
114
  // If avatar was updated, refresh accountStore with cache-busted URL
111
115
  if (updates.avatar && activeSessionId && oxyServices) {
112
116
  refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
@@ -220,6 +224,9 @@ export const useUploadAvatar = () => {
220
224
  queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
221
225
  }
222
226
 
227
+ // Update authStore so frontend components see the changes immediately
228
+ useAuthStore.getState().setUser(data);
229
+
223
230
  // Refresh accountStore with cache-busted URL if avatar was updated
224
231
  if (data?.avatar && activeSessionId && oxyServices) {
225
232
  refreshAvatarInStore(activeSessionId, data.avatar, oxyServices);
@@ -268,6 +275,10 @@ export const useUpdateAccountSettings = () => {
268
275
  },
269
276
  onSuccess: (data) => {
270
277
  queryClient.setQueryData(queryKeys.accounts.current(), data);
278
+
279
+ // Update authStore so frontend components see the changes immediately
280
+ useAuthStore.getState().setUser(data);
281
+
271
282
  invalidateAccountQueries(queryClient);
272
283
  toast.success('Settings updated successfully');
273
284
  },
@@ -394,10 +405,14 @@ export const useUpdatePrivacySettings = () => {
394
405
  // Also update account query if it contains privacy settings
395
406
  const currentUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
396
407
  if (currentUser) {
397
- queryClient.setQueryData<User>(queryKeys.accounts.current(), {
408
+ const updatedUser = {
398
409
  ...currentUser,
399
410
  privacySettings: data,
400
- });
411
+ };
412
+ queryClient.setQueryData<User>(queryKeys.accounts.current(), updatedUser);
413
+
414
+ // Update authStore so frontend components see the changes immediately
415
+ useAuthStore.getState().setUser(updatedUser);
401
416
  }
402
417
  invalidateAccountQueries(queryClient);
403
418
  },
@@ -25,6 +25,12 @@ export {
25
25
  useSecurityInfo,
26
26
  } from './useServicesQueries';
27
27
 
28
+ // Security activity query hooks
29
+ export {
30
+ useSecurityActivity,
31
+ useRecentSecurityActivity,
32
+ } from './useSecurityQueries';
33
+
28
34
  // Query keys and invalidation helpers (for advanced usage)
29
35
  export { queryKeys, invalidateAccountQueries, invalidateUserQueries, invalidateSessionQueries } from './queryKeys';
30
36
 
@@ -54,6 +54,15 @@ export const queryKeys = {
54
54
  all: ['privacy'] as const,
55
55
  settings: (userId?: string) => [...queryKeys.privacy.all, 'settings', userId || 'current'] as const,
56
56
  },
57
+
58
+ // Security activity queries
59
+ security: {
60
+ all: ['security'] as const,
61
+ activity: (limit?: number, offset?: number, eventType?: string) =>
62
+ [...queryKeys.security.all, 'activity', limit, offset, eventType] as const,
63
+ recent: (limit: number) =>
64
+ [...queryKeys.security.all, 'recent', limit] as const,
65
+ },
57
66
  } as const;
58
67
 
59
68
  /**
@@ -0,0 +1,64 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { queryKeys } from './queryKeys';
3
+ import { useOxy } from '../../context/OxyContext';
4
+ import type { SecurityActivity, SecurityEventType } from '../../../models/interfaces';
5
+
6
+ /**
7
+ * Get user's security activity with pagination
8
+ */
9
+ export const useSecurityActivity = (
10
+ options?: {
11
+ limit?: number;
12
+ offset?: number;
13
+ eventType?: SecurityEventType;
14
+ enabled?: boolean;
15
+ }
16
+ ) => {
17
+ const { oxyServices, activeSessionId } = useOxy();
18
+
19
+ return useQuery({
20
+ queryKey: queryKeys.security.activity(
21
+ options?.limit,
22
+ options?.offset,
23
+ options?.eventType
24
+ ),
25
+ queryFn: async () => {
26
+ if (!activeSessionId) {
27
+ throw new Error('No active session');
28
+ }
29
+
30
+ const response = await oxyServices.getSecurityActivity(
31
+ options?.limit,
32
+ options?.offset,
33
+ options?.eventType
34
+ );
35
+
36
+ return response;
37
+ },
38
+ enabled: (options?.enabled !== false) && !!activeSessionId,
39
+ staleTime: 5 * 60 * 1000, // 5 minutes
40
+ gcTime: 10 * 60 * 1000, // 10 minutes
41
+ });
42
+ };
43
+
44
+ /**
45
+ * Get recent security activity (convenience hook)
46
+ */
47
+ export const useRecentSecurityActivity = (limit: number = 10) => {
48
+ const { oxyServices, activeSessionId } = useOxy();
49
+
50
+ return useQuery<SecurityActivity[]>({
51
+ queryKey: queryKeys.security.recent(limit),
52
+ queryFn: async () => {
53
+ if (!activeSessionId) {
54
+ throw new Error('No active session');
55
+ }
56
+
57
+ return await oxyServices.getRecentSecurityActivity(limit);
58
+ },
59
+ enabled: !!activeSessionId,
60
+ staleTime: 5 * 60 * 1000, // 5 minutes
61
+ gcTime: 10 * 60 * 1000, // 10 minutes
62
+ });
63
+ };
64
+
@@ -1,6 +1,7 @@
1
1
  import type { OxyServices } from '../../core';
2
2
  import type { User } from '../../models/interfaces';
3
3
  import { useAccountStore } from '../stores/accountStore';
4
+ import { useAuthStore } from '../stores/authStore';
4
5
  import { QueryClient } from '@tanstack/react-query';
5
6
  import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
6
7
 
@@ -25,12 +26,11 @@ export async function updateAvatarVisibility(
25
26
 
26
27
  try {
27
28
  await oxyServices.assetUpdateVisibility(fileId, 'public');
28
- console.log(`[${contextName}] Avatar visibility updated to public`);
29
+ // Visibility update is logged by the API
29
30
  } catch (visError: any) {
30
- // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
31
- if (visError?.response?.status !== 404) {
32
- console.warn(`[${contextName}] Failed to update avatar visibility, continuing anyway:`, visError);
33
- }
31
+ // Silently handle errors - 404 means asset doesn't exist yet (which is OK)
32
+ // Other errors are logged by the API, so no need to log here
33
+ // Function continues gracefully regardless of visibility update success
34
34
  }
35
35
  }
36
36
 
@@ -104,6 +104,9 @@ export async function updateProfileWithAvatar(
104
104
  queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
105
105
  }
106
106
 
107
+ // Update authStore so frontend components see the changes immediately
108
+ useAuthStore.getState().setUser(data);
109
+
107
110
  // If avatar was updated, refresh accountStore with cache-busted URL
108
111
  if (updates.avatar && activeSessionId) {
109
112
  refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);