@oxyhq/services 5.16.12 → 5.16.13

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 (43) hide show
  1. package/lib/commonjs/crypto/keyManager.js +87 -22
  2. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  3. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js +3 -1
  4. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js.map +1 -1
  5. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js +3 -1
  6. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +28 -36
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +1 -1
  10. package/lib/commonjs/ui/context/utils/errorHandlers.js +10 -0
  11. package/lib/commonjs/ui/context/utils/errorHandlers.js.map +1 -1
  12. package/lib/commonjs/ui/utils/avatarUtils.js +4 -5
  13. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  14. package/lib/module/crypto/keyManager.js +87 -22
  15. package/lib/module/crypto/keyManager.js.map +1 -1
  16. package/lib/module/ui/components/TextField/TextFieldFlat.js +3 -1
  17. package/lib/module/ui/components/TextField/TextFieldFlat.js.map +1 -1
  18. package/lib/module/ui/components/TextField/TextFieldOutlined.js +3 -1
  19. package/lib/module/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  20. package/lib/module/ui/context/OxyContext.js +23 -30
  21. package/lib/module/ui/context/OxyContext.js.map +1 -1
  22. package/lib/module/ui/context/hooks/useSessionManagement.js +1 -1
  23. package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
  24. package/lib/module/ui/context/utils/errorHandlers.js +10 -0
  25. package/lib/module/ui/context/utils/errorHandlers.js.map +1 -1
  26. package/lib/module/ui/utils/avatarUtils.js +4 -5
  27. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  28. package/lib/typescript/crypto/keyManager.d.ts +9 -2
  29. package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
  30. package/lib/typescript/models/interfaces.d.ts +0 -4
  31. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  32. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  33. package/lib/typescript/ui/context/utils/errorHandlers.d.ts.map +1 -1
  34. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  35. package/package.json +5 -4
  36. package/src/crypto/keyManager.ts +92 -16
  37. package/src/models/interfaces.ts +1 -5
  38. package/src/ui/components/TextField/TextFieldFlat.tsx +1 -1
  39. package/src/ui/components/TextField/TextFieldOutlined.tsx +1 -1
  40. package/src/ui/context/OxyContext.tsx +26 -28
  41. package/src/ui/context/hooks/useSessionManagement.ts +1 -1
  42. package/src/ui/context/utils/errorHandlers.ts +10 -0
  43. package/src/ui/utils/avatarUtils.ts +4 -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
  }
@@ -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
  }
@@ -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
  );
@@ -25,12 +25,11 @@ export async function updateAvatarVisibility(
25
25
 
26
26
  try {
27
27
  await oxyServices.assetUpdateVisibility(fileId, 'public');
28
- console.log(`[${contextName}] Avatar visibility updated to public`);
28
+ // Visibility update is logged by the API
29
29
  } 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
- }
30
+ // Silently handle errors - 404 means asset doesn't exist yet (which is OK)
31
+ // Other errors are logged by the API, so no need to log here
32
+ // Function continues gracefully regardless of visibility update success
34
33
  }
35
34
  }
36
35