@explorins/pers-sdk 2.1.33-alpha.0 → 2.1.37
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/README.md +7 -0
- 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/business/api/business-api.d.ts +6 -3
- package/dist/business/api/business-api.d.ts.map +1 -1
- package/dist/business/services/business-service.d.ts +2 -2
- package/dist/business/services/business-service.d.ts.map +1 -1
- package/dist/business.cjs +1 -1
- package/dist/business.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/{base-token-service-BiSXWqOy.js → base-token-service-BGuuZX4b.js} +2 -2
- package/dist/chunks/{base-token-service-BiSXWqOy.js.map → base-token-service-BGuuZX4b.js.map} +1 -1
- package/dist/chunks/{base-token-service-CReAW_nz.cjs → base-token-service-DSye0WD2.cjs} +2 -2
- package/dist/chunks/{base-token-service-CReAW_nz.cjs.map → base-token-service-DSye0WD2.cjs.map} +1 -1
- package/dist/chunks/{business-membership-service-DXLG5fP9.cjs → business-membership-service-B9ItWZ2_.cjs} +18 -16
- package/dist/chunks/business-membership-service-B9ItWZ2_.cjs.map +1 -0
- package/dist/chunks/{business-membership-service-IyY5CkL0.js → business-membership-service-CPcE-AW0.js} +18 -16
- package/dist/chunks/business-membership-service-CPcE-AW0.js.map +1 -0
- package/dist/chunks/{index-8y63MFOX.js → index--OssIds0.js} +48 -22
- package/dist/chunks/index--OssIds0.js.map +1 -0
- package/dist/chunks/{index-CVuttuU8.cjs → index-C4K-jkRO.cjs} +48 -20
- package/dist/chunks/index-C4K-jkRO.cjs.map +1 -0
- package/dist/chunks/{pers-sdk-DAllRtm3.js → pers-sdk-Dds2lB27.js} +560 -51
- package/dist/chunks/pers-sdk-Dds2lB27.js.map +1 -0
- package/dist/chunks/{pers-sdk-CEAAI2U0.cjs → pers-sdk-DemghJ3a.cjs} +561 -50
- package/dist/chunks/pers-sdk-DemghJ3a.cjs.map +1 -0
- package/dist/chunks/{redemption-service-czBfCP-3.js → redemption-service-DWhZgrZT.js} +9 -1
- package/dist/chunks/{redemption-service-czBfCP-3.js.map → redemption-service-DWhZgrZT.js.map} +1 -1
- package/dist/chunks/{redemption-service-CVD2PzBO.cjs → redemption-service-Dc_0Kzd0.cjs} +9 -1
- package/dist/chunks/{redemption-service-CVD2PzBO.cjs.map → redemption-service-Dc_0Kzd0.cjs.map} +1 -1
- package/dist/chunks/{tenant-manager-BdJYwIgL.cjs → tenant-manager-DR5eSEJw.cjs} +2 -2
- package/dist/chunks/{tenant-manager-BdJYwIgL.cjs.map → tenant-manager-DR5eSEJw.cjs.map} +1 -1
- package/dist/chunks/{tenant-manager-D9ihQLhf.js → tenant-manager-xmYKBFGu.js} +2 -2
- package/dist/chunks/{tenant-manager-D9ihQLhf.js.map → tenant-manager-xmYKBFGu.js.map} +1 -1
- package/dist/chunks/{token-service-C1xe11OX.cjs → token-service-BJqu5Xap.cjs} +88 -2
- package/dist/chunks/token-service-BJqu5Xap.cjs.map +1 -0
- package/dist/chunks/{token-service-BxEO5YVN.js → token-service-qRSYG9uT.js} +88 -2
- package/dist/chunks/token-service-qRSYG9uT.js.map +1 -0
- package/dist/chunks/{web3-chain-service-DRoykR1u.js → web3-chain-service-BWRmwmmJ.js} +2 -2
- package/dist/chunks/{web3-chain-service-DRoykR1u.js.map → web3-chain-service-BWRmwmmJ.js.map} +1 -1
- package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs → web3-chain-service-D6tBL0F7.cjs} +2 -2
- package/dist/chunks/{web3-chain-service-CSxlvjMg.cjs.map → web3-chain-service-D6tBL0F7.cjs.map} +1 -1
- package/dist/chunks/{web3-manager-DKHJrBYE.cjs → web3-manager-C-JflQ86.cjs} +4 -4
- package/dist/chunks/{web3-manager-DKHJrBYE.cjs.map → web3-manager-C-JflQ86.cjs.map} +1 -1
- package/dist/chunks/{web3-manager-NMLZ3pu7.js → web3-manager-Dvcq4xmn.js} +4 -4
- package/dist/chunks/{web3-manager-NMLZ3pu7.js.map → web3-manager-Dvcq4xmn.js.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 +11 -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 +58 -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/errors/index.d.ts +6 -6
- package/dist/core/errors/index.d.ts.map +1 -1
- package/dist/core/events/event-types.d.ts +5 -1
- package/dist/core/events/event-types.d.ts.map +1 -1
- package/dist/core/pers-api-client.d.ts.map +1 -1
- package/dist/core/utils/jwt.function.d.ts +14 -0
- package/dist/core/utils/jwt.function.d.ts.map +1 -1
- package/dist/core.cjs +9 -7
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +7 -7
- package/dist/index.cjs +10 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/managers/analytics-manager.d.ts +48 -1
- package/dist/managers/analytics-manager.d.ts.map +1 -1
- package/dist/managers/redemption-manager.d.ts +45 -1
- package/dist/managers/redemption-manager.d.ts.map +1 -1
- package/dist/managers/token-manager.d.ts +97 -2
- package/dist/managers/token-manager.d.ts.map +1 -1
- package/dist/node.cjs +7 -7
- package/dist/node.js +7 -7
- package/dist/package.json +4 -2
- package/dist/redemption/services/redemption-service.d.ts +6 -0
- package/dist/redemption/services/redemption-service.d.ts.map +1 -1
- package/dist/redemption.cjs +1 -1
- package/dist/redemption.js +1 -1
- package/dist/token/api/token-api.d.ts +41 -1
- package/dist/token/api/token-api.d.ts.map +1 -1
- package/dist/token/index.d.ts +1 -1
- package/dist/token/index.d.ts.map +1 -1
- package/dist/token/services/token-service.d.ts +30 -1
- package/dist/token/services/token-service.d.ts.map +1 -1
- package/dist/token.cjs +2 -2
- package/dist/token.js +2 -2
- package/dist/user/api/user-api.d.ts.map +1 -1
- package/dist/user.cjs +6 -11
- package/dist/user.cjs.map +1 -1
- package/dist/user.js +6 -11
- package/dist/user.js.map +1 -1
- 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 +4 -2
- package/dist/chunks/analytics-service-CF9AsMQH.cjs.map +0 -1
- package/dist/chunks/analytics-service-vm7B7LhS.js.map +0 -1
- package/dist/chunks/business-membership-service-DXLG5fP9.cjs.map +0 -1
- package/dist/chunks/business-membership-service-IyY5CkL0.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-CEAAI2U0.cjs.map +0 -1
- package/dist/chunks/pers-sdk-DAllRtm3.js.map +0 -1
- package/dist/chunks/token-service-BxEO5YVN.js.map +0 -1
- package/dist/chunks/token-service-C1xe11OX.cjs.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
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--OssIds0.js';
|
|
3
3
|
import { UserService, UserApi } from '../user.js';
|
|
4
4
|
import { createUserStatusSDK } from '../user-status.js';
|
|
5
|
-
import { a as TokenService, T as TokenApi } from './token-service-
|
|
6
|
-
import { B as BusinessApi, b as BusinessService, a as BusinessMembershipApi, c as BusinessMembershipService } from './business-membership-service-
|
|
5
|
+
import { a as TokenService, T as TokenApi } from './token-service-qRSYG9uT.js';
|
|
6
|
+
import { B as BusinessApi, b as BusinessService, a as BusinessMembershipApi, c as BusinessMembershipService } from './business-membership-service-CPcE-AW0.js';
|
|
7
7
|
import { CampaignService, CampaignApi } from '../campaign.js';
|
|
8
|
-
import { a as RedemptionService, R as RedemptionApi } from './redemption-service-
|
|
8
|
+
import { a as RedemptionService, R as RedemptionApi } from './redemption-service-DWhZgrZT.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-xmYKBFGu.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,58 @@ 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
|
+
*
|
|
418
|
+
* Returns false for (retryable/recoverable):
|
|
419
|
+
* - Network errors
|
|
420
|
+
* - Timeouts
|
|
421
|
+
* - 5xx server errors
|
|
422
|
+
* - TOKEN_EXPIRED (normal refresh trigger)
|
|
423
|
+
* - Raw 401/403 without fatal code (should try refresh first)
|
|
424
|
+
*
|
|
425
|
+
* NOTE: We intentionally DO NOT treat raw 401/403 status as fatal.
|
|
426
|
+
* The HTTP client handles 401 by attempting token refresh first.
|
|
427
|
+
* Only if refresh fails WITH a fatal code do we logout.
|
|
428
|
+
*/
|
|
429
|
+
isDefinitiveAuthFailure(error) {
|
|
430
|
+
if (!error)
|
|
431
|
+
return false;
|
|
432
|
+
const errorAny = error;
|
|
433
|
+
// Check error code property against canonical list
|
|
434
|
+
if (errorAny?.code && this.isFatalAuthError(errorAny.code)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
// Check if error message contains fatal keywords (fallback for wrapped errors)
|
|
438
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
439
|
+
if (isFatalAuthErrorInMessage(errorMessage)) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
// NOTE: We intentionally DO NOT check HTTP status codes here.
|
|
443
|
+
// A raw 401/403 without a fatal error code should trigger a refresh attempt,
|
|
444
|
+
// not an immediate logout. The HTTP client (pers-api-client.ts) handles this:
|
|
445
|
+
// 1. 401 received → try refresh token
|
|
446
|
+
// 2. Refresh succeeds → retry original request
|
|
447
|
+
// 3. Refresh fails with fatal CODE → then logout
|
|
448
|
+
// Everything else is considered retryable
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Handle authentication failure with provider token recovery.
|
|
453
|
+
*
|
|
454
|
+
* Before logging out, attempts to recover the session using the stored
|
|
455
|
+
* provider token (Firebase JWT, etc.) if it's still valid.
|
|
456
|
+
*
|
|
457
|
+
* Recovery Flow:
|
|
458
|
+
* 1. Check if provider token exists and is not expired
|
|
459
|
+
* 2. Get the auth type (USER, BUSINESS, TENANT) to know which login to use
|
|
460
|
+
* 3. Attempt re-authentication with provider token
|
|
461
|
+
* 4. If successful: Session recovered, stay logged in
|
|
462
|
+
* 5. If failed: Clear tokens and emit AUTH_FAILED
|
|
404
463
|
*/
|
|
405
464
|
async handleAuthFailure() {
|
|
406
465
|
// Already failed - nothing to do
|
|
@@ -416,7 +475,7 @@ class AuthService {
|
|
|
416
475
|
return await this.activeAuthFailurePromise;
|
|
417
476
|
}
|
|
418
477
|
// Create the failure promise to deduplicate concurrent calls
|
|
419
|
-
this.activeAuthFailurePromise = this.
|
|
478
|
+
this.activeAuthFailurePromise = this.performAuthFailureWithRecovery();
|
|
420
479
|
try {
|
|
421
480
|
await this.activeAuthFailurePromise;
|
|
422
481
|
}
|
|
@@ -424,6 +483,77 @@ class AuthService {
|
|
|
424
483
|
this.activeAuthFailurePromise = null;
|
|
425
484
|
}
|
|
426
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Attempts to recover session using provider token before failing.
|
|
488
|
+
*/
|
|
489
|
+
async performAuthFailureWithRecovery() {
|
|
490
|
+
// Clear active refresh operations
|
|
491
|
+
this.activeRefreshPromise = null;
|
|
492
|
+
// Try to recover using provider token
|
|
493
|
+
const recovered = await this.attemptProviderTokenRecovery();
|
|
494
|
+
if (recovered) {
|
|
495
|
+
return; // Session recovered, don't fail
|
|
496
|
+
}
|
|
497
|
+
// Recovery failed - perform actual logout
|
|
498
|
+
await this.performAuthFailure();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Attempt to recover session using stored provider token.
|
|
502
|
+
* Extracts context (tenantId/businessId) from the current refresh token to ensure
|
|
503
|
+
* the recovered session maintains the same context.
|
|
504
|
+
*
|
|
505
|
+
* @returns true if session was successfully recovered
|
|
506
|
+
*/
|
|
507
|
+
async attemptProviderTokenRecovery() {
|
|
508
|
+
const extended = this.extendedProvider;
|
|
509
|
+
if (!extended?.getProviderToken || !extended?.getAuthType) {
|
|
510
|
+
return false; // Provider doesn't support recovery
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const providerToken = await extended.getProviderToken();
|
|
514
|
+
if (!providerToken) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
// Check if provider token is still valid (with small margin)
|
|
518
|
+
if (isTokenExpired(providerToken, 30)) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const authType = await extended.getAuthType();
|
|
522
|
+
if (!authType) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
// Extract context (tenantId/businessId) from the current refresh token
|
|
526
|
+
// This ensures multi-tenant/multi-business users recover to the same context
|
|
527
|
+
const refreshToken = await this.authProvider?.getRefreshToken?.();
|
|
528
|
+
const contextClaims = refreshToken ? decodeJwtPayload(refreshToken) : null;
|
|
529
|
+
// Re-authenticate using provider token based on auth type
|
|
530
|
+
// Pass the context from the previous session to avoid MULTIPLE_CONTEXT_SELECTION_REQUIRED
|
|
531
|
+
// Note: In AuthJWTPayload, when accountType=BUSINESS, sub contains the businessId
|
|
532
|
+
switch (authType) {
|
|
533
|
+
case AccountOwnerType.USER:
|
|
534
|
+
await this.loginUser(providerToken);
|
|
535
|
+
break;
|
|
536
|
+
case AccountOwnerType.BUSINESS:
|
|
537
|
+
// For BUSINESS auth, sub contains the businessId when accountType is BUSINESS
|
|
538
|
+
const businessId = contextClaims?.accountType === AccountOwnerType.BUSINESS
|
|
539
|
+
? contextClaims.sub
|
|
540
|
+
: undefined;
|
|
541
|
+
await this.loginBusiness(providerToken, businessId ? { businessId } : undefined);
|
|
542
|
+
break;
|
|
543
|
+
case AccountOwnerType.TENANT:
|
|
544
|
+
await this.loginTenantAdmin(providerToken, contextClaims?.tenantId ? { tenantId: contextClaims.tenantId } : undefined);
|
|
545
|
+
break;
|
|
546
|
+
default:
|
|
547
|
+
console.warn(`[AuthService] Unknown auth type for recovery: ${authType}`);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
return true; // Recovery successful
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
console.warn('[AuthService] Provider token recovery failed:', error);
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
427
557
|
/**
|
|
428
558
|
* Performs the actual auth failure cleanup
|
|
429
559
|
*/
|
|
@@ -503,16 +633,32 @@ class AuthService {
|
|
|
503
633
|
|
|
504
634
|
/**
|
|
505
635
|
* Token Refresh Manager
|
|
506
|
-
*
|
|
636
|
+
* Industry-standard token lifecycle management with:
|
|
637
|
+
* - Dynamic refresh margin based on token lifetime
|
|
638
|
+
* - Retry logic for transient failures
|
|
639
|
+
* - Only logout on definitive auth failures (not network errors)
|
|
507
640
|
*/
|
|
508
641
|
class TokenRefreshManager {
|
|
509
642
|
constructor(authService, authProvider) {
|
|
510
643
|
this.authService = authService;
|
|
511
644
|
this.authProvider = authProvider;
|
|
512
|
-
|
|
645
|
+
// Industry standard: refresh when 25% of token lifetime remains
|
|
646
|
+
this.REFRESH_THRESHOLD_PERCENT = 0.25;
|
|
647
|
+
// Minimum margin regardless of token lifetime
|
|
648
|
+
this.MIN_REFRESH_MARGIN_SECONDS = 30;
|
|
649
|
+
// Maximum margin (cap for very long-lived tokens)
|
|
650
|
+
this.MAX_REFRESH_MARGIN_SECONDS = 300; // 5 minutes
|
|
651
|
+
// Validation cache to avoid redundant checks
|
|
513
652
|
this.lastValidationTime = 0;
|
|
514
653
|
this.validationCacheDurationMs = 30000; // 30 seconds
|
|
654
|
+
// Retry configuration for transient failures
|
|
655
|
+
this.MAX_RETRY_ATTEMPTS = 3;
|
|
656
|
+
this.RETRY_DELAY_MS = 1000;
|
|
515
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Ensures the access token is valid, refreshing if needed.
|
|
660
|
+
* Uses dynamic margin based on token lifetime (industry standard).
|
|
661
|
+
*/
|
|
516
662
|
async ensureValidToken() {
|
|
517
663
|
const now = Date.now();
|
|
518
664
|
// Skip validation if we checked recently (within cache duration)
|
|
@@ -524,35 +670,95 @@ class TokenRefreshManager {
|
|
|
524
670
|
if (!token) {
|
|
525
671
|
return;
|
|
526
672
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
673
|
+
// Calculate dynamic refresh margin based on token lifetime
|
|
674
|
+
const marginSeconds = this.calculateRefreshMargin(token);
|
|
675
|
+
if (isTokenExpired(token, marginSeconds)) {
|
|
676
|
+
const result = await this.attemptRefreshWithRetry();
|
|
677
|
+
if (!result.success) {
|
|
678
|
+
if (result.retryable) {
|
|
679
|
+
// Transient error - don't logout, let next request retry
|
|
680
|
+
console.warn('[TokenRefreshManager] Refresh failed with retryable error, will retry on next request');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// Definitive auth failure - logout required
|
|
530
684
|
await this.authService.handleAuthFailure();
|
|
531
685
|
}
|
|
532
686
|
else {
|
|
533
|
-
//
|
|
687
|
+
// Refresh successful
|
|
534
688
|
this.lastValidationTime = now;
|
|
535
689
|
}
|
|
536
690
|
}
|
|
537
691
|
else {
|
|
538
|
-
// Token is valid
|
|
692
|
+
// Token is valid
|
|
539
693
|
this.lastValidationTime = now;
|
|
540
694
|
}
|
|
541
695
|
}
|
|
542
696
|
catch (error) {
|
|
543
|
-
|
|
697
|
+
// Unexpected error - log but don't logout (conservative approach)
|
|
698
|
+
console.error('[TokenRefreshManager] Unexpected error during token validation:', error);
|
|
544
699
|
}
|
|
545
700
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Calculate refresh margin dynamically based on token lifetime.
|
|
703
|
+
* Industry standard: refresh when 25% of lifetime remains.
|
|
704
|
+
*/
|
|
705
|
+
calculateRefreshMargin(token) {
|
|
706
|
+
const ttlSeconds = getTokenTimeToLive(token);
|
|
707
|
+
if (ttlSeconds === null || ttlSeconds <= 0) {
|
|
708
|
+
// Can't determine TTL, use minimum margin
|
|
709
|
+
return this.MIN_REFRESH_MARGIN_SECONDS;
|
|
550
710
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
711
|
+
// Calculate 25% of remaining lifetime
|
|
712
|
+
const dynamicMargin = Math.floor(ttlSeconds * this.REFRESH_THRESHOLD_PERCENT);
|
|
713
|
+
// Clamp between min and max
|
|
714
|
+
return Math.max(this.MIN_REFRESH_MARGIN_SECONDS, Math.min(dynamicMargin, this.MAX_REFRESH_MARGIN_SECONDS));
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Attempt token refresh with retry logic for transient failures.
|
|
718
|
+
* Only gives up on definitive auth errors.
|
|
719
|
+
*/
|
|
720
|
+
async attemptRefreshWithRetry() {
|
|
721
|
+
let lastError;
|
|
722
|
+
for (let attempt = 1; attempt <= this.MAX_RETRY_ATTEMPTS; attempt++) {
|
|
723
|
+
try {
|
|
724
|
+
await this.authService.refreshAccessToken();
|
|
725
|
+
return { success: true, retryable: false };
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
729
|
+
// Check if this is a definitive auth failure (non-retryable)
|
|
730
|
+
if (this.isDefinitiveAuthFailure(error)) {
|
|
731
|
+
console.warn(`[TokenRefreshManager] Definitive auth failure: ${lastError.message}`);
|
|
732
|
+
return { success: false, retryable: false, error: lastError };
|
|
733
|
+
}
|
|
734
|
+
// Retryable error - wait and try again
|
|
735
|
+
if (attempt < this.MAX_RETRY_ATTEMPTS) {
|
|
736
|
+
await this.delay(this.RETRY_DELAY_MS * attempt); // Exponential backoff
|
|
737
|
+
}
|
|
738
|
+
}
|
|
555
739
|
}
|
|
740
|
+
// All retries exhausted - return as retryable so we don't logout
|
|
741
|
+
console.warn(`[TokenRefreshManager] All ${this.MAX_RETRY_ATTEMPTS} refresh attempts failed`);
|
|
742
|
+
return { success: false, retryable: true, error: lastError };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Determine if an error is a definitive auth failure that requires logout.
|
|
746
|
+
* Network errors, timeouts, 5xx errors are NOT definitive - they're retryable.
|
|
747
|
+
*
|
|
748
|
+
* Delegates to AuthService.isDefinitiveAuthFailure() - the SINGLE SOURCE OF TRUTH.
|
|
749
|
+
*/
|
|
750
|
+
isDefinitiveAuthFailure(error) {
|
|
751
|
+
return this.authService.isDefinitiveAuthFailure(error);
|
|
752
|
+
}
|
|
753
|
+
delay(ms) {
|
|
754
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* @deprecated Use ensureValidToken() instead
|
|
758
|
+
*/
|
|
759
|
+
async attemptInternalRefresh() {
|
|
760
|
+
const result = await this.attemptRefreshWithRetry();
|
|
761
|
+
return result.success;
|
|
556
762
|
}
|
|
557
763
|
}
|
|
558
764
|
|
|
@@ -700,6 +906,22 @@ class AuthTokenManager {
|
|
|
700
906
|
this.cache = {};
|
|
701
907
|
await this.storage.clear();
|
|
702
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Clears only auth tokens (access, refresh, provider, authType), preserving DPoP keys.
|
|
911
|
+
* Use this when DPoP keys are regenerated - we want to invalidate auth tokens
|
|
912
|
+
* that were bound to old keys, but keep the newly generated DPoP keys.
|
|
913
|
+
*/
|
|
914
|
+
async clearAuthTokens() {
|
|
915
|
+
// Clear cache
|
|
916
|
+
this.cache = {};
|
|
917
|
+
// Remove all auth tokens and auth type, NOT DPoP keys
|
|
918
|
+
await Promise.all([
|
|
919
|
+
this.storage.remove(AUTH_STORAGE_KEYS.ACCESS_TOKEN),
|
|
920
|
+
this.storage.remove(AUTH_STORAGE_KEYS.REFRESH_TOKEN),
|
|
921
|
+
this.storage.remove(AUTH_STORAGE_KEYS.PROVIDER_TOKEN),
|
|
922
|
+
this.storage.remove(AUTH_STORAGE_KEYS.AUTH_TYPE),
|
|
923
|
+
]);
|
|
924
|
+
}
|
|
703
925
|
async hasAccessToken() {
|
|
704
926
|
// Use cached value if available to avoid storage read
|
|
705
927
|
if (this.cache.accessToken !== undefined) {
|
|
@@ -730,13 +952,12 @@ class IndexedDBTokenStorage {
|
|
|
730
952
|
constructor() {
|
|
731
953
|
this.supportsObjects = true;
|
|
732
954
|
this.dbPromise = null;
|
|
733
|
-
|
|
734
|
-
console.warn('IndexedDB is not available in this environment');
|
|
735
|
-
}
|
|
955
|
+
// IndexedDB availability is checked lazily on first operation
|
|
736
956
|
}
|
|
737
957
|
getDB() {
|
|
738
|
-
if (this.dbPromise)
|
|
958
|
+
if (this.dbPromise) {
|
|
739
959
|
return this.dbPromise;
|
|
960
|
+
}
|
|
740
961
|
this.dbPromise = new Promise((resolve, reject) => {
|
|
741
962
|
if (typeof indexedDB === 'undefined') {
|
|
742
963
|
return reject(new Error('IndexedDB not supported'));
|
|
@@ -764,8 +985,12 @@ class IndexedDBTokenStorage {
|
|
|
764
985
|
const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readonly');
|
|
765
986
|
const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
|
|
766
987
|
const request = store.get(key);
|
|
767
|
-
request.onsuccess = () =>
|
|
768
|
-
|
|
988
|
+
request.onsuccess = () => {
|
|
989
|
+
resolve(request.result || null);
|
|
990
|
+
};
|
|
991
|
+
request.onerror = () => {
|
|
992
|
+
reject(request.error);
|
|
993
|
+
};
|
|
769
994
|
});
|
|
770
995
|
}
|
|
771
996
|
catch (e) {
|
|
@@ -961,6 +1186,8 @@ class DPoPManager {
|
|
|
961
1186
|
constructor(storage, cryptoProvider, callbacks) {
|
|
962
1187
|
this.storage = storage;
|
|
963
1188
|
this.memoryKeyPair = null;
|
|
1189
|
+
/** Pending key pair promise to prevent concurrent generation race conditions */
|
|
1190
|
+
this.pendingKeyPair = null;
|
|
964
1191
|
this.cryptoProvider = cryptoProvider || new WebDPoPCryptoProvider();
|
|
965
1192
|
this.callbacks = callbacks || {};
|
|
966
1193
|
}
|
|
@@ -969,8 +1196,9 @@ class DPoPManager {
|
|
|
969
1196
|
* Useful for detecting if a key regeneration would be needed.
|
|
970
1197
|
*/
|
|
971
1198
|
async hasStoredKeys() {
|
|
972
|
-
if (this.memoryKeyPair)
|
|
1199
|
+
if (this.memoryKeyPair) {
|
|
973
1200
|
return true;
|
|
1201
|
+
}
|
|
974
1202
|
const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
|
|
975
1203
|
const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
|
|
976
1204
|
return !!(storedPublic && storedPrivate);
|
|
@@ -984,8 +1212,35 @@ class DPoPManager {
|
|
|
984
1212
|
* match refresh token binding" errors.
|
|
985
1213
|
*/
|
|
986
1214
|
async ensureKeyPair() {
|
|
987
|
-
|
|
1215
|
+
// Fast path: already have keys in memory
|
|
1216
|
+
if (this.memoryKeyPair) {
|
|
1217
|
+
return this.memoryKeyPair;
|
|
1218
|
+
}
|
|
1219
|
+
// Prevent concurrent key generation race condition:
|
|
1220
|
+
// If another call is already generating keys, wait for it instead of starting a new one
|
|
1221
|
+
if (this.pendingKeyPair) {
|
|
1222
|
+
return this.pendingKeyPair;
|
|
1223
|
+
}
|
|
1224
|
+
// Create a promise that subsequent callers will wait on
|
|
1225
|
+
this.pendingKeyPair = this.loadOrGenerateKeyPair();
|
|
1226
|
+
try {
|
|
1227
|
+
const keyPair = await this.pendingKeyPair;
|
|
1228
|
+
return keyPair;
|
|
1229
|
+
}
|
|
1230
|
+
finally {
|
|
1231
|
+
// Clear pending promise so future calls can proceed
|
|
1232
|
+
this.pendingKeyPair = null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Internal method that actually loads or generates keys.
|
|
1237
|
+
* Called only once per concurrent batch of ensureKeyPair() calls.
|
|
1238
|
+
*/
|
|
1239
|
+
async loadOrGenerateKeyPair() {
|
|
1240
|
+
// Double-check in case keys were set while waiting
|
|
1241
|
+
if (this.memoryKeyPair) {
|
|
988
1242
|
return this.memoryKeyPair;
|
|
1243
|
+
}
|
|
989
1244
|
const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
|
|
990
1245
|
const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
|
|
991
1246
|
if (storedPublic && storedPrivate) {
|
|
@@ -997,18 +1252,23 @@ class DPoPManager {
|
|
|
997
1252
|
return this.memoryKeyPair;
|
|
998
1253
|
}
|
|
999
1254
|
catch (e) {
|
|
1000
|
-
console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating
|
|
1255
|
+
console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating');
|
|
1001
1256
|
}
|
|
1002
1257
|
}
|
|
1003
1258
|
// Generate new key pair
|
|
1004
1259
|
// 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),
|
|
1260
|
+
// Adaptation: If storage supports raw objects (like Native Keychain on iOS/Android),
|
|
1007
1261
|
// we can generate Non-Extractable keys for maximum security.
|
|
1008
|
-
// If storage is text-only (LocalStorage)
|
|
1262
|
+
// If storage is text-only (LocalStorage) OR IndexedDB (CryptoKey doesn't survive reload),
|
|
1263
|
+
// we must use Extractable keys to serialize them as JWK.
|
|
1264
|
+
//
|
|
1265
|
+
// NOTE: IndexedDB CAN store CryptoKey objects, but they don't survive page reloads -
|
|
1266
|
+
// they come back as empty/corrupted objects. Only native secure storage can persist
|
|
1267
|
+
// non-extractable keys. Frontend should set supportsObjects: false for web storage.
|
|
1009
1268
|
const useHighSecurity = !!this.storage.supportsObjects;
|
|
1269
|
+
const extractable = !useHighSecurity;
|
|
1010
1270
|
const keyPair = await this.cryptoProvider.generateKeyPair({
|
|
1011
|
-
extractable
|
|
1271
|
+
extractable
|
|
1012
1272
|
});
|
|
1013
1273
|
// Save to storage
|
|
1014
1274
|
await this.storage.set(DPOP_STORAGE_KEYS.PUBLIC, keyPair.publicKey);
|
|
@@ -1042,6 +1302,20 @@ class DPoPManager {
|
|
|
1042
1302
|
}
|
|
1043
1303
|
return this.cryptoProvider.signProof(payload, keyPair);
|
|
1044
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Generate a short fingerprint of the public key for debugging
|
|
1307
|
+
* This helps identify if the same key is being used across operations
|
|
1308
|
+
*/
|
|
1309
|
+
/* private getPublicKeyFingerprint(publicKey: JsonWebKey): string {
|
|
1310
|
+
try {
|
|
1311
|
+
// Use the 'x' coordinate of the EC key as a fingerprint
|
|
1312
|
+
const x = publicKey.x || '';
|
|
1313
|
+
const y = publicKey.y || '';
|
|
1314
|
+
return `${x.substring(0, 8)}...${y.substring(0, 4)}`;
|
|
1315
|
+
} catch {
|
|
1316
|
+
return 'unknown';
|
|
1317
|
+
}
|
|
1318
|
+
} */
|
|
1045
1319
|
/**
|
|
1046
1320
|
* Clears DPoP keys (e.g. on logout)
|
|
1047
1321
|
*/
|
|
@@ -1090,13 +1364,15 @@ class DefaultAuthProvider {
|
|
|
1090
1364
|
this.tokenManager = new AuthTokenManager(storage);
|
|
1091
1365
|
if (this.dpopEnabled) {
|
|
1092
1366
|
this.dpopManager = new DPoPManager(storage, config.dpop?.cryptoProvider, {
|
|
1093
|
-
// When DPoP keys are regenerated (corrupted/missing),
|
|
1094
|
-
// bound to the old keys
|
|
1095
|
-
//
|
|
1367
|
+
// When DPoP keys are regenerated (corrupted/missing), auth tokens
|
|
1368
|
+
// bound to the old keys become invalid. Clear only auth tokens, NOT DPoP keys.
|
|
1369
|
+
// Access token has cnf claim binding it to the old public key.
|
|
1370
|
+
// Refresh token has DPoP binding to the old key pair.
|
|
1371
|
+
// NOTE: Do NOT call clearAllTokens() here - it would wipe the freshly saved DPoP keys!
|
|
1096
1372
|
onKeysRegenerated: () => {
|
|
1097
|
-
|
|
1098
|
-
this.tokenManager.
|
|
1099
|
-
console.warn('[DefaultAuthProvider] Failed to clear
|
|
1373
|
+
// Clear only auth tokens, preserve DPoP keys (they were just regenerated and saved)
|
|
1374
|
+
this.tokenManager.clearAuthTokens().catch(err => {
|
|
1375
|
+
console.warn('[DefaultAuthProvider] Failed to clear tokens after key regeneration:', err);
|
|
1100
1376
|
});
|
|
1101
1377
|
}
|
|
1102
1378
|
});
|
|
@@ -1238,6 +1514,41 @@ class DefaultAuthProvider {
|
|
|
1238
1514
|
return null;
|
|
1239
1515
|
}
|
|
1240
1516
|
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Store the provider token (Firebase JWT, Auth0 token, etc.)
|
|
1519
|
+
* Used for session recovery when refresh tokens expire.
|
|
1520
|
+
*
|
|
1521
|
+
* NOTE: This is called AUTOMATICALLY by loginWithToken(), so you typically
|
|
1522
|
+
* don't need to call this manually. The only reason to call this is if:
|
|
1523
|
+
*
|
|
1524
|
+
* 1. Your auth provider (Firebase, Auth0) refreshes tokens in the background
|
|
1525
|
+
* 2. You want to proactively update the stored token for better recovery chances
|
|
1526
|
+
*
|
|
1527
|
+
* The stored provider token is used as a fallback when:
|
|
1528
|
+
* - PERS refresh token expires (e.g., after 30 days of inactivity)
|
|
1529
|
+
* - SDK attempts recovery using the stored provider token
|
|
1530
|
+
* - If provider token is still valid → session recovered automatically
|
|
1531
|
+
* - If provider token is also expired → user must re-authenticate
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* // Optional: Update stored token when Firebase refreshes it
|
|
1535
|
+
* firebase.auth().onIdTokenChanged(async (user) => {
|
|
1536
|
+
* if (user) {
|
|
1537
|
+
* const freshToken = await user.getIdToken();
|
|
1538
|
+
* await sdk.auth.setProviderToken(freshToken);
|
|
1539
|
+
* }
|
|
1540
|
+
* });
|
|
1541
|
+
*/
|
|
1542
|
+
async setProviderToken(token) {
|
|
1543
|
+
await this.tokenManager.setProviderToken(token);
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Get the stored provider token (Firebase JWT, Auth0 token, etc.)
|
|
1547
|
+
* Used internally for session recovery when refresh tokens expire.
|
|
1548
|
+
*/
|
|
1549
|
+
async getProviderToken() {
|
|
1550
|
+
return this.tokenManager.getProviderToken();
|
|
1551
|
+
}
|
|
1241
1552
|
}
|
|
1242
1553
|
|
|
1243
1554
|
/**
|
|
@@ -1253,7 +1564,7 @@ class DefaultAuthProvider {
|
|
|
1253
1564
|
/** SDK package name */
|
|
1254
1565
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
1255
1566
|
/** SDK version - injected from package.json at build time */
|
|
1256
|
-
const SDK_VERSION = "2.1.
|
|
1567
|
+
const SDK_VERSION = "2.1.37";
|
|
1257
1568
|
/** Full SDK identifier for headers */
|
|
1258
1569
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
1259
1570
|
|
|
@@ -1402,7 +1713,9 @@ class PersApiClient {
|
|
|
1402
1713
|
// Handle 401 errors centrally through AuthService
|
|
1403
1714
|
if (status === 401) {
|
|
1404
1715
|
const backendError = ErrorUtils.extractBackendErrorDetails(error);
|
|
1405
|
-
// Check for
|
|
1716
|
+
// Check for FATAL auth error CODES (not just 401 status)
|
|
1717
|
+
// Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc. → immediate logout
|
|
1718
|
+
// Non-fatal codes: TOKEN_EXPIRED → try refresh first
|
|
1406
1719
|
if (this.authService.isFatalAuthError(backendError.code)) {
|
|
1407
1720
|
// Definitive auth failure - no retry, session is invalid
|
|
1408
1721
|
await this.authService.handleAuthFailure();
|
|
@@ -1462,9 +1775,9 @@ class PersApiClient {
|
|
|
1462
1775
|
}); */
|
|
1463
1776
|
this._events?.emitError({
|
|
1464
1777
|
domain: errorDetails.domain || 'external',
|
|
1465
|
-
type:
|
|
1466
|
-
userMessage: errorDetails.message || errorMessage,
|
|
1467
|
-
code: errorDetails.code,
|
|
1778
|
+
type: 'api_error', // Event type discriminator (fixed value)
|
|
1779
|
+
userMessage: errorDetails.message || errorMessage,
|
|
1780
|
+
code: errorDetails.code, // Actual error code from backend
|
|
1468
1781
|
details: {
|
|
1469
1782
|
endpoint,
|
|
1470
1783
|
method,
|
|
@@ -3100,6 +3413,107 @@ class TokenManager {
|
|
|
3100
3413
|
async getTokenByContract(contractAddress, contractTokenId = null) {
|
|
3101
3414
|
return this.tokenService.getTokenByContractAddress(contractAddress, contractTokenId);
|
|
3102
3415
|
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Get all token metadata with filtering and pagination
|
|
3418
|
+
*
|
|
3419
|
+
* Retrieves token metadata (rewards/stamps) with server-side filtering and pagination.
|
|
3420
|
+
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in admin tables.
|
|
3421
|
+
* Non-admin users always get active metadata only.
|
|
3422
|
+
*
|
|
3423
|
+
* @param options - Filter and pagination options
|
|
3424
|
+
* @returns Promise resolving to paginated token metadata
|
|
3425
|
+
*
|
|
3426
|
+
* @example Get paginated rewards
|
|
3427
|
+
* ```typescript
|
|
3428
|
+
* const rewards = await sdk.tokens.getTokenMetadata({
|
|
3429
|
+
* tokenType: 'ERC1155',
|
|
3430
|
+
* active: true,
|
|
3431
|
+
* page: 1,
|
|
3432
|
+
* limit: 10,
|
|
3433
|
+
* sortBy: 'name',
|
|
3434
|
+
* sortOrder: 'ASC'
|
|
3435
|
+
* });
|
|
3436
|
+
*
|
|
3437
|
+
* console.log(`Page 1 of ${rewards.pagination.pages}`);
|
|
3438
|
+
* rewards.data.forEach(r => console.log(r.name));
|
|
3439
|
+
* ```
|
|
3440
|
+
*
|
|
3441
|
+
* @example Get stamps with search
|
|
3442
|
+
* ```typescript
|
|
3443
|
+
* const stamps = await sdk.tokens.getTokenMetadata({
|
|
3444
|
+
* tokenType: 'ERC721',
|
|
3445
|
+
* search: 'gold',
|
|
3446
|
+
* limit: 25
|
|
3447
|
+
* });
|
|
3448
|
+
* ```
|
|
3449
|
+
*/
|
|
3450
|
+
async getTokenMetadata(options) {
|
|
3451
|
+
return this.tokenService.getTokenMetadata(options);
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Get rewards (ERC1155 token metadata) with filtering and pagination
|
|
3455
|
+
*
|
|
3456
|
+
* Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
|
|
3457
|
+
* that represent collectible items, vouchers, or other redeemable rewards.
|
|
3458
|
+
*
|
|
3459
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
3460
|
+
* @returns Promise resolving to paginated reward metadata
|
|
3461
|
+
*
|
|
3462
|
+
* @example Get active rewards
|
|
3463
|
+
* ```typescript
|
|
3464
|
+
* const rewards = await sdk.tokens.getRewards({
|
|
3465
|
+
* active: true,
|
|
3466
|
+
* limit: 10
|
|
3467
|
+
* });
|
|
3468
|
+
*
|
|
3469
|
+
* console.log(`Found ${rewards.pagination.total} rewards`);
|
|
3470
|
+
* rewards.data.forEach(r => console.log(`${r.name}: ${r.description}`));
|
|
3471
|
+
* ```
|
|
3472
|
+
*
|
|
3473
|
+
* @example Search rewards
|
|
3474
|
+
* ```typescript
|
|
3475
|
+
* const discountRewards = await sdk.tokens.getRewards({
|
|
3476
|
+
* search: 'discount',
|
|
3477
|
+
* sortBy: 'name',
|
|
3478
|
+
* sortOrder: 'ASC'
|
|
3479
|
+
* });
|
|
3480
|
+
* ```
|
|
3481
|
+
*/
|
|
3482
|
+
async getRewards(options) {
|
|
3483
|
+
return this.tokenService.getRewards(options);
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Get stamps (ERC721 token metadata) with filtering and pagination
|
|
3487
|
+
*
|
|
3488
|
+
* Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
|
|
3489
|
+
* that represent achievements, badges, or collectible status markers.
|
|
3490
|
+
*
|
|
3491
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
3492
|
+
* @returns Promise resolving to paginated stamp metadata
|
|
3493
|
+
*
|
|
3494
|
+
* @example Get active stamps
|
|
3495
|
+
* ```typescript
|
|
3496
|
+
* const stamps = await sdk.tokens.getStamps({
|
|
3497
|
+
* active: true,
|
|
3498
|
+
* limit: 10
|
|
3499
|
+
* });
|
|
3500
|
+
*
|
|
3501
|
+
* console.log(`Found ${stamps.pagination.total} stamps`);
|
|
3502
|
+
* stamps.data.forEach(s => console.log(`${s.name}: ${s.description}`));
|
|
3503
|
+
* ```
|
|
3504
|
+
*
|
|
3505
|
+
* @example Filter stamps by tags
|
|
3506
|
+
* ```typescript
|
|
3507
|
+
* const goldStamps = await sdk.tokens.getStamps({
|
|
3508
|
+
* tags: ['gold', 'premium'],
|
|
3509
|
+
* sortBy: 'createdAt',
|
|
3510
|
+
* sortOrder: 'DESC'
|
|
3511
|
+
* });
|
|
3512
|
+
* ```
|
|
3513
|
+
*/
|
|
3514
|
+
async getStamps(options) {
|
|
3515
|
+
return this.tokenService.getStamps(options);
|
|
3516
|
+
}
|
|
3103
3517
|
/**
|
|
3104
3518
|
* Admin: Create new token
|
|
3105
3519
|
*
|
|
@@ -5004,6 +5418,52 @@ class RedemptionManager {
|
|
|
5004
5418
|
async getRedemptions(options) {
|
|
5005
5419
|
return this.redemptionService.getRedemptions(options);
|
|
5006
5420
|
}
|
|
5421
|
+
/**
|
|
5422
|
+
* Get a specific redemption by ID
|
|
5423
|
+
*
|
|
5424
|
+
* Retrieves detailed information about a specific redemption offer.
|
|
5425
|
+
* Useful when you already have the redemption ID and need full details.
|
|
5426
|
+
*
|
|
5427
|
+
* @param id - Redemption UUID
|
|
5428
|
+
* @param include - Optional relations to include: 'redeemCount'
|
|
5429
|
+
* @returns Promise resolving to the redemption
|
|
5430
|
+
*
|
|
5431
|
+
* @example Basic usage
|
|
5432
|
+
* ```typescript
|
|
5433
|
+
* const redemption = await sdk.redemptions.getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
|
|
5434
|
+
* console.log(redemption.name);
|
|
5435
|
+
* console.log(redemption.description);
|
|
5436
|
+
* ```
|
|
5437
|
+
*
|
|
5438
|
+
* @example With redeem count
|
|
5439
|
+
* ```typescript
|
|
5440
|
+
* const redemption = await sdk.redemptions.getRedemptionById(
|
|
5441
|
+
* 'redemption-123',
|
|
5442
|
+
* ['redeemCount']
|
|
5443
|
+
* );
|
|
5444
|
+
* console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
|
|
5445
|
+
* ```
|
|
5446
|
+
*
|
|
5447
|
+
* @example Get redemption details for display
|
|
5448
|
+
* ```typescript
|
|
5449
|
+
* const redemption = await sdk.redemptions.getRedemptionById(id);
|
|
5450
|
+
*
|
|
5451
|
+
* console.log('Redemption Details:');
|
|
5452
|
+
* console.log(`Name: ${redemption.name}`);
|
|
5453
|
+
* console.log(`Description: ${redemption.description}`);
|
|
5454
|
+
* console.log(`Active: ${redemption.isActive}`);
|
|
5455
|
+
*
|
|
5456
|
+
* if (redemption.tokenUnits?.length) {
|
|
5457
|
+
* console.log('Cost:');
|
|
5458
|
+
* redemption.tokenUnits.forEach(unit => {
|
|
5459
|
+
* console.log(` ${unit.amount} ${unit.token.symbol}`);
|
|
5460
|
+
* });
|
|
5461
|
+
* }
|
|
5462
|
+
* ```
|
|
5463
|
+
*/
|
|
5464
|
+
async getRedemptionById(id, include) {
|
|
5465
|
+
return this.redemptionService.getRedemptionById(id, include);
|
|
5466
|
+
}
|
|
5007
5467
|
/**
|
|
5008
5468
|
* Get available redemption types
|
|
5009
5469
|
*
|
|
@@ -7529,6 +7989,55 @@ class AnalyticsManager {
|
|
|
7529
7989
|
async getCampaignClaimAnalytics(request) {
|
|
7530
7990
|
return this.analyticsService.getCampaignClaimAnalytics(request);
|
|
7531
7991
|
}
|
|
7992
|
+
/**
|
|
7993
|
+
* Get redemption redeem analytics with aggregation
|
|
7994
|
+
*
|
|
7995
|
+
* Retrieves aggregated redemption redeem data with flexible grouping and metrics.
|
|
7996
|
+
* Perfect for charts, dashboards, and redeem pattern analysis across redemptions,
|
|
7997
|
+
* businesses, and time periods.
|
|
7998
|
+
*
|
|
7999
|
+
* @param request - Analytics request with filters, groupBy, and metrics
|
|
8000
|
+
* @returns Promise resolving to redemption redeem analytics data
|
|
8001
|
+
*
|
|
8002
|
+
* @example Redeems per redemption
|
|
8003
|
+
* ```typescript
|
|
8004
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
8005
|
+
* groupBy: ['redemptionId'],
|
|
8006
|
+
* metrics: ['count'],
|
|
8007
|
+
* sortBy: 'count',
|
|
8008
|
+
* sortOrder: 'DESC',
|
|
8009
|
+
* limit: 10
|
|
8010
|
+
* });
|
|
8011
|
+
*
|
|
8012
|
+
* console.log('Top redemptions by redeems:', analytics.results);
|
|
8013
|
+
* ```
|
|
8014
|
+
*
|
|
8015
|
+
* @example Redeems by status over time
|
|
8016
|
+
* ```typescript
|
|
8017
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
8018
|
+
* groupBy: ['month', 'status'],
|
|
8019
|
+
* metrics: ['count'],
|
|
8020
|
+
* sortBy: 'month',
|
|
8021
|
+
* sortOrder: 'DESC',
|
|
8022
|
+
* startDate: new Date('2026-01-01'),
|
|
8023
|
+
* endDate: new Date('2026-12-31')
|
|
8024
|
+
* });
|
|
8025
|
+
* ```
|
|
8026
|
+
*
|
|
8027
|
+
* @example Redeems by business
|
|
8028
|
+
* ```typescript
|
|
8029
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
8030
|
+
* filters: { status: 'COMPLETED' },
|
|
8031
|
+
* groupBy: ['businessId'],
|
|
8032
|
+
* metrics: ['count'],
|
|
8033
|
+
* sortBy: 'count',
|
|
8034
|
+
* sortOrder: 'DESC'
|
|
8035
|
+
* });
|
|
8036
|
+
* ```
|
|
8037
|
+
*/
|
|
8038
|
+
async getRedemptionRedeemAnalytics(request) {
|
|
8039
|
+
return this.analyticsService.getRedemptionRedeemAnalytics(request);
|
|
8040
|
+
}
|
|
7532
8041
|
/**
|
|
7533
8042
|
* Get user analytics with engagement metrics
|
|
7534
8043
|
*
|
|
@@ -10310,5 +10819,5 @@ function createPersSDK(httpClient, config) {
|
|
|
10310
10819
|
return new PersSDK(httpClient, config);
|
|
10311
10820
|
}
|
|
10312
10821
|
|
|
10313
|
-
export { AuthStatus as A, BusinessManager as B, CampaignManager as C, DefaultAuthProvider as D,
|
|
10314
|
-
//# sourceMappingURL=pers-sdk-
|
|
10822
|
+
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 };
|
|
10823
|
+
//# sourceMappingURL=pers-sdk-Dds2lB27.js.map
|