@explorins/pers-sdk 2.1.33-alpha.0 → 2.1.33

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 (75) hide show
  1. package/dist/analytics/api/analytics-api.d.ts +42 -1
  2. package/dist/analytics/api/analytics-api.d.ts.map +1 -1
  3. package/dist/analytics/models/index.d.ts +2 -2
  4. package/dist/analytics/models/index.d.ts.map +1 -1
  5. package/dist/analytics/services/analytics-service.d.ts +8 -1
  6. package/dist/analytics/services/analytics-service.d.ts.map +1 -1
  7. package/dist/analytics.cjs +1 -1
  8. package/dist/analytics.js +1 -1
  9. package/dist/chunks/{analytics-service-vm7B7LhS.js → analytics-service-CF7hSwhy.js} +53 -1
  10. package/dist/chunks/analytics-service-CF7hSwhy.js.map +1 -0
  11. package/dist/chunks/{analytics-service-CF9AsMQH.cjs → analytics-service-CRs9cpkg.cjs} +53 -1
  12. package/dist/chunks/analytics-service-CRs9cpkg.cjs.map +1 -0
  13. package/dist/chunks/{index-CVuttuU8.cjs → index-B_n8XiMt.cjs} +36 -11
  14. package/dist/chunks/index-B_n8XiMt.cjs.map +1 -0
  15. package/dist/chunks/{index-8y63MFOX.js → index-CgbzGLYT.js} +35 -12
  16. package/dist/chunks/index-CgbzGLYT.js.map +1 -0
  17. package/dist/chunks/{pers-sdk-DAllRtm3.js → pers-sdk-CySmOdzr.js} +398 -45
  18. package/dist/chunks/pers-sdk-CySmOdzr.js.map +1 -0
  19. package/dist/chunks/{pers-sdk-CEAAI2U0.cjs → pers-sdk-DgsYyo1X.cjs} +399 -44
  20. package/dist/chunks/pers-sdk-DgsYyo1X.cjs.map +1 -0
  21. package/dist/chunks/{tenant-manager-D9ihQLhf.js → tenant-manager-BQP25Alv.js} +2 -2
  22. package/dist/chunks/{tenant-manager-D9ihQLhf.js.map → tenant-manager-BQP25Alv.js.map} +1 -1
  23. package/dist/chunks/{tenant-manager-BdJYwIgL.cjs → tenant-manager-DeEuSlk7.cjs} +2 -2
  24. package/dist/chunks/{tenant-manager-BdJYwIgL.cjs.map → tenant-manager-DeEuSlk7.cjs.map} +1 -1
  25. package/dist/chunks/{web3-chain-service-DRoykR1u.js → web3-chain-service-BLrjcBTb.js} +2 -2
  26. package/dist/chunks/{web3-chain-service-DRoykR1u.js.map → web3-chain-service-BLrjcBTb.js.map} +1 -1
  27. package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs → web3-chain-service-Bilgr4M8.cjs} +2 -2
  28. package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs.map → web3-chain-service-Bilgr4M8.cjs.map} +1 -1
  29. package/dist/chunks/{web3-manager-NMLZ3pu7.js → web3-manager-BSvr7wJ7.js} +4 -4
  30. package/dist/chunks/{web3-manager-NMLZ3pu7.js.map → web3-manager-BSvr7wJ7.js.map} +1 -1
  31. package/dist/chunks/{web3-manager-DKHJrBYE.cjs → web3-manager-DbfqhJF4.cjs} +4 -4
  32. package/dist/chunks/{web3-manager-DKHJrBYE.cjs.map → web3-manager-DbfqhJF4.cjs.map} +1 -1
  33. package/dist/core/auth/default-auth-provider.d.ts +31 -0
  34. package/dist/core/auth/default-auth-provider.d.ts.map +1 -1
  35. package/dist/core/auth/dpop/dpop-manager.d.ts +12 -0
  36. package/dist/core/auth/dpop/dpop-manager.d.ts.map +1 -1
  37. package/dist/core/auth/indexed-db-storage.d.ts.map +1 -1
  38. package/dist/core/auth/refresh-manager.d.ts +34 -2
  39. package/dist/core/auth/refresh-manager.d.ts.map +1 -1
  40. package/dist/core/auth/services/auth-service.d.ts +54 -2
  41. package/dist/core/auth/services/auth-service.d.ts.map +1 -1
  42. package/dist/core/auth/token-storage.d.ts +6 -0
  43. package/dist/core/auth/token-storage.d.ts.map +1 -1
  44. package/dist/core/pers-api-client.d.ts.map +1 -1
  45. package/dist/core/utils/jwt.function.d.ts +23 -0
  46. package/dist/core/utils/jwt.function.d.ts.map +1 -1
  47. package/dist/core.cjs +6 -4
  48. package/dist/core.cjs.map +1 -1
  49. package/dist/core.js +4 -4
  50. package/dist/index.cjs +6 -4
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.js +4 -4
  53. package/dist/managers/analytics-manager.d.ts +48 -1
  54. package/dist/managers/analytics-manager.d.ts.map +1 -1
  55. package/dist/node.cjs +4 -4
  56. package/dist/node.js +4 -4
  57. package/dist/package.json +4 -2
  58. package/dist/user/api/user-api.d.ts.map +1 -1
  59. package/dist/user.cjs +6 -11
  60. package/dist/user.cjs.map +1 -1
  61. package/dist/user.js +6 -11
  62. package/dist/user.js.map +1 -1
  63. package/dist/web3-chain.cjs +2 -2
  64. package/dist/web3-chain.js +2 -2
  65. package/dist/web3-manager.cjs +4 -4
  66. package/dist/web3-manager.js +4 -4
  67. package/dist/web3.cjs +4 -4
  68. package/dist/web3.js +4 -4
  69. package/package.json +4 -2
  70. package/dist/chunks/analytics-service-CF9AsMQH.cjs.map +0 -1
  71. package/dist/chunks/analytics-service-vm7B7LhS.js.map +0 -1
  72. package/dist/chunks/index-8y63MFOX.js.map +0 -1
  73. package/dist/chunks/index-CVuttuU8.cjs.map +0 -1
  74. package/dist/chunks/pers-sdk-CEAAI2U0.cjs.map +0 -1
  75. package/dist/chunks/pers-sdk-DAllRtm3.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { AccountOwnerType, MembershipRole, WebhookMethod, WebhookExecutionStatus } from '@explorins/pers-shared';
2
- import { i as isTokenExpired, E as ErrorUtils, A as AuthenticationError, b as PersApiError } from './index-8y63MFOX.js';
2
+ import { i as isTokenExpired, d as decodeJwtPayload, e as getTokenTimeToLive, E as ErrorUtils, A as AuthenticationError, b as PersApiError } from './index-CgbzGLYT.js';
3
3
  import { UserService, UserApi } from '../user.js';
4
4
  import { createUserStatusSDK } from '../user-status.js';
5
5
  import { a as TokenService, T as TokenApi } from './token-service-BxEO5YVN.js';
@@ -8,9 +8,9 @@ import { CampaignService, CampaignApi } from '../campaign.js';
8
8
  import { a as RedemptionService, R as RedemptionApi } from './redemption-service-czBfCP-3.js';
9
9
  import { a as TransactionService, T as TransactionApi } from './transaction-request.builder-BZ6Uq6Qe.js';
10
10
  import { a as PaymentService, P as PurchaseApi } from './payment-service-IvM6rryM.js';
11
- import { T as TenantManager } from './tenant-manager-D9ihQLhf.js';
11
+ import { T as TenantManager } from './tenant-manager-BQP25Alv.js';
12
12
  import { b as buildPaginationParams, n as normalizeToPaginated } from './pagination-utils-9vQ-Npkr.js';
13
- import { a as AnalyticsService, A as AnalyticsApi } from './analytics-service-vm7B7LhS.js';
13
+ import { a as AnalyticsService, A as AnalyticsApi } from './analytics-service-CF7hSwhy.js';
14
14
  import { DonationService, DonationApi } from '../donation.js';
15
15
  import { TriggerSourceService, TriggerSourceApi } from '../trigger-source.js';
16
16
 
@@ -237,6 +237,8 @@ function isExtendedProvider(provider) {
237
237
  * TOKEN_EXPIRED is NOT in this list - it's the normal "please refresh" case.
238
238
  *
239
239
  * Values validated against CommonErrorCodes from @explorins/pers-shared at compile time.
240
+ *
241
+ * SINGLE SOURCE OF TRUTH - use this constant everywhere, never duplicate these values.
240
242
  */
241
243
  const FATAL_AUTH_CODES = [
242
244
  'REFRESH_TOKEN_EXPIRED',
@@ -244,6 +246,13 @@ const FATAL_AUTH_CODES = [
244
246
  'INVALID_TOKEN',
245
247
  'TOKEN_REVOKED'
246
248
  ];
249
+ /**
250
+ * Check if an error message contains any fatal auth error code.
251
+ * Useful for detecting fatal errors in wrapped/stringified errors.
252
+ */
253
+ function isFatalAuthErrorInMessage(message) {
254
+ return FATAL_AUTH_CODES.some(code => message.includes(code));
255
+ }
247
256
  /**
248
257
  * Platform-agnostic authentication service
249
258
  * Handles login, token refresh, and storage operations
@@ -399,8 +408,53 @@ class AuthService {
399
408
  return FATAL_AUTH_CODES.includes(errorCode);
400
409
  }
401
410
  /**
402
- * Handle authentication failure with proper cleanup.
403
- * Called when backend returns fatal auth error codes or refresh fails.
411
+ * Comprehensive check if an error represents a DEFINITIVE auth failure.
412
+ * This is the SINGLE SOURCE OF TRUTH for determining if logout is required.
413
+ *
414
+ * Returns true for:
415
+ * - Known fatal error codes (REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc.)
416
+ * - Fatal error codes in error messages (wrapped errors)
417
+ * - 401/403 HTTP status codes
418
+ *
419
+ * Returns false for (retryable errors):
420
+ * - Network errors
421
+ * - Timeouts
422
+ * - 5xx server errors
423
+ * - TOKEN_EXPIRED (normal refresh trigger)
424
+ */
425
+ isDefinitiveAuthFailure(error) {
426
+ if (!error)
427
+ return false;
428
+ const errorAny = error;
429
+ // Check error code property against canonical list
430
+ if (errorAny?.code && this.isFatalAuthError(errorAny.code)) {
431
+ return true;
432
+ }
433
+ // Check if error message contains fatal keywords (fallback for wrapped errors)
434
+ const errorMessage = error instanceof Error ? error.message : String(error);
435
+ if (isFatalAuthErrorInMessage(errorMessage)) {
436
+ return true;
437
+ }
438
+ // Check HTTP status if available
439
+ const status = errorAny?.status || errorAny?.response?.status;
440
+ if (status === 401 || status === 403) {
441
+ return true;
442
+ }
443
+ // Everything else is considered retryable
444
+ return false;
445
+ }
446
+ /**
447
+ * Handle authentication failure with provider token recovery.
448
+ *
449
+ * Before logging out, attempts to recover the session using the stored
450
+ * provider token (Firebase JWT, etc.) if it's still valid.
451
+ *
452
+ * Recovery Flow:
453
+ * 1. Check if provider token exists and is not expired
454
+ * 2. Get the auth type (USER, BUSINESS, TENANT) to know which login to use
455
+ * 3. Attempt re-authentication with provider token
456
+ * 4. If successful: Session recovered, stay logged in
457
+ * 5. If failed: Clear tokens and emit AUTH_FAILED
404
458
  */
405
459
  async handleAuthFailure() {
406
460
  // Already failed - nothing to do
@@ -416,7 +470,7 @@ class AuthService {
416
470
  return await this.activeAuthFailurePromise;
417
471
  }
418
472
  // Create the failure promise to deduplicate concurrent calls
419
- this.activeAuthFailurePromise = this.performAuthFailure();
473
+ this.activeAuthFailurePromise = this.performAuthFailureWithRecovery();
420
474
  try {
421
475
  await this.activeAuthFailurePromise;
422
476
  }
@@ -424,6 +478,72 @@ class AuthService {
424
478
  this.activeAuthFailurePromise = null;
425
479
  }
426
480
  }
481
+ /**
482
+ * Attempts to recover session using provider token before failing.
483
+ */
484
+ async performAuthFailureWithRecovery() {
485
+ // Clear active refresh operations
486
+ this.activeRefreshPromise = null;
487
+ // Try to recover using provider token
488
+ const recovered = await this.attemptProviderTokenRecovery();
489
+ if (recovered) {
490
+ return; // Session recovered, don't fail
491
+ }
492
+ // Recovery failed - perform actual logout
493
+ await this.performAuthFailure();
494
+ }
495
+ /**
496
+ * Attempt to recover session using stored provider token.
497
+ * Extracts context (tenantId/businessId) from the current refresh token to ensure
498
+ * the recovered session maintains the same context.
499
+ *
500
+ * @returns true if session was successfully recovered
501
+ */
502
+ async attemptProviderTokenRecovery() {
503
+ const extended = this.extendedProvider;
504
+ if (!extended?.getProviderToken || !extended?.getAuthType) {
505
+ return false; // Provider doesn't support recovery
506
+ }
507
+ try {
508
+ const providerToken = await extended.getProviderToken();
509
+ if (!providerToken) {
510
+ return false;
511
+ }
512
+ // Check if provider token is still valid (with small margin)
513
+ if (isTokenExpired(providerToken, 30)) {
514
+ return false;
515
+ }
516
+ const authType = await extended.getAuthType();
517
+ if (!authType) {
518
+ return false;
519
+ }
520
+ // Extract context (tenantId/businessId) from the current refresh token
521
+ // This ensures multi-tenant/multi-business users recover to the same context
522
+ const refreshToken = await this.authProvider?.getRefreshToken?.();
523
+ const contextClaims = refreshToken ? decodeJwtPayload(refreshToken) : null;
524
+ // Re-authenticate using provider token based on auth type
525
+ // Pass the context from the previous session to avoid MULTIPLE_CONTEXT_SELECTION_REQUIRED
526
+ switch (authType) {
527
+ case AccountOwnerType.USER:
528
+ await this.loginUser(providerToken);
529
+ break;
530
+ case AccountOwnerType.BUSINESS:
531
+ await this.loginBusiness(providerToken, contextClaims?.businessId ? { businessId: contextClaims.businessId } : undefined);
532
+ break;
533
+ case AccountOwnerType.TENANT:
534
+ await this.loginTenantAdmin(providerToken, contextClaims?.tenantId ? { tenantId: contextClaims.tenantId } : undefined);
535
+ break;
536
+ default:
537
+ console.warn(`[AuthService] Unknown auth type for recovery: ${authType}`);
538
+ return false;
539
+ }
540
+ return true; // Recovery successful
541
+ }
542
+ catch (error) {
543
+ console.warn('[AuthService] Provider token recovery failed:', error);
544
+ return false;
545
+ }
546
+ }
427
547
  /**
428
548
  * Performs the actual auth failure cleanup
429
549
  */
@@ -503,16 +623,32 @@ class AuthService {
503
623
 
504
624
  /**
505
625
  * Token Refresh Manager
506
- * Simple container for token refresh logic with better organization
626
+ * Industry-standard token lifecycle management with:
627
+ * - Dynamic refresh margin based on token lifetime
628
+ * - Retry logic for transient failures
629
+ * - Only logout on definitive auth failures (not network errors)
507
630
  */
508
631
  class TokenRefreshManager {
509
632
  constructor(authService, authProvider) {
510
633
  this.authService = authService;
511
634
  this.authProvider = authProvider;
512
- this.tokenRefreshMarginSeconds = 120; // 2 minutes
635
+ // Industry standard: refresh when 25% of token lifetime remains
636
+ this.REFRESH_THRESHOLD_PERCENT = 0.25;
637
+ // Minimum margin regardless of token lifetime
638
+ this.MIN_REFRESH_MARGIN_SECONDS = 30;
639
+ // Maximum margin (cap for very long-lived tokens)
640
+ this.MAX_REFRESH_MARGIN_SECONDS = 300; // 5 minutes
641
+ // Validation cache to avoid redundant checks
513
642
  this.lastValidationTime = 0;
514
643
  this.validationCacheDurationMs = 30000; // 30 seconds
644
+ // Retry configuration for transient failures
645
+ this.MAX_RETRY_ATTEMPTS = 3;
646
+ this.RETRY_DELAY_MS = 1000;
515
647
  }
648
+ /**
649
+ * Ensures the access token is valid, refreshing if needed.
650
+ * Uses dynamic margin based on token lifetime (industry standard).
651
+ */
516
652
  async ensureValidToken() {
517
653
  const now = Date.now();
518
654
  // Skip validation if we checked recently (within cache duration)
@@ -524,35 +660,95 @@ class TokenRefreshManager {
524
660
  if (!token) {
525
661
  return;
526
662
  }
527
- if (isTokenExpired(token, this.tokenRefreshMarginSeconds)) {
528
- const refreshSuccess = await this.attemptInternalRefresh();
529
- if (!refreshSuccess) {
663
+ // Calculate dynamic refresh margin based on token lifetime
664
+ const marginSeconds = this.calculateRefreshMargin(token);
665
+ if (isTokenExpired(token, marginSeconds)) {
666
+ const result = await this.attemptRefreshWithRetry();
667
+ if (!result.success) {
668
+ if (result.retryable) {
669
+ // Transient error - don't logout, let next request retry
670
+ console.warn('[TokenRefreshManager] Refresh failed with retryable error, will retry on next request');
671
+ return;
672
+ }
673
+ // Definitive auth failure - logout required
530
674
  await this.authService.handleAuthFailure();
531
675
  }
532
676
  else {
533
- // Update validation time on successful refresh
677
+ // Refresh successful
534
678
  this.lastValidationTime = now;
535
679
  }
536
680
  }
537
681
  else {
538
- // Token is valid, update validation time
682
+ // Token is valid
539
683
  this.lastValidationTime = now;
540
684
  }
541
685
  }
542
686
  catch (error) {
543
- await this.authService.handleAuthFailure();
687
+ // Unexpected error - log but don't logout (conservative approach)
688
+ console.error('[TokenRefreshManager] Unexpected error during token validation:', error);
544
689
  }
545
690
  }
546
- async attemptInternalRefresh() {
547
- try {
548
- await this.authService.refreshAccessToken();
549
- return true;
691
+ /**
692
+ * Calculate refresh margin dynamically based on token lifetime.
693
+ * Industry standard: refresh when 25% of lifetime remains.
694
+ */
695
+ calculateRefreshMargin(token) {
696
+ const ttlSeconds = getTokenTimeToLive(token);
697
+ if (ttlSeconds === null || ttlSeconds <= 0) {
698
+ // Can't determine TTL, use minimum margin
699
+ return this.MIN_REFRESH_MARGIN_SECONDS;
550
700
  }
551
- catch (error) {
552
- // Don't call handleAuthFailure here - let the caller decide
553
- // This prevents duplicate auth failure handling in race conditions
554
- return false;
701
+ // Calculate 25% of remaining lifetime
702
+ const dynamicMargin = Math.floor(ttlSeconds * this.REFRESH_THRESHOLD_PERCENT);
703
+ // Clamp between min and max
704
+ return Math.max(this.MIN_REFRESH_MARGIN_SECONDS, Math.min(dynamicMargin, this.MAX_REFRESH_MARGIN_SECONDS));
705
+ }
706
+ /**
707
+ * Attempt token refresh with retry logic for transient failures.
708
+ * Only gives up on definitive auth errors.
709
+ */
710
+ async attemptRefreshWithRetry() {
711
+ let lastError;
712
+ for (let attempt = 1; attempt <= this.MAX_RETRY_ATTEMPTS; attempt++) {
713
+ try {
714
+ await this.authService.refreshAccessToken();
715
+ return { success: true, retryable: false };
716
+ }
717
+ catch (error) {
718
+ lastError = error instanceof Error ? error : new Error(String(error));
719
+ // Check if this is a definitive auth failure (non-retryable)
720
+ if (this.isDefinitiveAuthFailure(error)) {
721
+ console.warn(`[TokenRefreshManager] Definitive auth failure: ${lastError.message}`);
722
+ return { success: false, retryable: false, error: lastError };
723
+ }
724
+ // Retryable error - wait and try again
725
+ if (attempt < this.MAX_RETRY_ATTEMPTS) {
726
+ await this.delay(this.RETRY_DELAY_MS * attempt); // Exponential backoff
727
+ }
728
+ }
555
729
  }
730
+ // All retries exhausted - return as retryable so we don't logout
731
+ console.warn(`[TokenRefreshManager] All ${this.MAX_RETRY_ATTEMPTS} refresh attempts failed`);
732
+ return { success: false, retryable: true, error: lastError };
733
+ }
734
+ /**
735
+ * Determine if an error is a definitive auth failure that requires logout.
736
+ * Network errors, timeouts, 5xx errors are NOT definitive - they're retryable.
737
+ *
738
+ * Delegates to AuthService.isDefinitiveAuthFailure() - the SINGLE SOURCE OF TRUTH.
739
+ */
740
+ isDefinitiveAuthFailure(error) {
741
+ return this.authService.isDefinitiveAuthFailure(error);
742
+ }
743
+ delay(ms) {
744
+ return new Promise(resolve => setTimeout(resolve, ms));
745
+ }
746
+ /**
747
+ * @deprecated Use ensureValidToken() instead
748
+ */
749
+ async attemptInternalRefresh() {
750
+ const result = await this.attemptRefreshWithRetry();
751
+ return result.success;
556
752
  }
557
753
  }
558
754
 
@@ -700,6 +896,22 @@ class AuthTokenManager {
700
896
  this.cache = {};
701
897
  await this.storage.clear();
702
898
  }
899
+ /**
900
+ * Clears only auth tokens (access, refresh, provider, authType), preserving DPoP keys.
901
+ * Use this when DPoP keys are regenerated - we want to invalidate auth tokens
902
+ * that were bound to old keys, but keep the newly generated DPoP keys.
903
+ */
904
+ async clearAuthTokens() {
905
+ // Clear cache
906
+ this.cache = {};
907
+ // Remove all auth tokens and auth type, NOT DPoP keys
908
+ await Promise.all([
909
+ this.storage.remove(AUTH_STORAGE_KEYS.ACCESS_TOKEN),
910
+ this.storage.remove(AUTH_STORAGE_KEYS.REFRESH_TOKEN),
911
+ this.storage.remove(AUTH_STORAGE_KEYS.PROVIDER_TOKEN),
912
+ this.storage.remove(AUTH_STORAGE_KEYS.AUTH_TYPE),
913
+ ]);
914
+ }
703
915
  async hasAccessToken() {
704
916
  // Use cached value if available to avoid storage read
705
917
  if (this.cache.accessToken !== undefined) {
@@ -730,13 +942,12 @@ class IndexedDBTokenStorage {
730
942
  constructor() {
731
943
  this.supportsObjects = true;
732
944
  this.dbPromise = null;
733
- if (typeof indexedDB === 'undefined') {
734
- console.warn('IndexedDB is not available in this environment');
735
- }
945
+ // IndexedDB availability is checked lazily on first operation
736
946
  }
737
947
  getDB() {
738
- if (this.dbPromise)
948
+ if (this.dbPromise) {
739
949
  return this.dbPromise;
950
+ }
740
951
  this.dbPromise = new Promise((resolve, reject) => {
741
952
  if (typeof indexedDB === 'undefined') {
742
953
  return reject(new Error('IndexedDB not supported'));
@@ -764,8 +975,12 @@ class IndexedDBTokenStorage {
764
975
  const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readonly');
765
976
  const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
766
977
  const request = store.get(key);
767
- request.onsuccess = () => resolve(request.result || null);
768
- request.onerror = () => reject(request.error);
978
+ request.onsuccess = () => {
979
+ resolve(request.result || null);
980
+ };
981
+ request.onerror = () => {
982
+ reject(request.error);
983
+ };
769
984
  });
770
985
  }
771
986
  catch (e) {
@@ -961,6 +1176,8 @@ class DPoPManager {
961
1176
  constructor(storage, cryptoProvider, callbacks) {
962
1177
  this.storage = storage;
963
1178
  this.memoryKeyPair = null;
1179
+ /** Pending key pair promise to prevent concurrent generation race conditions */
1180
+ this.pendingKeyPair = null;
964
1181
  this.cryptoProvider = cryptoProvider || new WebDPoPCryptoProvider();
965
1182
  this.callbacks = callbacks || {};
966
1183
  }
@@ -969,8 +1186,9 @@ class DPoPManager {
969
1186
  * Useful for detecting if a key regeneration would be needed.
970
1187
  */
971
1188
  async hasStoredKeys() {
972
- if (this.memoryKeyPair)
1189
+ if (this.memoryKeyPair) {
973
1190
  return true;
1191
+ }
974
1192
  const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
975
1193
  const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
976
1194
  return !!(storedPublic && storedPrivate);
@@ -984,8 +1202,35 @@ class DPoPManager {
984
1202
  * match refresh token binding" errors.
985
1203
  */
986
1204
  async ensureKeyPair() {
987
- if (this.memoryKeyPair)
1205
+ // Fast path: already have keys in memory
1206
+ if (this.memoryKeyPair) {
1207
+ return this.memoryKeyPair;
1208
+ }
1209
+ // Prevent concurrent key generation race condition:
1210
+ // If another call is already generating keys, wait for it instead of starting a new one
1211
+ if (this.pendingKeyPair) {
1212
+ return this.pendingKeyPair;
1213
+ }
1214
+ // Create a promise that subsequent callers will wait on
1215
+ this.pendingKeyPair = this.loadOrGenerateKeyPair();
1216
+ try {
1217
+ const keyPair = await this.pendingKeyPair;
1218
+ return keyPair;
1219
+ }
1220
+ finally {
1221
+ // Clear pending promise so future calls can proceed
1222
+ this.pendingKeyPair = null;
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Internal method that actually loads or generates keys.
1227
+ * Called only once per concurrent batch of ensureKeyPair() calls.
1228
+ */
1229
+ async loadOrGenerateKeyPair() {
1230
+ // Double-check in case keys were set while waiting
1231
+ if (this.memoryKeyPair) {
988
1232
  return this.memoryKeyPair;
1233
+ }
989
1234
  const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
990
1235
  const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
991
1236
  if (storedPublic && storedPrivate) {
@@ -997,18 +1242,23 @@ class DPoPManager {
997
1242
  return this.memoryKeyPair;
998
1243
  }
999
1244
  catch (e) {
1000
- console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating.');
1245
+ console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating');
1001
1246
  }
1002
1247
  }
1003
1248
  // Generate new key pair
1004
1249
  // IMPORTANT: This invalidates any existing refresh tokens bound to the old keys
1005
- console.info('[DPoPManager] Generating new DPoP key pair');
1006
- // Adaptation: If storage supports raw objects (like IndexedDB or Native Keychain),
1250
+ // Adaptation: If storage supports raw objects (like Native Keychain on iOS/Android),
1007
1251
  // we can generate Non-Extractable keys for maximum security.
1008
- // If storage is text-only (LocalStorage), we must use Extractable keys to serialize them.
1252
+ // If storage is text-only (LocalStorage) OR IndexedDB (CryptoKey doesn't survive reload),
1253
+ // we must use Extractable keys to serialize them as JWK.
1254
+ //
1255
+ // NOTE: IndexedDB CAN store CryptoKey objects, but they don't survive page reloads -
1256
+ // they come back as empty/corrupted objects. Only native secure storage can persist
1257
+ // non-extractable keys. Frontend should set supportsObjects: false for web storage.
1009
1258
  const useHighSecurity = !!this.storage.supportsObjects;
1259
+ const extractable = !useHighSecurity;
1010
1260
  const keyPair = await this.cryptoProvider.generateKeyPair({
1011
- extractable: !useHighSecurity
1261
+ extractable
1012
1262
  });
1013
1263
  // Save to storage
1014
1264
  await this.storage.set(DPOP_STORAGE_KEYS.PUBLIC, keyPair.publicKey);
@@ -1042,6 +1292,21 @@ class DPoPManager {
1042
1292
  }
1043
1293
  return this.cryptoProvider.signProof(payload, keyPair);
1044
1294
  }
1295
+ /**
1296
+ * Generate a short fingerprint of the public key for debugging
1297
+ * This helps identify if the same key is being used across operations
1298
+ */
1299
+ getPublicKeyFingerprint(publicKey) {
1300
+ try {
1301
+ // Use the 'x' coordinate of the EC key as a fingerprint
1302
+ const x = publicKey.x || '';
1303
+ const y = publicKey.y || '';
1304
+ return `${x.substring(0, 8)}...${y.substring(0, 4)}`;
1305
+ }
1306
+ catch {
1307
+ return 'unknown';
1308
+ }
1309
+ }
1045
1310
  /**
1046
1311
  * Clears DPoP keys (e.g. on logout)
1047
1312
  */
@@ -1090,13 +1355,15 @@ class DefaultAuthProvider {
1090
1355
  this.tokenManager = new AuthTokenManager(storage);
1091
1356
  if (this.dpopEnabled) {
1092
1357
  this.dpopManager = new DPoPManager(storage, config.dpop?.cryptoProvider, {
1093
- // When DPoP keys are regenerated (corrupted/missing), the refresh token
1094
- // bound to the old keys becomes invalid. Clear it to prevent
1095
- // "DPoP proof does not match refresh token binding" errors.
1358
+ // When DPoP keys are regenerated (corrupted/missing), auth tokens
1359
+ // bound to the old keys become invalid. Clear only auth tokens, NOT DPoP keys.
1360
+ // Access token has cnf claim binding it to the old public key.
1361
+ // Refresh token has DPoP binding to the old key pair.
1362
+ // NOTE: Do NOT call clearAllTokens() here - it would wipe the freshly saved DPoP keys!
1096
1363
  onKeysRegenerated: () => {
1097
- console.info('[DefaultAuthProvider] DPoP keys regenerated, clearing invalid refresh token');
1098
- this.tokenManager.clearRefreshToken().catch(err => {
1099
- console.warn('[DefaultAuthProvider] Failed to clear refresh token:', err);
1364
+ // Clear only auth tokens, preserve DPoP keys (they were just regenerated and saved)
1365
+ this.tokenManager.clearAuthTokens().catch(err => {
1366
+ console.warn('[DefaultAuthProvider] Failed to clear tokens after key regeneration:', err);
1100
1367
  });
1101
1368
  }
1102
1369
  });
@@ -1238,6 +1505,41 @@ class DefaultAuthProvider {
1238
1505
  return null;
1239
1506
  }
1240
1507
  }
1508
+ /**
1509
+ * Store the provider token (Firebase JWT, Auth0 token, etc.)
1510
+ * Used for session recovery when refresh tokens expire.
1511
+ *
1512
+ * NOTE: This is called AUTOMATICALLY by loginWithToken(), so you typically
1513
+ * don't need to call this manually. The only reason to call this is if:
1514
+ *
1515
+ * 1. Your auth provider (Firebase, Auth0) refreshes tokens in the background
1516
+ * 2. You want to proactively update the stored token for better recovery chances
1517
+ *
1518
+ * The stored provider token is used as a fallback when:
1519
+ * - PERS refresh token expires (e.g., after 30 days of inactivity)
1520
+ * - SDK attempts recovery using the stored provider token
1521
+ * - If provider token is still valid → session recovered automatically
1522
+ * - If provider token is also expired → user must re-authenticate
1523
+ *
1524
+ * @example
1525
+ * // Optional: Update stored token when Firebase refreshes it
1526
+ * firebase.auth().onIdTokenChanged(async (user) => {
1527
+ * if (user) {
1528
+ * const freshToken = await user.getIdToken();
1529
+ * await sdk.auth.setProviderToken(freshToken);
1530
+ * }
1531
+ * });
1532
+ */
1533
+ async setProviderToken(token) {
1534
+ await this.tokenManager.setProviderToken(token);
1535
+ }
1536
+ /**
1537
+ * Get the stored provider token (Firebase JWT, Auth0 token, etc.)
1538
+ * Used internally for session recovery when refresh tokens expire.
1539
+ */
1540
+ async getProviderToken() {
1541
+ return this.tokenManager.getProviderToken();
1542
+ }
1241
1543
  }
1242
1544
 
1243
1545
  /**
@@ -1253,7 +1555,7 @@ class DefaultAuthProvider {
1253
1555
  /** SDK package name */
1254
1556
  const SDK_NAME = "@explorins/pers-sdk";
1255
1557
  /** SDK version - injected from package.json at build time */
1256
- const SDK_VERSION = "2.1.33-alpha.0";
1558
+ const SDK_VERSION = "2.1.33";
1257
1559
  /** Full SDK identifier for headers */
1258
1560
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
1259
1561
 
@@ -1402,7 +1704,9 @@ class PersApiClient {
1402
1704
  // Handle 401 errors centrally through AuthService
1403
1705
  if (status === 401) {
1404
1706
  const backendError = ErrorUtils.extractBackendErrorDetails(error);
1405
- // Check for fatal auth errors that require immediate logout
1707
+ // Check for FATAL auth error CODES (not just 401 status)
1708
+ // Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc. → immediate logout
1709
+ // Non-fatal codes: TOKEN_EXPIRED → try refresh first
1406
1710
  if (this.authService.isFatalAuthError(backendError.code)) {
1407
1711
  // Definitive auth failure - no retry, session is invalid
1408
1712
  await this.authService.handleAuthFailure();
@@ -7529,6 +7833,55 @@ class AnalyticsManager {
7529
7833
  async getCampaignClaimAnalytics(request) {
7530
7834
  return this.analyticsService.getCampaignClaimAnalytics(request);
7531
7835
  }
7836
+ /**
7837
+ * Get redemption redeem analytics with aggregation
7838
+ *
7839
+ * Retrieves aggregated redemption redeem data with flexible grouping and metrics.
7840
+ * Perfect for charts, dashboards, and redeem pattern analysis across redemptions,
7841
+ * businesses, and time periods.
7842
+ *
7843
+ * @param request - Analytics request with filters, groupBy, and metrics
7844
+ * @returns Promise resolving to redemption redeem analytics data
7845
+ *
7846
+ * @example Redeems per redemption
7847
+ * ```typescript
7848
+ * const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
7849
+ * groupBy: ['redemptionId'],
7850
+ * metrics: ['count'],
7851
+ * sortBy: 'count',
7852
+ * sortOrder: 'DESC',
7853
+ * limit: 10
7854
+ * });
7855
+ *
7856
+ * console.log('Top redemptions by redeems:', analytics.results);
7857
+ * ```
7858
+ *
7859
+ * @example Redeems by status over time
7860
+ * ```typescript
7861
+ * const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
7862
+ * groupBy: ['month', 'status'],
7863
+ * metrics: ['count'],
7864
+ * sortBy: 'month',
7865
+ * sortOrder: 'DESC',
7866
+ * startDate: new Date('2026-01-01'),
7867
+ * endDate: new Date('2026-12-31')
7868
+ * });
7869
+ * ```
7870
+ *
7871
+ * @example Redeems by business
7872
+ * ```typescript
7873
+ * const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
7874
+ * filters: { status: 'COMPLETED' },
7875
+ * groupBy: ['businessId'],
7876
+ * metrics: ['count'],
7877
+ * sortBy: 'count',
7878
+ * sortOrder: 'DESC'
7879
+ * });
7880
+ * ```
7881
+ */
7882
+ async getRedemptionRedeemAnalytics(request) {
7883
+ return this.analyticsService.getRedemptionRedeemAnalytics(request);
7884
+ }
7532
7885
  /**
7533
7886
  * Get user analytics with engagement metrics
7534
7887
  *
@@ -10310,5 +10663,5 @@ function createPersSDK(httpClient, config) {
10310
10663
  return new PersSDK(httpClient, config);
10311
10664
  }
10312
10665
 
10313
- export { AuthStatus as A, BusinessManager as B, CampaignManager as C, DefaultAuthProvider as D, FileApi as E, FileManager as F, FileService as G, ApiKeyApi as H, IndexedDBTokenStorage as I, WebhookApi as J, WebhookService as K, LocalStorageTokenStorage as L, MemoryTokenStorage as M, PersEventsClient as N, createPersEventsClient as O, PersSDK as P, RedemptionManager as R, SDK_NAME as S, TokenManager as T, UserManager as U, WebDPoPCryptoProvider as W, AuthTokenManager as a, AUTH_STORAGE_KEYS as b, createPersSDK as c, DPOP_STORAGE_KEYS as d, SDK_VERSION as e, SDK_USER_AGENT as f, PersApiClient as g, DEFAULT_PERS_CONFIG as h, buildApiRoot as i, buildWalletEventsWsUrl as j, StaticJwtAuthProvider as k, AuthApi as l, mergeWithDefaults as m, AuthService as n, DPoPManager as o, PersEventEmitter as p, AuthManager as q, UserStatusManager as r, TransactionManager as s, PurchaseManager as t, ApiKeyManager as u, AnalyticsManager as v, DonationManager as w, TriggerSourceManager as x, WebhookManager as y, WalletEventsManager as z };
10314
- //# sourceMappingURL=pers-sdk-DAllRtm3.js.map
10666
+ export { AuthStatus as A, BusinessManager as B, CampaignManager as C, DefaultAuthProvider as D, WebhookManager as E, FATAL_AUTH_CODES as F, WalletEventsManager as G, FileApi as H, IndexedDBTokenStorage as I, FileService as J, ApiKeyApi as K, LocalStorageTokenStorage as L, MemoryTokenStorage as M, WebhookApi as N, WebhookService as O, PersSDK as P, PersEventsClient as Q, RedemptionManager as R, SDK_NAME as S, TokenManager as T, UserManager as U, createPersEventsClient as V, WebDPoPCryptoProvider as W, AuthTokenManager as a, AUTH_STORAGE_KEYS as b, createPersSDK as c, DPOP_STORAGE_KEYS as d, SDK_VERSION as e, SDK_USER_AGENT as f, PersApiClient as g, DEFAULT_PERS_CONFIG as h, buildApiRoot as i, buildWalletEventsWsUrl as j, StaticJwtAuthProvider as k, AuthApi as l, mergeWithDefaults as m, isFatalAuthErrorInMessage as n, AuthService as o, DPoPManager as p, PersEventEmitter as q, AuthManager as r, UserStatusManager as s, TransactionManager as t, PurchaseManager as u, FileManager as v, ApiKeyManager as w, AnalyticsManager as x, DonationManager as y, TriggerSourceManager as z };
10667
+ //# sourceMappingURL=pers-sdk-CySmOdzr.js.map