@oxyhq/services 5.16.38 → 5.16.40

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.
@@ -34,7 +34,7 @@ export interface UseAuthOperationsOptions {
34
34
 
35
35
  export interface UseAuthOperationsResult {
36
36
  /** Create a new identity locally (offline-first) and optionally sync with server */
37
- createIdentity: () => Promise<{ synced: boolean }>;
37
+ createIdentity: (username?: string) => Promise<{ synced: boolean }>;
38
38
  /** Import an existing identity from backup file data */
39
39
  importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
40
40
  /** Sign in with existing identity on device */
@@ -50,7 +50,7 @@ export interface UseAuthOperationsResult {
50
50
  /** Check if identity is synced with server */
51
51
  isIdentitySynced: () => Promise<boolean>;
52
52
  /** Sync local identity with server (when online) */
53
- syncIdentity: () => Promise<User>;
53
+ syncIdentity: (username?: string) => Promise<User>;
54
54
  }
55
55
 
56
56
  const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
@@ -313,18 +313,27 @@ export const useAuthOperations = ({
313
313
  );
314
314
 
315
315
  /**
316
- * Create a new identity (offline-first)
317
- * Identity is purely cryptographic - no username or email required
316
+ * Create a new identity (offline-first).
317
+ *
318
+ * Generates cryptographic keys locally without requiring server connection.
319
+ * Identity is based on the public/private key pair - no username or email required.
320
+ *
321
+ * IMPORTANT: This function only clears session data if the identity actually changes
322
+ * (i.e., a new key pair is generated). Retrying registration with the same identity
323
+ * will NOT clear session data.
324
+ *
325
+ * @param username - Optional username to set during registration (if online)
326
+ * @returns Object with synced status indicating if identity was registered with server
318
327
  */
319
328
  const createIdentity = useCallback(
320
- async (): Promise<{ synced: boolean }> => {
329
+ async (username?: string): Promise<{ synced: boolean }> => {
321
330
  if (!storage) throw new Error('Storage not initialized');
322
331
 
323
332
  setAuthState({ isLoading: true, error: null });
324
333
 
325
334
  try {
326
- // CRITICAL: Get old public key before creating new identity
327
- // If identity changes, we must clear all session data to prevent data leakage
335
+ // Get old public key before creating new identity
336
+ // This is used to detect if identity actually changed
328
337
  const oldPublicKey = await KeyManager.getPublicKey().catch(() => null);
329
338
 
330
339
  if (__DEV__ && logger) {
@@ -339,39 +348,56 @@ export const useAuthOperations = ({
339
348
  logger('Identity keys generated', { publicKey: publicKey.substring(0, 16) + '...' });
340
349
  }
341
350
 
342
- // Clear sessions if identity changed (prevents data leakage)
351
+ // Only clear sessions if identity actually changed
352
+ // This prevents clearing sessions on registration retries
343
353
  await clearSessionsIfIdentityChanged(oldPublicKey, publicKey);
344
354
 
345
- // Mark as not synced
355
+ // Mark as not synced initially
346
356
  await storage.setItem('oxy_identity_synced', 'false');
347
357
  setIdentitySynced(false);
348
358
 
349
- // Try to sync with server (will succeed if online)
350
- try {
351
- const { signature, timestamp } = await SignatureService.createRegistrationSignature();
352
- await oxyServices.register(publicKey, signature, timestamp);
353
-
354
- // Mark as synced (Zustand store + storage)
355
- await storage.setItem('oxy_identity_synced', 'true');
356
- setIdentitySynced(true);
357
-
358
- if (__DEV__ && logger) {
359
- logger('Identity synced with server successfully');
359
+ // If username provided, try to register immediately (online only)
360
+ if (username) {
361
+ // Validate username format before attempting registration
362
+ const trimmedUsername = username.trim();
363
+ if (trimmedUsername && /^[a-zA-Z0-9]{3,30}$/.test(trimmedUsername)) {
364
+ try {
365
+ const { signature, timestamp } = await SignatureService.createRegistrationSignature();
366
+ await oxyServices.register(publicKey, signature, timestamp, trimmedUsername);
367
+
368
+ // Mark as synced (Zustand store + storage)
369
+ await storage.setItem('oxy_identity_synced', 'true');
370
+ setIdentitySynced(true);
371
+
372
+ if (__DEV__ && logger) {
373
+ logger('Identity synced with server successfully with username');
374
+ }
375
+
376
+ return { synced: true };
377
+ } catch (syncError) {
378
+ // Offline or server error - identity created locally but not synced
379
+ if (__DEV__ && logger) {
380
+ logger('Identity created locally with username (offline), will sync when online', syncError);
381
+ }
382
+
383
+ return { synced: false };
384
+ }
385
+ } else {
386
+ // Invalid username format - log the issue
387
+ if (__DEV__ && logger) {
388
+ logger('Invalid username format, identity created without username', {
389
+ providedUsername: username.substring(0, 20),
390
+ });
391
+ }
360
392
  }
393
+ }
361
394
 
362
- return {
363
- synced: true,
364
- };
365
- } catch (syncError) {
366
- // Offline or server error - identity is created locally but not synced
367
- if (__DEV__ && logger) {
368
- logger('Identity created locally (offline), will sync when online', syncError);
369
- }
370
-
371
- return {
372
- synced: false,
373
- };
395
+ // No username provided or invalid format - defer registration until later
396
+ if (__DEV__ && logger) {
397
+ logger('Identity created locally without username, will register during sync');
374
398
  }
399
+
400
+ return { synced: false };
375
401
  } catch (error) {
376
402
  // CRITICAL: Never delete identity on error - it may have been successfully created
377
403
  // Only log the error and let the user recover using their backup file
@@ -387,7 +413,7 @@ export const useAuthOperations = ({
387
413
  await storage.setItem('oxy_identity_synced', 'false').catch(() => {});
388
414
  setIdentitySynced(false);
389
415
  if (__DEV__ && logger) {
390
- logger('Identity was created but sync failed - user can sync later using backup file');
416
+ logger('Identity was created but sync failed - user can sync later');
391
417
  }
392
418
  } else {
393
419
  // No identity exists - this was a generation failure, safe to clean up sync flag
@@ -423,11 +449,19 @@ export const useAuthOperations = ({
423
449
  }, [storage, setIdentitySynced]);
424
450
 
425
451
  /**
426
- * Sync local identity with server (call when online)
427
- * TanStack Query handles offline mutations automatically
452
+ * Sync local identity with server.
453
+ *
454
+ * Registers the identity with the server if not already registered.
455
+ * This function is idempotent - calling it multiple times is safe.
456
+ *
457
+ * TanStack Query handles offline mutations automatically, so this will
458
+ * retry automatically when connection is restored.
459
+ *
460
+ * @param username - Optional username to set during sync/registration
461
+ * @returns User object after successful sync and sign-in
428
462
  */
429
463
  const syncIdentity = useCallback(
430
- async (): Promise<User> => {
464
+ async (username?: string): Promise<User> => {
431
465
  if (!storage) throw new Error('Storage not initialized');
432
466
 
433
467
  setAuthState({ isLoading: true, error: null });
@@ -443,6 +477,7 @@ export const useAuthOperations = ({
443
477
  const alreadySynced = await storage.getItem('oxy_identity_synced');
444
478
  if (alreadySynced === 'true') {
445
479
  setIdentitySynced(true);
480
+ // Identity is already synced, just sign in
446
481
  return await performSignIn(publicKey);
447
482
  }
448
483
 
@@ -452,7 +487,7 @@ export const useAuthOperations = ({
452
487
  if (!registered) {
453
488
  // Register with server (identity is just the publicKey)
454
489
  const { signature, timestamp } = await SignatureService.createRegistrationSignature();
455
- await oxyServices.register(publicKey, signature, timestamp);
490
+ await oxyServices.register(publicKey, signature, timestamp, username);
456
491
  }
457
492
 
458
493
  // Mark as synced (Zustand store + storage)
@@ -462,13 +497,6 @@ export const useAuthOperations = ({
462
497
  // Sign in
463
498
  const user = await performSignIn(publicKey);
464
499
 
465
- // Check if user has username - required for syncing
466
- if (!user.username) {
467
- const usernameError = new Error('USERNAME_REQUIRED');
468
- (usernameError as any).code = 'USERNAME_REQUIRED';
469
- throw usernameError;
470
- }
471
-
472
500
  // TanStack Query will automatically retry any pending mutations
473
501
 
474
502
  return user;
@@ -491,10 +519,22 @@ export const useAuthOperations = ({
491
519
  );
492
520
 
493
521
  /**
494
- * Import identity from backup file data (offline-first)
522
+ * Import identity from encrypted backup file.
523
+ *
524
+ * Restores a previously created identity from an encrypted backup.
525
+ * The backup is decrypted using the provided password.
526
+ *
527
+ * IMPORTANT: This function clears session data only if importing a different
528
+ * identity than the one currently stored. Re-importing the same identity
529
+ * will NOT clear session data.
530
+ *
531
+ * @param backupData - The encrypted backup data object
532
+ * @param password - Password to decrypt the backup
533
+ * @param username - Optional username to set during registration if not yet registered
534
+ * @returns Object with synced status indicating if identity was registered with server
495
535
  */
496
536
  const importIdentity = useCallback(
497
- async (backupData: BackupData, password: string): Promise<{ synced: boolean }> => {
537
+ async (backupData: BackupData, password: string, username?: string): Promise<{ synced: boolean }> => {
498
538
  if (!storage) throw new Error('Storage not initialized');
499
539
 
500
540
  // Validate arguments - ensure backupData is an object, not a string (old signature)
@@ -513,8 +553,8 @@ export const useAuthOperations = ({
513
553
  setAuthState({ isLoading: true, error: null });
514
554
 
515
555
  try {
516
- // CRITICAL: Get old public key before importing new identity
517
- // If identity changes, we must clear all session data to prevent data leakage
556
+ // Get old public key before importing identity
557
+ // This is used to detect if identity actually changed
518
558
  const oldPublicKey = await KeyManager.getPublicKey().catch(() => null);
519
559
 
520
560
  if (__DEV__ && logger) {
@@ -569,10 +609,11 @@ export const useAuthOperations = ({
569
609
  throw new Error('Backup file is corrupted or password is incorrect');
570
610
  }
571
611
 
572
- // Clear sessions if identity changed (prevents data leakage)
612
+ // Only clear sessions if identity actually changed
613
+ // This prevents clearing sessions when re-importing the same identity
573
614
  await clearSessionsIfIdentityChanged(oldPublicKey, publicKey);
574
615
 
575
- // Mark as not synced
616
+ // Mark as not synced initially (will check server status below)
576
617
  await storage.setItem('oxy_identity_synced', 'false');
577
618
  setIdentitySynced(false);
578
619
 
@@ -582,21 +623,21 @@ export const useAuthOperations = ({
582
623
  const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
583
624
 
584
625
  if (registered) {
585
- // Identity exists, mark as synced
626
+ // Identity exists on server, mark as synced
586
627
  await storage.setItem('oxy_identity_synced', 'true');
587
628
  setIdentitySynced(true);
588
629
  return { synced: true };
589
630
  } else {
590
- // Need to register this identity (identity is just the publicKey)
631
+ // Need to register this identity
591
632
  const { signature, timestamp } = await SignatureService.createRegistrationSignature();
592
- await oxyServices.register(publicKey, signature, timestamp);
633
+ await oxyServices.register(publicKey, signature, timestamp, username);
593
634
 
594
635
  await storage.setItem('oxy_identity_synced', 'true');
595
636
  setIdentitySynced(true);
596
637
  return { synced: true };
597
638
  }
598
639
  } catch (syncError) {
599
- // Offline - identity restored locally but not synced
640
+ // Offline or server error - identity restored locally but not synced
600
641
  if (__DEV__) {
601
642
  console.log('[Auth] Identity imported locally, will sync when online:', syncError);
602
643
  }