@explorins/pers-sdk 2.1.32 → 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.
- package/dist/analytics/api/analytics-api.d.ts +42 -1
- package/dist/analytics/api/analytics-api.d.ts.map +1 -1
- package/dist/analytics/models/index.d.ts +2 -2
- package/dist/analytics/models/index.d.ts.map +1 -1
- package/dist/analytics/services/analytics-service.d.ts +8 -1
- package/dist/analytics/services/analytics-service.d.ts.map +1 -1
- package/dist/analytics.cjs +1 -1
- package/dist/analytics.js +1 -1
- package/dist/chunks/{analytics-service-vm7B7LhS.js → analytics-service-CF7hSwhy.js} +53 -1
- package/dist/chunks/analytics-service-CF7hSwhy.js.map +1 -0
- package/dist/chunks/{analytics-service-CF9AsMQH.cjs → analytics-service-CRs9cpkg.cjs} +53 -1
- package/dist/chunks/analytics-service-CRs9cpkg.cjs.map +1 -0
- package/dist/chunks/{index-CVuttuU8.cjs → index-B_n8XiMt.cjs} +36 -11
- package/dist/chunks/index-B_n8XiMt.cjs.map +1 -0
- package/dist/chunks/{index-8y63MFOX.js → index-CgbzGLYT.js} +35 -12
- package/dist/chunks/index-CgbzGLYT.js.map +1 -0
- package/dist/chunks/{pers-sdk-DLgR_7OQ.js → pers-sdk-CySmOdzr.js} +398 -45
- package/dist/chunks/pers-sdk-CySmOdzr.js.map +1 -0
- package/dist/chunks/{pers-sdk-CDj1sZaP.cjs → pers-sdk-DgsYyo1X.cjs} +399 -44
- package/dist/chunks/pers-sdk-DgsYyo1X.cjs.map +1 -0
- package/dist/chunks/{tenant-manager-D9ihQLhf.js → tenant-manager-BQP25Alv.js} +2 -2
- package/dist/chunks/{tenant-manager-D9ihQLhf.js.map → tenant-manager-BQP25Alv.js.map} +1 -1
- package/dist/chunks/{tenant-manager-BdJYwIgL.cjs → tenant-manager-DeEuSlk7.cjs} +2 -2
- package/dist/chunks/{tenant-manager-BdJYwIgL.cjs.map → tenant-manager-DeEuSlk7.cjs.map} +1 -1
- package/dist/chunks/{web3-chain-service-DRoykR1u.js → web3-chain-service-BLrjcBTb.js} +2 -2
- package/dist/chunks/{web3-chain-service-DRoykR1u.js.map → web3-chain-service-BLrjcBTb.js.map} +1 -1
- package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs → web3-chain-service-Bilgr4M8.cjs} +2 -2
- package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs.map → web3-chain-service-Bilgr4M8.cjs.map} +1 -1
- package/dist/chunks/{web3-manager-NMLZ3pu7.js → web3-manager-BSvr7wJ7.js} +4 -4
- package/dist/chunks/{web3-manager-NMLZ3pu7.js.map → web3-manager-BSvr7wJ7.js.map} +1 -1
- package/dist/chunks/{web3-manager-DKHJrBYE.cjs → web3-manager-DbfqhJF4.cjs} +4 -4
- package/dist/chunks/{web3-manager-DKHJrBYE.cjs.map → web3-manager-DbfqhJF4.cjs.map} +1 -1
- package/dist/core/auth/default-auth-provider.d.ts +31 -0
- package/dist/core/auth/default-auth-provider.d.ts.map +1 -1
- package/dist/core/auth/dpop/dpop-manager.d.ts +12 -0
- package/dist/core/auth/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/core/auth/indexed-db-storage.d.ts.map +1 -1
- package/dist/core/auth/refresh-manager.d.ts +34 -2
- package/dist/core/auth/refresh-manager.d.ts.map +1 -1
- package/dist/core/auth/services/auth-service.d.ts +54 -2
- package/dist/core/auth/services/auth-service.d.ts.map +1 -1
- package/dist/core/auth/token-storage.d.ts +6 -0
- package/dist/core/auth/token-storage.d.ts.map +1 -1
- package/dist/core/pers-api-client.d.ts.map +1 -1
- package/dist/core/utils/jwt.function.d.ts +23 -0
- package/dist/core/utils/jwt.function.d.ts.map +1 -1
- package/dist/core.cjs +6 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +4 -4
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/managers/analytics-manager.d.ts +48 -1
- package/dist/managers/analytics-manager.d.ts.map +1 -1
- package/dist/node.cjs +4 -4
- package/dist/node.js +4 -4
- package/dist/package.json +5 -2
- package/dist/web3-chain.cjs +2 -2
- package/dist/web3-chain.js +2 -2
- package/dist/web3-manager.cjs +4 -4
- package/dist/web3-manager.js +4 -4
- package/dist/web3.cjs +4 -4
- package/dist/web3.js +4 -4
- package/package.json +5 -2
- package/dist/chunks/analytics-service-CF9AsMQH.cjs.map +0 -1
- package/dist/chunks/analytics-service-vm7B7LhS.js.map +0 -1
- package/dist/chunks/index-8y63MFOX.js.map +0 -1
- package/dist/chunks/index-CVuttuU8.cjs.map +0 -1
- package/dist/chunks/pers-sdk-CDj1sZaP.cjs.map +0 -1
- package/dist/chunks/pers-sdk-DLgR_7OQ.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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
403
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
//
|
|
677
|
+
// Refresh successful
|
|
534
678
|
this.lastValidationTime = now;
|
|
535
679
|
}
|
|
536
680
|
}
|
|
537
681
|
else {
|
|
538
|
-
// Token is valid
|
|
682
|
+
// Token is valid
|
|
539
683
|
this.lastValidationTime = now;
|
|
540
684
|
}
|
|
541
685
|
}
|
|
542
686
|
catch (error) {
|
|
543
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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),
|
|
1094
|
-
// bound to the old keys
|
|
1095
|
-
//
|
|
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
|
-
|
|
1098
|
-
this.tokenManager.
|
|
1099
|
-
console.warn('[DefaultAuthProvider] Failed to clear
|
|
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.
|
|
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
|
|
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,
|
|
10314
|
-
//# sourceMappingURL=pers-sdk-
|
|
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
|