@explorins/pers-sdk-react-native 2.1.5 → 2.1.6

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/index.js CHANGED
@@ -2593,6 +2593,8 @@ exports.TransactionStatus = void 0;
2593
2593
  TransactionStatus["PROCESSING"] = "processing";
2594
2594
  // when transaction is pending to be signed
2595
2595
  TransactionStatus["PENDING_SIGNATURE"] = "pending_signature";
2596
+ // when transaction is signed but pending submission (sign-only flow)
2597
+ TransactionStatus["PENDING_SUBMISSION"] = "pending_submission";
2596
2598
  // after transaction has been broadcasted
2597
2599
  TransactionStatus["BROADCASTED"] = "broadcasted";
2598
2600
  // after transaction has succeeded
@@ -2634,6 +2636,7 @@ exports.ApiKeyType = void 0;
2634
2636
  ApiKeyType["BLOCKCHAIN_WRITER_JWT"] = "BLOCKCHAIN_WRITER_JWT";
2635
2637
  ApiKeyType["BLOCKCHAIN_READER_JWT"] = "BLOCKCHAIN_READER_JWT";
2636
2638
  ApiKeyType["TRANSACTION_JWT_ACCESS_TOKEN"] = "TRANSACTION_JWT_ACCESS_TOKEN";
2639
+ ApiKeyType["WEBHOOK_OUTBOUND_JWT"] = "WEBHOOK_OUTBOUND_JWT";
2637
2640
  // TODO: this needs to be removed. However, there is still dependency
2638
2641
  ApiKeyType["TENANT_LEGACY_ADMIN"] = "TENANT_ADMIN_JWT";
2639
2642
  // TENANT_SYSTEM_API_SECRET = 'TENANT_SYSTEM_API_SECRET',
@@ -2850,6 +2853,66 @@ exports.ProcessRecordStatus = void 0;
2850
2853
  ProcessRecordStatus["FAILED"] = "FAILED"; // Transaction processing failed (blockchain or validation error)
2851
2854
  })(exports.ProcessRecordStatus || (exports.ProcessRecordStatus = {}));
2852
2855
 
2856
+ /**
2857
+ * Entity types that support file uploads in the storage system.
2858
+ * Used for organizing uploaded files by entity category.
2859
+ */
2860
+ exports.FileUploadEntityType = void 0;
2861
+ (function (FileUploadEntityType) {
2862
+ FileUploadEntityType["TOKEN"] = "token";
2863
+ FileUploadEntityType["CAMPAIGN"] = "campaign";
2864
+ FileUploadEntityType["REDEMPTION"] = "redemption";
2865
+ FileUploadEntityType["BUSINESS"] = "business";
2866
+ FileUploadEntityType["TENANT"] = "tenant";
2867
+ FileUploadEntityType["USER"] = "user";
2868
+ })(exports.FileUploadEntityType || (exports.FileUploadEntityType = {}));
2869
+
2870
+ /**
2871
+ * Types of signed URLs for storage operations.
2872
+ * GET: For downloading/reading files
2873
+ * PUT: For uploading/writing files
2874
+ */
2875
+ exports.SignedUrlType = void 0;
2876
+ (function (SignedUrlType) {
2877
+ SignedUrlType["GET"] = "GET";
2878
+ SignedUrlType["PUT"] = "PUT";
2879
+ })(exports.SignedUrlType || (exports.SignedUrlType = {}));
2880
+
2881
+ /**
2882
+ * Token Validity Type Enum
2883
+ *
2884
+ * Defines how token expiry is calculated:
2885
+ * - FIXED_DATE: Uses expiryDate directly as absolute expiration
2886
+ * - DAYS_FROM_ISSUANCE: Computes expiry as issuance date + validityDuration days
2887
+ * - HOURS_FROM_ISSUANCE: Computes expiry as issuance date + validityDuration hours
2888
+ * - MONTHS_FROM_ISSUANCE: Computes expiry as issuance date + validityDuration months
2889
+ * - END_OF_MONTH: Token expires at end of the month when issued
2890
+ * - END_OF_YEAR: Token expires at end of the year when issued
2891
+ */
2892
+ const TokenValidityTypes = {
2893
+ FIXED_DATE: 'fixed_date',
2894
+ DAYS_FROM_ISSUANCE: 'days_from_issuance',
2895
+ HOURS_FROM_ISSUANCE: 'hours_from_issuance',
2896
+ MONTHS_FROM_ISSUANCE: 'months_from_issuance',
2897
+ END_OF_MONTH: 'end_of_month',
2898
+ END_OF_YEAR: 'end_of_year',
2899
+ };
2900
+ const TOKEN_VALIDITY_TYPE_VALUES = Object.values(TokenValidityTypes);
2901
+
2902
+ /**
2903
+ * Balance Migration Type
2904
+ *
2905
+ * Defines how token balances should be migrated between accounts.
2906
+ * Used during user merge operations and account balance transfers.
2907
+ */
2908
+ exports.BalanceMigrationType = void 0;
2909
+ (function (BalanceMigrationType) {
2910
+ /** Replace: New balance = from balance - to balance (diff only) */
2911
+ BalanceMigrationType["REPLACE"] = "replace";
2912
+ /** Merge: New balance = from balance (full transfer) */
2913
+ BalanceMigrationType["MERGE"] = "merge";
2914
+ })(exports.BalanceMigrationType || (exports.BalanceMigrationType = {}));
2915
+
2853
2916
  exports.PurchaseStatus = void 0;
2854
2917
  (function (PurchaseStatus) {
2855
2918
  // after creation of payment and before payment is done by user
@@ -2911,6 +2974,208 @@ exports.CampaignTriggerType = void 0;
2911
2974
  CampaignTriggerType["CLAIM_BY_BUSINESS"] = "CLAIM_BY_BUSINESS";
2912
2975
  })(exports.CampaignTriggerType || (exports.CampaignTriggerType = {}));
2913
2976
 
2977
+ /**
2978
+ * AI Operation Types
2979
+ *
2980
+ * Categorizes AI generation operations for analytics and billing.
2981
+ * Used across AI module and Analytics module.
2982
+ */
2983
+ exports.AiOperationType = void 0;
2984
+ (function (AiOperationType) {
2985
+ /** Text generation using Gemini models */
2986
+ AiOperationType["TEXT_GENERATION"] = "TEXT_GENERATION";
2987
+ /** Text generation with streaming response */
2988
+ AiOperationType["TEXT_GENERATION_STREAM"] = "TEXT_GENERATION_STREAM";
2989
+ /** Text generation with reasoning/thinking (Gemini 2.5+) */
2990
+ AiOperationType["TEXT_WITH_REASONING"] = "TEXT_WITH_REASONING";
2991
+ /** Image generation using Imagen models */
2992
+ AiOperationType["IMAGE_GENERATION"] = "IMAGE_GENERATION";
2993
+ /** Structured output generation with schema */
2994
+ AiOperationType["STRUCTURED_OUTPUT"] = "STRUCTURED_OUTPUT";
2995
+ /** Multimodal input processing (text + images) */
2996
+ AiOperationType["MULTIMODAL"] = "MULTIMODAL";
2997
+ })(exports.AiOperationType || (exports.AiOperationType = {}));
2998
+
2999
+ /**
3000
+ * AI Trigger Process Types
3001
+ *
3002
+ * Identifies which business process triggered an AI generation.
3003
+ * Used for cost allocation and analytics grouping.
3004
+ *
3005
+ * NOTE: This enum will grow as AI is integrated into more processes.
3006
+ */
3007
+ exports.AiTriggerProcessType = void 0;
3008
+ (function (AiTriggerProcessType) {
3009
+ /** AI triggered during transaction processing (e.g., dynamic token image/description) */
3010
+ AiTriggerProcessType["TRANSACTION"] = "TRANSACTION";
3011
+ })(exports.AiTriggerProcessType || (exports.AiTriggerProcessType = {}));
3012
+
3013
+ /**
3014
+ * AI Analytics Metric Enum
3015
+ *
3016
+ * Available metrics for AI analytics aggregation and sorting.
3017
+ */
3018
+ exports.AiAnalyticsMetric = void 0;
3019
+ (function (AiAnalyticsMetric) {
3020
+ /** Count of generations */
3021
+ AiAnalyticsMetric["COUNT"] = "count";
3022
+ /** Total tokens (input + output) */
3023
+ AiAnalyticsMetric["TOTAL_TOKENS"] = "totalTokens";
3024
+ /** Input tokens */
3025
+ AiAnalyticsMetric["INPUT_TOKENS"] = "inputTokens";
3026
+ /** Output tokens */
3027
+ AiAnalyticsMetric["OUTPUT_TOKENS"] = "outputTokens";
3028
+ /** Total estimated cost in USD */
3029
+ AiAnalyticsMetric["TOTAL_COST_USD"] = "totalCostUsd";
3030
+ /** Average response time in milliseconds */
3031
+ AiAnalyticsMetric["AVG_RESPONSE_TIME_MS"] = "avgResponseTimeMs";
3032
+ })(exports.AiAnalyticsMetric || (exports.AiAnalyticsMetric = {}));
3033
+ /**
3034
+ * AI Analytics Group By Enum
3035
+ *
3036
+ * Available fields for grouping AI analytics results.
3037
+ */
3038
+ exports.AiAnalyticsGroupBy = void 0;
3039
+ (function (AiAnalyticsGroupBy) {
3040
+ /** Group by day */
3041
+ AiAnalyticsGroupBy["DAY"] = "day";
3042
+ /** Group by week */
3043
+ AiAnalyticsGroupBy["WEEK"] = "week";
3044
+ /** Group by month */
3045
+ AiAnalyticsGroupBy["MONTH"] = "month";
3046
+ /** Group by AI model */
3047
+ AiAnalyticsGroupBy["MODEL"] = "model";
3048
+ /** Group by operation type */
3049
+ AiAnalyticsGroupBy["OPERATION_TYPE"] = "operationType";
3050
+ /** Group by trigger process type */
3051
+ AiAnalyticsGroupBy["TRIGGER_PROCESS_TYPE"] = "triggerProcessType";
3052
+ /** Group by user ID */
3053
+ AiAnalyticsGroupBy["USER_ID"] = "userId";
3054
+ /** Group by success status */
3055
+ AiAnalyticsGroupBy["SUCCESS"] = "success";
3056
+ })(exports.AiAnalyticsGroupBy || (exports.AiAnalyticsGroupBy = {}));
3057
+
3058
+ /**
3059
+ * Webhook-related enums
3060
+ * Single source of truth for webhook types across the application
3061
+ */
3062
+ /**
3063
+ * HTTP methods allowed for webhook forwarding
3064
+ */
3065
+ exports.WebhookMethod = void 0;
3066
+ (function (WebhookMethod) {
3067
+ WebhookMethod["GET"] = "GET";
3068
+ WebhookMethod["POST"] = "POST";
3069
+ WebhookMethod["PUT"] = "PUT";
3070
+ WebhookMethod["PATCH"] = "PATCH";
3071
+ WebhookMethod["DELETE"] = "DELETE";
3072
+ })(exports.WebhookMethod || (exports.WebhookMethod = {}));
3073
+ /**
3074
+ * Authentication types for outbound webhook forwarding
3075
+ */
3076
+ exports.WebhookAuthType = void 0;
3077
+ (function (WebhookAuthType) {
3078
+ WebhookAuthType["NONE"] = "NONE";
3079
+ WebhookAuthType["BEARER"] = "BEARER";
3080
+ WebhookAuthType["JWT"] = "JWT";
3081
+ })(exports.WebhookAuthType || (exports.WebhookAuthType = {}));
3082
+ /**
3083
+ * Sources that can invoke a webhook
3084
+ */
3085
+ exports.WebhookSource = void 0;
3086
+ (function (WebhookSource) {
3087
+ // Internal authenticated sources
3088
+ WebhookSource["USER"] = "USER";
3089
+ WebhookSource["BUSINESS"] = "BUSINESS";
3090
+ WebhookSource["TENANT"] = "TENANT";
3091
+ // External verified sources
3092
+ WebhookSource["STRIPE"] = "STRIPE";
3093
+ // Allow any external traffic (no verification)
3094
+ WebhookSource["ANY"] = "ANY";
3095
+ })(exports.WebhookSource || (exports.WebhookSource = {}));
3096
+ /**
3097
+ * Webhook execution status
3098
+ */
3099
+ exports.WebhookExecutionStatus = void 0;
3100
+ (function (WebhookExecutionStatus) {
3101
+ WebhookExecutionStatus["PENDING"] = "PENDING";
3102
+ WebhookExecutionStatus["SUCCESS"] = "SUCCESS";
3103
+ WebhookExecutionStatus["FAILED"] = "FAILED";
3104
+ WebhookExecutionStatus["BLOCKED"] = "BLOCKED";
3105
+ })(exports.WebhookExecutionStatus || (exports.WebhookExecutionStatus = {}));
3106
+ /**
3107
+ * Webhook callback status (from external workflow engine)
3108
+ */
3109
+ exports.WebhookCallbackStatus = void 0;
3110
+ (function (WebhookCallbackStatus) {
3111
+ /** No callback received yet */
3112
+ WebhookCallbackStatus["NONE"] = "NONE";
3113
+ /** Workflow completed successfully */
3114
+ WebhookCallbackStatus["SUCCESS"] = "SUCCESS";
3115
+ /** Workflow failed */
3116
+ WebhookCallbackStatus["FAILED"] = "FAILED";
3117
+ /** Workflow was cancelled */
3118
+ WebhookCallbackStatus["CANCELLED"] = "CANCELLED";
3119
+ })(exports.WebhookCallbackStatus || (exports.WebhookCallbackStatus = {}));
3120
+
3121
+ /**
3122
+ * Core Entity Types
3123
+ *
3124
+ * Single source of truth for entity type identifiers used across the system.
3125
+ * Other type consts (ErrorDomains, AuditEntityTypes) extend from this base.
3126
+ */
3127
+ const EntityTypes = {
3128
+ // Core entities
3129
+ USER: 'user',
3130
+ ADMIN: 'admin',
3131
+ TENANT: 'tenant',
3132
+ // Token entities
3133
+ TOKEN: 'token',
3134
+ TOKEN_METADATA: 'token_metadata',
3135
+ TOKEN_TYPE: 'token_type',
3136
+ // Financial entities
3137
+ WALLET: 'wallet',
3138
+ BALANCE: 'balance',
3139
+ TRANSACTION: 'transaction',
3140
+ PURCHASE: 'purchase',
3141
+ // Business entities
3142
+ BUSINESS: 'business',
3143
+ BUSINESS_TYPE: 'business_type',
3144
+ // Campaign entities
3145
+ CAMPAIGN: 'campaign',
3146
+ CAMPAIGN_TRIGGER: 'campaign_trigger',
3147
+ // Redemption entities
3148
+ REDEMPTION: 'redemption',
3149
+ REDEMPTION_TYPE: 'redemption_type',
3150
+ // Infrastructure entities
3151
+ CONTRACT: 'contract',
3152
+ SIGNING_ACCOUNT: 'signing_account',
3153
+ API_KEY: 'api_key',
3154
+ WEBHOOK: 'webhook',
3155
+ };
3156
+ /**
3157
+ * Error-specific domains (extends EntityTypes)
3158
+ *
3159
+ * Includes entity types plus error-specific classification domains.
3160
+ */
3161
+ const ErrorDomains = {
3162
+ ...EntityTypes,
3163
+ // Error-specific domains (not entities, but error classifications)
3164
+ VALIDATION: 'validation',
3165
+ SYSTEM: 'system',
3166
+ AUTHENTICATION: 'authentication',
3167
+ EXTERNAL: 'external',
3168
+ };
3169
+ /**
3170
+ * Auditable Entity Types (extends EntityTypes)
3171
+ *
3172
+ * All entities that can be audited. Currently same as EntityTypes,
3173
+ * but can be extended with audit-specific entities if needed.
3174
+ */
3175
+ const AuditEntityTypes = {
3176
+ ...EntityTypes,
3177
+ };
3178
+
2914
3179
  /**
2915
3180
  * Error categories for classification and handling
2916
3181
  *
@@ -2953,6 +3218,62 @@ exports.ErrorCategory = void 0;
2953
3218
  ErrorCategory["UNKNOWN"] = "UNKNOWN";
2954
3219
  })(exports.ErrorCategory || (exports.ErrorCategory = {}));
2955
3220
 
3221
+ /**
3222
+ * PERS WS-Relay Shared Types
3223
+ *
3224
+ * Type-safe WebSocket communication types for the PERS event relay system.
3225
+ *
3226
+ * @version 1.2.0
3227
+ */
3228
+ // =============================================================================
3229
+ // HELPER FUNCTIONS (TYPE GUARDS)
3230
+ // =============================================================================
3231
+ /**
3232
+ * Type guard for server messages
3233
+ */
3234
+ function isServerMessage(data) {
3235
+ return (typeof data === 'object' &&
3236
+ data !== null &&
3237
+ 'type' in data &&
3238
+ 'timestamp' in data);
3239
+ }
3240
+ /**
3241
+ * Type guard for event messages
3242
+ */
3243
+ function isEventMessage(msg) {
3244
+ return msg.type === 'event';
3245
+ }
3246
+ /**
3247
+ * Type guard for connected messages
3248
+ */
3249
+ function isConnectedMessage(msg) {
3250
+ return msg.type === 'connected';
3251
+ }
3252
+ /**
3253
+ * Type guard for subscribed messages
3254
+ */
3255
+ function isSubscribedMessage(msg) {
3256
+ return msg.type === 'subscribed';
3257
+ }
3258
+ /**
3259
+ * Type guard for unsubscribed messages
3260
+ */
3261
+ function isUnsubscribedMessage(msg) {
3262
+ return msg.type === 'unsubscribed';
3263
+ }
3264
+ /**
3265
+ * Type guard for error messages
3266
+ */
3267
+ function isErrorMessage(msg) {
3268
+ return msg.type === 'error';
3269
+ }
3270
+ /**
3271
+ * Type guard for ping messages
3272
+ */
3273
+ function isPingMessage(msg) {
3274
+ return msg.type === 'ping';
3275
+ }
3276
+
2956
3277
  const apiPublicKeyTestPrefix = 'pk_test_';
2957
3278
  const testnetPrefix = 'testnet-';
2958
3279
  const testnetShortPrefix = 'tn-';
@@ -3218,6 +3539,42 @@ function isValidRedemptionRedeemRelation(value) {
3218
3539
  return VALID_REDEMPTION_REDEEM_RELATIONS.includes(value);
3219
3540
  }
3220
3541
 
3542
+ /**
3543
+ * Valid business membership relations for runtime validation
3544
+ */
3545
+ const VALID_BUSINESS_MEMBERSHIP_RELATIONS = [
3546
+ 'user',
3547
+ 'business'
3548
+ ];
3549
+ /**
3550
+ * Type guard to validate business membership relation strings at runtime
3551
+ */
3552
+ function isValidBusinessMembershipRelation(value) {
3553
+ return VALID_BUSINESS_MEMBERSHIP_RELATIONS.includes(value);
3554
+ }
3555
+
3556
+ /**
3557
+ * Valid include relations for User endpoints.
3558
+ * These relations can be requested via ?include=status,balances query parameter.
3559
+ */
3560
+ const VALID_USER_RELATIONS = ['status', 'balances'];
3561
+ /**
3562
+ * Type guard to check if a string is a valid UserIncludeRelation
3563
+ */
3564
+ function isValidUserRelation(value) {
3565
+ return VALID_USER_RELATIONS.includes(value);
3566
+ }
3567
+
3568
+ /**
3569
+ * Chain Data Types
3570
+ *
3571
+ * Types for blockchain chain configuration.
3572
+ */
3573
+ const ChainTypes = {
3574
+ PRIVATE: "PRIVATE",
3575
+ PUBLIC: "PUBLIC"
3576
+ };
3577
+
3221
3578
  /**
3222
3579
  * Transaction format constants for Ethereum and EVM-compatible chains
3223
3580
  * Using const assertions for zero runtime overhead
@@ -3504,6 +3861,281 @@ const ApiErrorDetector = {
3504
3861
  ErrorUtils.isTokenExpiredError(error)
3505
3862
  };
3506
3863
 
3864
+ /**
3865
+ * Centralized Cache Service for PERS SDK
3866
+ * Simple, efficient caching with memory management
3867
+ */
3868
+ const DEFAULT_CACHE_CONFIG = {
3869
+ defaultTtl: 30 * 60 * 1000, // 30 minutes
3870
+ maxMemoryMB: 50, // 50MB cache limit
3871
+ cleanupInterval: 5 * 60 * 1000, // Cleanup every 5 minutes
3872
+ maxEntries: 10000, // Entry count limit
3873
+ evictionBatchPercent: 0.2, // Remove 20% when evicting
3874
+ };
3875
+ // Constants for memory estimation
3876
+ const ENTRY_OVERHEAD_BYTES = 64;
3877
+ const DEFAULT_OBJECT_SIZE_BYTES = 1024;
3878
+ const MEMORY_CHECK_INTERVAL_MS = 60000; // Lazy check every 60s
3879
+ class CacheService {
3880
+ constructor(config) {
3881
+ this.cache = new Map();
3882
+ this.cleanupTimer = null;
3883
+ this.lastMemoryCheck = 0;
3884
+ this.pendingFetches = new Map();
3885
+ this.accessOrder = new Set();
3886
+ this.stats = {
3887
+ hits: 0,
3888
+ misses: 0,
3889
+ errors: 0
3890
+ };
3891
+ this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
3892
+ this.startCleanupTimer();
3893
+ }
3894
+ /**
3895
+ * Set a value in cache with optional TTL
3896
+ */
3897
+ set(key, value, ttl) {
3898
+ if (!key || value === undefined)
3899
+ return;
3900
+ const entry = {
3901
+ value,
3902
+ timestamp: Date.now(),
3903
+ ttl: ttl || this.config.defaultTtl,
3904
+ lastAccessed: Date.now()
3905
+ };
3906
+ this.cache.set(key, entry);
3907
+ this.accessOrder.add(key);
3908
+ // Fast entry count check first
3909
+ if (this.cache.size > this.config.maxEntries) {
3910
+ this.evictOldestEntries();
3911
+ }
3912
+ // Lazy memory check (only every 10s)
3913
+ const now = Date.now();
3914
+ if (now - this.lastMemoryCheck > MEMORY_CHECK_INTERVAL_MS) {
3915
+ this.lastMemoryCheck = now;
3916
+ this.enforceMemoryLimit();
3917
+ }
3918
+ }
3919
+ /**
3920
+ * Get a value from cache
3921
+ */
3922
+ get(key) {
3923
+ const entry = this.cache.get(key);
3924
+ if (!entry) {
3925
+ this.stats.misses++;
3926
+ return null;
3927
+ }
3928
+ // Check if expired
3929
+ const now = Date.now();
3930
+ if (now - entry.timestamp > entry.ttl) {
3931
+ this.cache.delete(key);
3932
+ this.accessOrder.delete(key);
3933
+ this.stats.misses++;
3934
+ return null;
3935
+ }
3936
+ // Update access order efficiently
3937
+ this.accessOrder.delete(key);
3938
+ this.accessOrder.add(key);
3939
+ entry.lastAccessed = now;
3940
+ this.stats.hits++;
3941
+ return entry.value;
3942
+ }
3943
+ /**
3944
+ * Get or set pattern - common caching pattern with race condition protection
3945
+ */
3946
+ async getOrSet(key, fetcher, ttl) {
3947
+ const cached = this.get(key);
3948
+ if (cached !== null)
3949
+ return cached;
3950
+ // Prevent duplicate fetches
3951
+ if (this.pendingFetches.has(key)) {
3952
+ return this.pendingFetches.get(key);
3953
+ }
3954
+ const fetchPromise = fetcher()
3955
+ .then(value => {
3956
+ this.pendingFetches.delete(key);
3957
+ if (value !== undefined) {
3958
+ this.set(key, value, ttl);
3959
+ }
3960
+ return value;
3961
+ })
3962
+ .catch(error => {
3963
+ this.pendingFetches.delete(key);
3964
+ this.stats.errors++;
3965
+ throw error;
3966
+ });
3967
+ this.pendingFetches.set(key, fetchPromise);
3968
+ return fetchPromise;
3969
+ }
3970
+ /**
3971
+ * Delete a specific key
3972
+ */
3973
+ delete(key) {
3974
+ this.accessOrder.delete(key);
3975
+ return this.cache.delete(key);
3976
+ }
3977
+ /**
3978
+ * Clear all keys matching a prefix
3979
+ */
3980
+ clearByPrefix(prefix) {
3981
+ const keysToDelete = Array.from(this.cache.keys()).filter(key => key.startsWith(prefix));
3982
+ keysToDelete.forEach(key => {
3983
+ this.cache.delete(key);
3984
+ this.accessOrder.delete(key);
3985
+ });
3986
+ return keysToDelete.length;
3987
+ }
3988
+ /**
3989
+ * Clear all cache
3990
+ */
3991
+ clear() {
3992
+ this.cache.clear();
3993
+ this.accessOrder.clear();
3994
+ this.pendingFetches.clear();
3995
+ this.stats = { hits: 0, misses: 0, errors: 0 };
3996
+ }
3997
+ /**
3998
+ * Force cleanup of expired entries
3999
+ */
4000
+ cleanup() {
4001
+ const now = Date.now();
4002
+ const keysToDelete = [];
4003
+ for (const [key, entry] of this.cache.entries()) {
4004
+ if (now - entry.timestamp > entry.ttl) {
4005
+ keysToDelete.push(key);
4006
+ }
4007
+ }
4008
+ keysToDelete.forEach(key => {
4009
+ this.cache.delete(key);
4010
+ this.accessOrder.delete(key);
4011
+ });
4012
+ return keysToDelete.length;
4013
+ }
4014
+ /**
4015
+ * Stop cleanup timer and clear cache
4016
+ */
4017
+ destroy() {
4018
+ this.stopCleanupTimer();
4019
+ this.clear();
4020
+ }
4021
+ /**
4022
+ * Create a namespaced cache interface
4023
+ */
4024
+ createNamespace(namespace) {
4025
+ if (!namespace || namespace.includes(':')) {
4026
+ throw new Error('Namespace must be non-empty and cannot contain ":"');
4027
+ }
4028
+ return {
4029
+ set: (key, value, ttl) => this.set(`${namespace}:${key}`, value, ttl),
4030
+ get: (key) => this.get(`${namespace}:${key}`),
4031
+ getOrSet: (key, fetcher, ttl) => this.getOrSet(`${namespace}:${key}`, fetcher, ttl),
4032
+ delete: (key) => this.delete(`${namespace}:${key}`),
4033
+ clear: () => this.clearByPrefix(`${namespace}:`)
4034
+ };
4035
+ }
4036
+ startCleanupTimer() {
4037
+ this.stopCleanupTimer();
4038
+ this.cleanupTimer = setInterval(() => {
4039
+ this.safeCleanup();
4040
+ }, this.config.cleanupInterval);
4041
+ // Platform-agnostic: Prevent hanging process in Node.js environments
4042
+ if (this.cleanupTimer && typeof this.cleanupTimer.unref === 'function') {
4043
+ this.cleanupTimer.unref();
4044
+ }
4045
+ }
4046
+ stopCleanupTimer() {
4047
+ if (this.cleanupTimer) {
4048
+ clearInterval(this.cleanupTimer);
4049
+ this.cleanupTimer = null;
4050
+ }
4051
+ }
4052
+ safeCleanup() {
4053
+ try {
4054
+ this.cleanup();
4055
+ this.enforceMemoryLimit();
4056
+ }
4057
+ catch (error) {
4058
+ this.stats.errors++;
4059
+ // Platform-agnostic error logging
4060
+ if (typeof console !== 'undefined' && console.warn) {
4061
+ console.warn('Cache cleanup error:', error);
4062
+ }
4063
+ }
4064
+ }
4065
+ estimateValueSize(value) {
4066
+ if (typeof value === 'string') {
4067
+ return value.length * 2; // UTF-16 encoding
4068
+ }
4069
+ if (typeof value === 'number' || typeof value === 'boolean') {
4070
+ return 8; // Primitive values
4071
+ }
4072
+ if (value === null || value === undefined) {
4073
+ return 4;
4074
+ }
4075
+ try {
4076
+ // More accurate JSON-based estimation for serializable objects
4077
+ const jsonString = JSON.stringify(value);
4078
+ return jsonString.length * 2;
4079
+ }
4080
+ catch {
4081
+ // Fallback for non-serializable objects
4082
+ if (Array.isArray(value)) {
4083
+ return value.length * 100; // Better estimate for arrays
4084
+ }
4085
+ if (value && typeof value === 'object') {
4086
+ return Object.keys(value).length * 50; // Better estimate for objects
4087
+ }
4088
+ return DEFAULT_OBJECT_SIZE_BYTES;
4089
+ }
4090
+ }
4091
+ estimateMemoryUsage() {
4092
+ let totalSize = 0;
4093
+ for (const [key, entry] of this.cache.entries()) {
4094
+ totalSize += key.length * 2; // Key size
4095
+ totalSize += this.estimateValueSize(entry.value);
4096
+ totalSize += ENTRY_OVERHEAD_BYTES;
4097
+ }
4098
+ return totalSize / (1024 * 1024);
4099
+ }
4100
+ enforceMemoryLimit() {
4101
+ const memoryUsage = this.estimateMemoryUsage();
4102
+ if (memoryUsage <= this.config.maxMemoryMB || this.cache.size === 0)
4103
+ return;
4104
+ this.evictOldestEntries();
4105
+ }
4106
+ evictOldestEntries() {
4107
+ if (this.cache.size === 0)
4108
+ return;
4109
+ const batchSize = Math.floor(this.cache.size * this.config.evictionBatchPercent);
4110
+ const toRemove = Math.max(batchSize, 1);
4111
+ // Efficiently remove oldest entries using access order
4112
+ const iterator = this.accessOrder.values();
4113
+ for (let i = 0; i < toRemove; i++) {
4114
+ const result = iterator.next();
4115
+ if (result.done)
4116
+ break;
4117
+ const key = result.value;
4118
+ this.cache.delete(key);
4119
+ this.accessOrder.delete(key);
4120
+ }
4121
+ }
4122
+ }
4123
+ // Singleton instance
4124
+ const globalCacheService = new CacheService();
4125
+
4126
+ /**
4127
+ * Cache module exports
4128
+ */
4129
+ // Predefined cache TTL constants
4130
+ const CacheTTL = {
4131
+ SHORT: 5 * 60 * 1000, // 5 minutes
4132
+ MEDIUM: 30 * 60 * 1000, // 30 minutes
4133
+ LONG: 24 * 60 * 60 * 1000, // 24 hours
4134
+ METADATA: 24 * 60 * 60 * 1000, // 24 hours - IPFS metadata is immutable
4135
+ GATEWAY: 30 * 60 * 1000, // 30 minutes - Gateway config can change
4136
+ PROVIDER: 60 * 60 * 1000, // 1 hour - Provider connections with auth
4137
+ };
4138
+
3507
4139
  // ==========================================
3508
4140
  // UTILITY FUNCTIONS
3509
4141
  // ==========================================
@@ -3713,9 +4345,15 @@ class UserApi {
3713
4345
  /**
3714
4346
  * AUTH: Get current authenticated user
3715
4347
  * Uses new RESTful /users/me endpoint
4348
+ *
4349
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
3716
4350
  */
3717
- async getRemoteUser() {
3718
- return this.apiClient.get(`${this.basePath}/me`);
4351
+ async getRemoteUser(options) {
4352
+ let url = `${this.basePath}/me`;
4353
+ if (options?.include?.length) {
4354
+ url += `?include=${options.include.join(',')}`;
4355
+ }
4356
+ return this.apiClient.get(url);
3719
4357
  }
3720
4358
  /**
3721
4359
  * AUTH: Update current authenticated user
@@ -3791,19 +4429,66 @@ class UserApi {
3791
4429
  return this.apiClient.put(`${this.basePath}/${id}`, userData);
3792
4430
  }
3793
4431
  /**
3794
- * ADMIN: Toggle user active status
4432
+ * ADMIN: Set or toggle user active status
3795
4433
  * Uses new consistent /users/{id}/status endpoint
3796
4434
  * Enhanced: Follows RESTful status management pattern across all domains
4435
+ *
4436
+ * @param userId - User ID
4437
+ * @param isActive - Optional explicit status. If provided, sets to this value. If omitted, toggles current status.
4438
+ *
4439
+ * @example
4440
+ * ```typescript
4441
+ * // Explicit set
4442
+ * await sdk.users.setUserActiveStatus('user-123', true); // Activate
4443
+ * await sdk.users.setUserActiveStatus('user-123', false); // Deactivate
4444
+ *
4445
+ * // Toggle (current behavior)
4446
+ * await sdk.users.setUserActiveStatus('user-123'); // Flips current status
4447
+ * ```
3797
4448
  */
3798
- async toggleUserActiveStatusByUser(user) {
3799
- return this.apiClient.put(`${this.basePath}/${user.id}/status`, {});
4449
+ async setUserActiveStatus(userId, isActive) {
4450
+ const body = isActive !== undefined ? { isActive } : {};
4451
+ return this.apiClient.put(`${this.basePath}/${userId}/status`, body);
3800
4452
  }
3801
4453
  /**
3802
4454
  * ADMIN: Get user by unique identifier
3803
4455
  * Uses new RESTful /users/{id} endpoint
4456
+ *
4457
+ * @param id - User unique identifier (id, email, externalId, accountAddress, etc.)
4458
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
3804
4459
  */
3805
- async getUserByUniqueIdentifier(id) {
3806
- return this.apiClient.get(`${this.basePath}/${id}`);
4460
+ async getUserByUniqueIdentifier(id, options) {
4461
+ let url = `${this.basePath}/${id}`;
4462
+ if (options?.include?.length) {
4463
+ url += `?include=${options.include.join(',')}`;
4464
+ }
4465
+ return this.apiClient.get(url);
4466
+ }
4467
+ /**
4468
+ * ADMIN: Delete user by identifier (soft delete)
4469
+ * Uses RESTful /users/{identifier} DELETE endpoint
4470
+ *
4471
+ * Soft deletes a user. The user data is retained for 30 days before GDPR anonymization.
4472
+ * Use restoreUser() to restore within the grace period.
4473
+ *
4474
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
4475
+ * @returns Promise resolving to success status and message
4476
+ */
4477
+ async deleteUser(identifier) {
4478
+ return this.apiClient.delete(`${this.basePath}/${identifier}`);
4479
+ }
4480
+ /**
4481
+ * ADMIN: Restore deleted user by identifier
4482
+ * Uses RESTful /users/{identifier}/restore POST endpoint
4483
+ *
4484
+ * Restores a soft-deleted user within the 30-day grace period.
4485
+ * After GDPR anonymization (30 days), restoration is not possible.
4486
+ *
4487
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
4488
+ * @returns Promise resolving to restored user data
4489
+ */
4490
+ async restoreUser(identifier) {
4491
+ return this.apiClient.post(`${this.basePath}/${identifier}/restore`);
3807
4492
  }
3808
4493
  }
3809
4494
 
@@ -3833,9 +4518,11 @@ class UserService {
3833
4518
  // ==========================================
3834
4519
  /**
3835
4520
  * AUTH: Get current authenticated user
4521
+ *
4522
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
3836
4523
  */
3837
- async getRemoteUser() {
3838
- return this.userApi.getRemoteUser();
4524
+ async getRemoteUser(options) {
4525
+ return this.userApi.getRemoteUser(options);
3839
4526
  }
3840
4527
  /**
3841
4528
  * AUTH: Update current authenticated user
@@ -3886,15 +4573,47 @@ class UserService {
3886
4573
  async updateUserAsAdmin(id, userData) {
3887
4574
  return this.userApi.updateUserAsAdmin(id, userData);
3888
4575
  }
3889
- async getUserByUniqueIdentifier(id) {
3890
- return this.userApi.getUserByUniqueIdentifier(id);
4576
+ /**
4577
+ * ADMIN: Get user by unique identifier
4578
+ *
4579
+ * @param id - User unique identifier (id, email, externalId, accountAddress, etc.)
4580
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
4581
+ */
4582
+ async getUserByUniqueIdentifier(id, options) {
4583
+ return this.userApi.getUserByUniqueIdentifier(id, options);
4584
+ }
4585
+ /**
4586
+ * ADMIN: Set or toggle user active status
4587
+ *
4588
+ * @param userId - User ID
4589
+ * @param isActive - Optional explicit status. If provided, sets to this value. If omitted, toggles current status.
4590
+ */
4591
+ async setUserActiveStatus(userId, isActive) {
4592
+ return this.userApi.setUserActiveStatus(userId, isActive);
3891
4593
  }
3892
4594
  /**
3893
- * ADMIN: Toggle user active status by user object
3894
- * ✅ FIXED: Matches API method signature exactly
4595
+ * ADMIN: Delete user by identifier (soft delete)
4596
+ *
4597
+ * Soft deletes a user. The user data is retained for 30 days before GDPR anonymization.
4598
+ * Use restoreUser() to restore within the grace period.
4599
+ *
4600
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
4601
+ * @returns Promise resolving to success status and message
3895
4602
  */
3896
- async toggleUserActiveStatusByUser(user) {
3897
- return this.userApi.toggleUserActiveStatusByUser(user);
4603
+ async deleteUser(identifier) {
4604
+ return this.userApi.deleteUser(identifier);
4605
+ }
4606
+ /**
4607
+ * ADMIN: Restore deleted user by identifier
4608
+ *
4609
+ * Restores a soft-deleted user within the 30-day grace period.
4610
+ * After GDPR anonymization (30 days), restoration is not possible.
4611
+ *
4612
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
4613
+ * @returns Promise resolving to restored user data
4614
+ */
4615
+ async restoreUser(identifier) {
4616
+ return this.userApi.restoreUser(identifier);
3898
4617
  }
3899
4618
  }
3900
4619
 
@@ -3973,6 +4692,32 @@ class UserStatusApi {
3973
4692
  throw error;
3974
4693
  }
3975
4694
  }
4695
+ /**
4696
+ * ADMIN: Update user status type
4697
+ * PUT /users/status-types/:id
4698
+ */
4699
+ async updateUserStatusType(id, userStatusType) {
4700
+ try {
4701
+ return await this.apiClient.put(`${this.basePath}/status-types/${id}`, userStatusType);
4702
+ }
4703
+ catch (error) {
4704
+ console.error('Error updating user status type', error);
4705
+ throw error;
4706
+ }
4707
+ }
4708
+ /**
4709
+ * ADMIN: Delete user status type
4710
+ * DELETE /users/status-types/:id
4711
+ */
4712
+ async deleteUserStatusType(id) {
4713
+ try {
4714
+ await this.apiClient.delete(`${this.basePath}/status-types/${id}`);
4715
+ }
4716
+ catch (error) {
4717
+ console.error('Error deleting user status type', error);
4718
+ throw error;
4719
+ }
4720
+ }
3976
4721
  }
3977
4722
 
3978
4723
  /**
@@ -4013,6 +4758,18 @@ class UserStatusService {
4013
4758
  async createUserStatusType(userStatusType) {
4014
4759
  return this.userStatusApi.createUserStatusType(userStatusType);
4015
4760
  }
4761
+ /**
4762
+ * ADMIN: Update user status type
4763
+ */
4764
+ async updateUserStatusType(id, userStatusType) {
4765
+ return this.userStatusApi.updateUserStatusType(id, userStatusType);
4766
+ }
4767
+ /**
4768
+ * ADMIN: Delete user status type
4769
+ */
4770
+ async deleteUserStatusType(id) {
4771
+ return this.userStatusApi.deleteUserStatusType(id);
4772
+ }
4016
4773
  }
4017
4774
 
4018
4775
  /**
@@ -4040,6 +4797,8 @@ function createUserStatusSDK(apiClient) {
4040
4797
  getRemoteEarnedUserStatus: (options) => userStatusService.getRemoteEarnedUserStatus(options),
4041
4798
  // Admin methods
4042
4799
  createUserStatusType: (userStatusType) => userStatusService.createUserStatusType(userStatusType),
4800
+ updateUserStatusType: (id, userStatusType) => userStatusService.updateUserStatusType(id, userStatusType),
4801
+ deleteUserStatusType: (id) => userStatusService.deleteUserStatusType(id),
4043
4802
  // Advanced access for edge cases
4044
4803
  api: userStatusApi,
4045
4804
  service: userStatusService
@@ -4166,6 +4925,13 @@ class TokenApi {
4166
4925
  async createTokenMetadata(tokenId, tokenData) {
4167
4926
  return this.apiClient.post(`${this.basePath}/${tokenId}/metadata`, tokenData);
4168
4927
  }
4928
+ /**
4929
+ * ADMIN: Update token metadata (ERC721 only)
4930
+ * Note: Existing minted NFTs retain their original metadata - this only affects future mints
4931
+ */
4932
+ async updateTokenMetadata(metadataId, tokenData) {
4933
+ return this.apiClient.put(`${this.basePath}/metadata/${metadataId}`, tokenData);
4934
+ }
4169
4935
  /**
4170
4936
  * ADMIN: Toggle token metadata status (separate from token status)
4171
4937
  */
@@ -4240,6 +5006,13 @@ class TokenService {
4240
5006
  async createTokenMetadata(tokenId, tokenData) {
4241
5007
  return this.tokenApi.createTokenMetadata(tokenId, tokenData);
4242
5008
  }
5009
+ /**
5010
+ * ADMIN: Update token metadata (ERC721 only)
5011
+ * Note: Existing minted NFTs retain their original metadata - this only affects future mints
5012
+ */
5013
+ async updateTokenMetadata(metadataId, tokenData) {
5014
+ return this.tokenApi.updateTokenMetadata(metadataId, tokenData);
5015
+ }
4243
5016
  /**
4244
5017
  * ADMIN: Toggle token active status
4245
5018
  */
@@ -4508,7 +5281,7 @@ class BusinessMembershipApi {
4508
5281
  * Min Role: VIEWER (any member can view)
4509
5282
  *
4510
5283
  * @param businessId - The business UUID
4511
- * @param options - Pagination and filter options
5284
+ * @param options - Pagination, filter, and include options
4512
5285
  * @returns Paginated array of business memberships with user and role details
4513
5286
  *
4514
5287
  * @throws {AuthenticationError} 401 - Not authenticated
@@ -4520,15 +5293,19 @@ class BusinessMembershipApi {
4520
5293
  * const page1 = await membershipApi.getMembers('business-123');
4521
5294
  * page1.data.forEach(m => console.log(`${m.userId}: ${m.role}`));
4522
5295
  *
4523
- * // Filter by role
4524
- * const admins = await membershipApi.getMembers('business-123', {
4525
- * role: MembershipRole.ADMIN,
5296
+ * // Include user data for display
5297
+ * const withUsers = await membershipApi.getMembers('business-123', {
5298
+ * include: ['user'],
4526
5299
  * page: 1,
4527
5300
  * limit: 50
4528
5301
  * });
5302
+ * withUsers.data.forEach(m => console.log(m.included?.user?.email));
4529
5303
  *
4530
- * // Paginate through all members
4531
- * const page2 = await membershipApi.getMembers('business-123', { page: 2 });
5304
+ * // Filter by role with user data
5305
+ * const admins = await membershipApi.getMembers('business-123', {
5306
+ * role: MembershipRole.ADMIN,
5307
+ * include: ['user']
5308
+ * });
4532
5309
  * ```
4533
5310
  */
4534
5311
  async getMembers(businessId, options) {
@@ -4536,6 +5313,9 @@ class BusinessMembershipApi {
4536
5313
  if (options?.role) {
4537
5314
  params.set('role', options.role);
4538
5315
  }
5316
+ if (options?.include?.length) {
5317
+ params.set('include', options.include.join(','));
5318
+ }
4539
5319
  const response = await this.apiClient.get(`${this.getMembersPath(businessId)}?${params.toString()}`);
4540
5320
  return normalizeToPaginated(response);
4541
5321
  }
@@ -4795,8 +5575,15 @@ class BusinessMembershipService {
4795
5575
  * Get all members of a business with pagination
4796
5576
  *
4797
5577
  * @param businessId - The business UUID
4798
- * @param options - Pagination options
5578
+ * @param options - Pagination and include options
4799
5579
  * @returns Paginated response with business memberships
5580
+ *
5581
+ * @example
5582
+ * ```typescript
5583
+ * // Get members with user data for display
5584
+ * const members = await service.getMembers('biz-123', { include: ['user'] });
5585
+ * members.data.forEach(m => console.log(m.included?.user?.email));
5586
+ * ```
4800
5587
  */
4801
5588
  async getMembers(businessId, options) {
4802
5589
  return this.membershipApi.getMembers(businessId, options);
@@ -6346,7 +7133,9 @@ class TransactionService {
6346
7133
  return this.transactionApi.getTransactionAnalytics(analyticsRequest);
6347
7134
  }
6348
7135
  }
6349
-
7136
+ // ============================================================================
7137
+ // CLIENT TRANSACTION TYPES
7138
+ // ============================================================================
6350
7139
  /**
6351
7140
  * Client-side transaction types extending backend Web3TransactionType
6352
7141
  * Includes client-specific flows like pending submissions (POS flow)
@@ -7054,6 +7843,156 @@ class TenantService {
7054
7843
  }
7055
7844
  }
7056
7845
 
7846
+ /**
7847
+ * Tenant Manager - Clean, high-level interface for tenant operations
7848
+ *
7849
+ * Provides a simplified API for common tenant management tasks while maintaining
7850
+ * access to the full tenant SDK for advanced use cases.
7851
+ *
7852
+ * Also provides chain-agnostic IPFS URL resolution using tenant configuration.
7853
+ */
7854
+ class TenantManager {
7855
+ constructor(apiClient) {
7856
+ this.apiClient = apiClient;
7857
+ this.cache = globalCacheService.createNamespace('tenant');
7858
+ const tenantApi = new TenantApi(apiClient);
7859
+ this.tenantService = new TenantService(tenantApi);
7860
+ }
7861
+ /**
7862
+ * Get current tenant information
7863
+ *
7864
+ * @returns Promise resolving to tenant data
7865
+ */
7866
+ async getTenantInfo() {
7867
+ return this.tenantService.getRemoteTenant();
7868
+ }
7869
+ /**
7870
+ * Get tenant login token
7871
+ *
7872
+ * @returns Promise resolving to login token
7873
+ */
7874
+ async getLoginToken() {
7875
+ return this.tenantService.getRemoteLoginToken();
7876
+ }
7877
+ /**
7878
+ * Get tenant client configuration (cached)
7879
+ *
7880
+ * @returns Promise resolving to client config
7881
+ */
7882
+ async getClientConfig() {
7883
+ return this.cache.getOrSet('clientConfig', () => this.tenantService.getRemoteClientConfig(), CacheTTL.LONG);
7884
+ }
7885
+ // ==========================================
7886
+ // IPFS OPERATIONS (Chain-agnostic)
7887
+ // ==========================================
7888
+ /**
7889
+ * Resolve IPFS URL to HTTPS URL using tenant's configured gateway.
7890
+ *
7891
+ * This is chain-agnostic - IPFS gateway is configured at the tenant level,
7892
+ * not per-chain. Use this for resolving any ipfs:// URLs (images, metadata, etc.).
7893
+ *
7894
+ * @param url - URL to resolve (can be ipfs:// or https://)
7895
+ * @returns Resolved HTTPS URL
7896
+ * @throws Error if tenant's ipfsGatewayDomain is not configured
7897
+ *
7898
+ * @example
7899
+ * ```typescript
7900
+ * const imageUrl = await sdk.tenant.resolveIPFSUrl('ipfs://QmXxx.../image.png');
7901
+ * // Returns: 'https://pers.mypinata.cloud/ipfs/QmXxx.../image.png'
7902
+ *
7903
+ * // Non-IPFS URLs pass through unchanged
7904
+ * const httpUrl = await sdk.tenant.resolveIPFSUrl('https://example.com/image.png');
7905
+ * // Returns: 'https://example.com/image.png'
7906
+ * ```
7907
+ */
7908
+ async resolveIPFSUrl(url) {
7909
+ if (!url || !url.startsWith('ipfs://')) {
7910
+ return url;
7911
+ }
7912
+ const gateway = await this.getIpfsGatewayDomain();
7913
+ return url.replace('ipfs://', `https://${gateway}/ipfs/`);
7914
+ }
7915
+ /**
7916
+ * Get IPFS gateway domain from tenant configuration (cached).
7917
+ *
7918
+ * @returns IPFS gateway domain (e.g., 'pers.mypinata.cloud')
7919
+ * @throws Error if ipfsGatewayDomain is not configured for this tenant
7920
+ */
7921
+ async getIpfsGatewayDomain() {
7922
+ return this.cache.getOrSet('ipfsGateway', async () => {
7923
+ const config = await this.getClientConfig();
7924
+ if (!config.ipfsGatewayDomain) {
7925
+ throw new Error('IPFS gateway domain not configured for tenant. ' +
7926
+ 'Please configure ipfsGatewayDomain in tenant settings.');
7927
+ }
7928
+ return config.ipfsGatewayDomain;
7929
+ }, CacheTTL.GATEWAY);
7930
+ }
7931
+ /**
7932
+ * Get Google API key from tenant configuration (cached).
7933
+ *
7934
+ * @returns Google API key or undefined if not configured
7935
+ */
7936
+ async getGoogleApiKey() {
7937
+ const config = await this.getClientConfig();
7938
+ return config.googleApiKey;
7939
+ }
7940
+ // ==========================================
7941
+ // ADMIN OPERATIONS
7942
+ // ==========================================
7943
+ /**
7944
+ * Admin: Update tenant data
7945
+ *
7946
+ * @param tenantData - Updated tenant data
7947
+ * @returns Promise resolving to updated tenant
7948
+ */
7949
+ async updateTenant(tenantData) {
7950
+ return this.tenantService.updateRemoteTenant(tenantData);
7951
+ }
7952
+ /**
7953
+ * Admin: Get all admins
7954
+ *
7955
+ * @param options - Pagination options
7956
+ * @returns Promise resolving to paginated admins
7957
+ */
7958
+ async getAdmins(options) {
7959
+ return this.tenantService.getAdmins(options);
7960
+ }
7961
+ /**
7962
+ * Admin: Create new admin
7963
+ *
7964
+ * @param adminData - Admin data
7965
+ * @returns Promise resolving to created admin
7966
+ */
7967
+ async createAdmin(adminData) {
7968
+ return this.tenantService.postAdmin(adminData);
7969
+ }
7970
+ /**
7971
+ * Admin: Update existing admin
7972
+ *
7973
+ * @param adminId - ID of the admin to update
7974
+ * @param adminData - Updated admin data
7975
+ * @returns Promise resolving to updated admin
7976
+ */
7977
+ async updateAdmin(adminId, adminData) {
7978
+ return this.tenantService.putAdmin(adminId, adminData);
7979
+ }
7980
+ /**
7981
+ * Get the tenant service for advanced operations
7982
+ *
7983
+ * @returns TenantService instance
7984
+ */
7985
+ getTenantService() {
7986
+ return this.tenantService;
7987
+ }
7988
+ /**
7989
+ * Clear tenant cache (useful after config changes)
7990
+ */
7991
+ clearCache() {
7992
+ this.cache.clear();
7993
+ }
7994
+ }
7995
+
7057
7996
  /**
7058
7997
  * Platform-Agnostic Analytics API Client
7059
7998
  *
@@ -7257,6 +8196,57 @@ class AnalyticsApi {
7257
8196
  async getRetentionAnalytics(request = {}) {
7258
8197
  return this.apiClient.post('/analytics/users/retention', request);
7259
8198
  }
8199
+ // ==========================================
8200
+ // TAG ANALYTICS
8201
+ // ==========================================
8202
+ /**
8203
+ * ADMIN: Get tag usage analytics across entities
8204
+ *
8205
+ * Aggregates tag usage across all taggable entities (campaigns, redemptions,
8206
+ * businesses, token metadata, user status types). Perfect for:
8207
+ * - Tag autocomplete/suggestions
8208
+ * - Tag management dashboards
8209
+ * - Usage analytics
8210
+ *
8211
+ * @param request - Optional filters for entity types
8212
+ * @returns Aggregated tag usage with per-entity-type breakdown
8213
+ *
8214
+ * @example Get all tags across all entities
8215
+ * ```typescript
8216
+ * const allTags = await analyticsApi.getTagAnalytics();
8217
+ * console.log(`${allTags.totalTags} unique tags, ${allTags.totalUsage} total usages`);
8218
+ *
8219
+ * // Use for autocomplete suggestions
8220
+ * const suggestions = allTags.tags.map(t => t.tag);
8221
+ * ```
8222
+ *
8223
+ * @example Get tags only from campaigns and businesses
8224
+ * ```typescript
8225
+ * const filteredTags = await analyticsApi.getTagAnalytics({
8226
+ * entityTypes: [TaggableEntityType.CAMPAIGN, TaggableEntityType.BUSINESS]
8227
+ * });
8228
+ * ```
8229
+ *
8230
+ * @example Tag usage breakdown
8231
+ * ```typescript
8232
+ * const analytics = await analyticsApi.getTagAnalytics();
8233
+ * analytics.tags.forEach(({ tag, count, usageByEntityType }) => {
8234
+ * console.log(`${tag}: ${count} total`);
8235
+ * usageByEntityType.forEach(({ entityType, count }) => {
8236
+ * console.log(` - ${entityType}: ${count}`);
8237
+ * });
8238
+ * });
8239
+ * ```
8240
+ */
8241
+ async getTagAnalytics(request = {}) {
8242
+ const params = new URLSearchParams();
8243
+ if (request.entityTypes?.length) {
8244
+ params.set('entityTypes', request.entityTypes.join(','));
8245
+ }
8246
+ const queryString = params.toString();
8247
+ const url = queryString ? `/analytics/tags?${queryString}` : '/analytics/tags';
8248
+ return this.apiClient.get(url);
8249
+ }
7260
8250
  }
7261
8251
 
7262
8252
  /**
@@ -7321,6 +8311,18 @@ class AnalyticsService {
7321
8311
  async getRetentionAnalytics(request = {}) {
7322
8312
  return this.analyticsApi.getRetentionAnalytics(request);
7323
8313
  }
8314
+ // ==========================================
8315
+ // TAG ANALYTICS
8316
+ // ==========================================
8317
+ /**
8318
+ * ADMIN: Get tag usage analytics across entities
8319
+ *
8320
+ * Aggregates tag usage across all taggable entities for autocomplete,
8321
+ * tag management dashboards, and usage analytics.
8322
+ */
8323
+ async getTagAnalytics(request = {}) {
8324
+ return this.analyticsApi.getTagAnalytics(request);
8325
+ }
7324
8326
  }
7325
8327
 
7326
8328
  /**
@@ -7523,19 +8525,40 @@ const DEFAULT_PERS_CONFIG = {
7523
8525
  timeout: 30000,
7524
8526
  retries: 3,
7525
8527
  tokenRefreshMargin: 60, // Refresh tokens 60 seconds before expiry
7526
- backgroundRefreshThreshold: 30 // Use background refresh if >30s remaining
8528
+ backgroundRefreshThreshold: 30, // Use background refresh if >30s remaining
8529
+ captureWalletEvents: true // Auto-connect to wallet events after auth
7527
8530
  };
7528
8531
  /**
7529
- * Internal function to construct API root from environment
7530
- * Now defaults to production and v2
8532
+ * Build the API root URL based on config
8533
+ *
8534
+ * Priority:
8535
+ * 1. customApiUrl (if provided)
8536
+ * 2. Environment-based URL (staging/production)
8537
+ *
8538
+ * @internal
7531
8539
  */
7532
- function buildApiRoot(environment = 'production', version = 'v2') {
8540
+ function buildApiRoot(environment = 'production', version = 'v2', customApiUrl) {
8541
+ // Custom URL takes priority
8542
+ if (customApiUrl) {
8543
+ return customApiUrl;
8544
+ }
7533
8545
  const baseUrls = {
7534
- development: 'https://explorins-loyalty.ngrok.io',
7535
8546
  staging: `https://dev.api.pers.ninja/${version}`,
7536
8547
  production: `https://api.pers.ninja/${version}`
7537
8548
  };
7538
- return `${baseUrls[environment]}`;
8549
+ return baseUrls[environment];
8550
+ }
8551
+ /**
8552
+ * Build wallet events WebSocket URL based on config
8553
+ *
8554
+ * @internal
8555
+ */
8556
+ function buildWalletEventsWsUrl(environment = 'production', customWsUrl) {
8557
+ const wsUrls = {
8558
+ staging: 'wss://events-staging.pers.ninja',
8559
+ production: 'wss://events.pers.ninja'
8560
+ };
8561
+ return wsUrls[environment];
7539
8562
  }
7540
8563
  /**
7541
8564
  * Merge user config with defaults
@@ -8454,8 +9477,8 @@ class PersApiClient {
8454
9477
  this.initializationPromise = null;
8455
9478
  // Merge user config with defaults (production + v2)
8456
9479
  this.mergedConfig = mergeWithDefaults(config);
8457
- // Build API root from merged environment and version
8458
- this.apiRoot = buildApiRoot(this.mergedConfig.environment, this.mergedConfig.apiVersion);
9480
+ // Build API root - customApiUrl takes priority over environment
9481
+ this.apiRoot = buildApiRoot(this.mergedConfig.environment, this.mergedConfig.apiVersion, this.mergedConfig.customApiUrl);
8459
9482
  // Initialize auth services for direct authentication
8460
9483
  this.authApi = new AuthApi(this);
8461
9484
  // Auto-create auth provider if none provided
@@ -8795,7 +9818,6 @@ class PersEventEmitter {
8795
9818
  constructor() {
8796
9819
  this.handlers = new Set();
8797
9820
  this._instanceId = ++emitterInstanceCounter;
8798
- console.log(`[PersEventEmitter] Instance #${this._instanceId} created`);
8799
9821
  }
8800
9822
  get instanceId() {
8801
9823
  return this._instanceId;
@@ -9340,23 +10362,32 @@ class UserManager {
9340
10362
  * Retrieves the complete profile of the currently authenticated user.
9341
10363
  * Requires valid authentication tokens.
9342
10364
  *
10365
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
9343
10366
  * @returns Promise resolving to current user data with full profile information
9344
10367
  * @throws {PersApiError} When user is not authenticated
9345
10368
  *
9346
- * @example
10369
+ * @example Basic Usage
9347
10370
  * ```typescript
9348
10371
  * try {
9349
10372
  * const user = await sdk.users.getCurrentUser();
9350
- * console.log('Current user:', user.name, user.email);
9351
- * console.log('User ID:', user.id);
9352
- * console.log('Registration date:', user.createdAt);
10373
+ * console.log('Current user:', user.firstName, user.email);
9353
10374
  * } catch (error) {
9354
10375
  * console.log('User not authenticated');
9355
10376
  * }
9356
10377
  * ```
10378
+ *
10379
+ * @example With Status Types and Balances
10380
+ * ```typescript
10381
+ * // Include status types and token balances
10382
+ * const user = await sdk.users.getCurrentUser({
10383
+ * include: ['status', 'balances']
10384
+ * });
10385
+ * console.log('Status types:', user.included?.statusTypes);
10386
+ * console.log('Token balances:', user.included?.tokenBalances);
10387
+ * ```
9357
10388
  */
9358
- async getCurrentUser() {
9359
- return this.userService.getRemoteUser();
10389
+ async getCurrentUser(options) {
10390
+ return this.userService.getRemoteUser(options);
9360
10391
  }
9361
10392
  /**
9362
10393
  * Update current user profile
@@ -9402,25 +10433,34 @@ class UserManager {
9402
10433
  * requires appropriate permissions (admin or specific access rights).
9403
10434
  *
9404
10435
  * @param identifier - Unique identifier for the user (user ID, email, etc.)
10436
+ * @param options - Query options. Use `include` to specify relations: 'status' (user status types), 'balances' (token balances)
9405
10437
  * @returns Promise resolving to user data
9406
10438
  * @throws {PersApiError} When user not found or insufficient permissions
9407
10439
  *
9408
- * @example
10440
+ * @example Basic Usage
9409
10441
  * ```typescript
9410
10442
  * try {
9411
10443
  * const user = await sdk.users.getUserById('user-123');
9412
- * console.log('Found user:', user.name);
10444
+ * console.log('Found user:', user.firstName);
9413
10445
  * } catch (error) {
9414
10446
  * if (error.statusCode === 404) {
9415
10447
  * console.log('User not found');
9416
- * } else if (error.statusCode === 403) {
9417
- * console.log('Access denied');
9418
10448
  * }
9419
10449
  * }
9420
10450
  * ```
10451
+ *
10452
+ * @example With Status Types and Balances
10453
+ * ```typescript
10454
+ * // Get user with status types and token balances
10455
+ * const user = await sdk.users.getUserById('user-123', {
10456
+ * include: ['status', 'balances']
10457
+ * });
10458
+ * console.log('Status types:', user.included?.statusTypes);
10459
+ * console.log('Token balances:', user.included?.tokenBalances);
10460
+ * ```
9421
10461
  */
9422
- async getUserById(identifier) {
9423
- return this.userService.getUserByUniqueIdentifier(identifier);
10462
+ async getUserById(identifier, options) {
10463
+ return this.userService.getUserByUniqueIdentifier(identifier, options);
9424
10464
  }
9425
10465
  /**
9426
10466
  * Get all users public profiles with optional filtering
@@ -9600,34 +10640,104 @@ class UserManager {
9600
10640
  return this.userService.updateUserAsAdmin(userId, userData);
9601
10641
  }
9602
10642
  /**
9603
- * Admin: Toggle user active status
10643
+ * Admin: Set or toggle user active status
9604
10644
  *
9605
- * Toggles the active/inactive status of a user account. This is typically
9606
- * used for account suspension or reactivation. Requires administrator privileges.
10645
+ * Sets the active/inactive status of a user account explicitly, or toggles
10646
+ * if no explicit value is provided. This is typically used for account
10647
+ * suspension or reactivation. Requires administrator privileges.
9607
10648
  *
9608
- * @param user - User object to toggle status for
10649
+ * @param userId - User ID to update
10650
+ * @param isActive - Optional explicit status. If provided, sets to this value. If omitted, toggles current status.
9609
10651
  * @returns Promise resolving to updated user data
9610
10652
  * @throws {PersApiError} When not authenticated as admin
9611
10653
  *
9612
- * @example Suspend User
10654
+ * @example Explicit Status
9613
10655
  * ```typescript
9614
- * // Admin operation - toggle user status
10656
+ * // Admin operation - explicitly activate user
10657
+ * const activated = await sdk.users.setUserActiveStatus('user-123', true);
10658
+ * console.log('User activated:', activated.isActive); // true
10659
+ *
10660
+ * // Explicitly deactivate user
10661
+ * const deactivated = await sdk.users.setUserActiveStatus('user-123', false);
10662
+ * console.log('User deactivated:', deactivated.isActive); // false
10663
+ * ```
10664
+ *
10665
+ * @example Toggle Status
10666
+ * ```typescript
10667
+ * // Admin operation - toggle current status
10668
+ * const toggled = await sdk.users.setUserActiveStatus('user-123');
10669
+ * console.log('User status toggled:', toggled.isActive);
10670
+ * ```
10671
+ */
10672
+ async setUserActiveStatus(userId, isActive) {
10673
+ return this.userService.setUserActiveStatus(userId, isActive);
10674
+ }
10675
+ /**
10676
+ * Admin: Delete user (soft delete)
10677
+ *
10678
+ * Soft deletes a user account. The user data is retained for 30 days before
10679
+ * GDPR anonymization. Use restoreUser() to restore within the grace period.
10680
+ *
10681
+ * ⚠️ This operation is irreversible after 30 days. Consider using toggleUserStatus()
10682
+ * for temporary deactivation instead.
10683
+ *
10684
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
10685
+ * @returns Promise resolving to success status and message
10686
+ * @throws {PersApiError} When not authenticated as admin or user not found
10687
+ *
10688
+ * @example Delete User
10689
+ * ```typescript
10690
+ * // Admin operation - soft delete a user
9615
10691
  * try {
9616
- * const user = await sdk.users.getUserById('problematic-user-123');
9617
- * const updated = await sdk.users.toggleUserStatus(user);
10692
+ * const result = await sdk.users.deleteUser('user-123');
10693
+ * console.log(result.message); // "User user-123 has been deleted"
10694
+ * } catch (error) {
10695
+ * console.log('Failed to delete user:', error.message);
10696
+ * }
10697
+ * ```
10698
+ */
10699
+ async deleteUser(identifier) {
10700
+ const result = await this.userService.deleteUser(identifier);
10701
+ this.events?.emitSuccess({
10702
+ domain: 'user',
10703
+ type: 'USER_DELETED',
10704
+ userMessage: 'User deleted successfully',
10705
+ details: { identifier }
10706
+ });
10707
+ return result;
10708
+ }
10709
+ /**
10710
+ * Admin: Restore deleted user
9618
10711
  *
9619
- * if (updated.isActive) {
9620
- * console.log('User account reactivated');
9621
- * } else {
9622
- * console.log('User account suspended');
9623
- * }
10712
+ * Restores a soft-deleted user within the 30-day grace period.
10713
+ * After GDPR anonymization (30 days), restoration is not possible.
10714
+ *
10715
+ * @param identifier - User unique identifier (id, email, externalId, accountAddress, etc.)
10716
+ * @returns Promise resolving to restored user data
10717
+ * @throws {PersApiError} When not authenticated as admin, user not found, or already anonymized
10718
+ *
10719
+ * @example Restore Deleted User
10720
+ * ```typescript
10721
+ * // Admin operation - restore a deleted user
10722
+ * try {
10723
+ * const user = await sdk.users.restoreUser('user-123');
10724
+ * console.log('User restored:', user.firstName);
9624
10725
  * } catch (error) {
9625
- * console.log('Failed to update user status');
10726
+ * if (error.message.includes('anonymized')) {
10727
+ * console.log('User cannot be restored - already anonymized');
10728
+ * }
9626
10729
  * }
9627
10730
  * ```
9628
10731
  */
9629
- async toggleUserStatus(user) {
9630
- return this.userService.toggleUserActiveStatusByUser(user);
10732
+ async restoreUser(identifier) {
10733
+ const result = await this.userService.restoreUser(identifier);
10734
+ this.events?.emitSuccess({
10735
+ domain: 'user',
10736
+ type: 'USER_RESTORED',
10737
+ userMessage: 'User restored successfully',
10738
+ details: { identifier, userId: result.id }
10739
+ });
10740
+ return result;
9631
10741
  }
9632
10742
  /**
9633
10743
  * Get the user service for advanced operations
@@ -9686,6 +10796,25 @@ class UserStatusManager {
9686
10796
  async createUserStatusType(userStatusType) {
9687
10797
  return this.userStatusSDK.createUserStatusType(userStatusType);
9688
10798
  }
10799
+ /**
10800
+ * Admin: Update existing user status type
10801
+ *
10802
+ * @param id - User status type ID
10803
+ * @param userStatusType - User status type data
10804
+ * @returns Promise resolving to updated user status type
10805
+ */
10806
+ async updateUserStatusType(id, userStatusType) {
10807
+ return this.userStatusSDK.updateUserStatusType(id, userStatusType);
10808
+ }
10809
+ /**
10810
+ * Admin: Delete user status type
10811
+ *
10812
+ * @param id - User status type ID
10813
+ * @returns Promise resolving when deleted
10814
+ */
10815
+ async deleteUserStatusType(id) {
10816
+ return this.userStatusSDK.deleteUserStatusType(id);
10817
+ }
9689
10818
  /**
9690
10819
  * Get the full user status SDK for advanced operations
9691
10820
  *
@@ -10401,22 +11530,23 @@ class BusinessManager {
10401
11530
  * Any member of the business can view the member list.
10402
11531
  *
10403
11532
  * @param businessId - The business UUID
10404
- * @returns Promise resolving to array of business memberships
11533
+ * @param options - Pagination and include options
11534
+ * @returns Promise resolving to paginated business memberships
10405
11535
  * @throws {PersApiError} 401 - Not authenticated
10406
11536
  * @throws {PersApiError} 403 - Not a member of this business
10407
11537
  *
10408
11538
  * @example
10409
11539
  * ```typescript
10410
- * const members = await sdk.business.getMembers('business-123');
11540
+ * // Get members with user data for display
11541
+ * const { data: members } = await sdk.business.getMembers('business-123', {
11542
+ * include: ['user']
11543
+ * });
10411
11544
  *
10412
11545
  * console.log('Business Members:');
10413
11546
  * members.forEach(member => {
10414
- * console.log(`- User ${member.userId}: ${member.role}`);
11547
+ * const email = member.included?.user?.email || member.userId;
11548
+ * console.log(`- ${email}: ${member.role}`);
10415
11549
  * });
10416
- *
10417
- * // Count members by role
10418
- * const admins = members.filter(m => m.role === 'ADMIN' || m.role === 'OWNER');
10419
- * console.log(`Administrators: ${admins.length}`);
10420
11550
  * ```
10421
11551
  */
10422
11552
  async getMembers(businessId, options) {
@@ -13656,89 +14786,6 @@ class FileManager {
13656
14786
  }
13657
14787
  }
13658
14788
 
13659
- /**
13660
- * Tenant Manager - Clean, high-level interface for tenant operations
13661
- *
13662
- * Provides a simplified API for common tenant management tasks while maintaining
13663
- * access to the full tenant SDK for advanced use cases.
13664
- */
13665
- class TenantManager {
13666
- constructor(apiClient) {
13667
- this.apiClient = apiClient;
13668
- const tenantApi = new TenantApi(apiClient);
13669
- this.tenantService = new TenantService(tenantApi);
13670
- }
13671
- /**
13672
- * Get current tenant information
13673
- *
13674
- * @returns Promise resolving to tenant data
13675
- */
13676
- async getTenantInfo() {
13677
- return this.tenantService.getRemoteTenant();
13678
- }
13679
- /**
13680
- * Get tenant login token
13681
- *
13682
- * @returns Promise resolving to login token
13683
- */
13684
- async getLoginToken() {
13685
- return this.tenantService.getRemoteLoginToken();
13686
- }
13687
- /**
13688
- * Get tenant client configuration
13689
- *
13690
- * @returns Promise resolving to client config
13691
- */
13692
- async getClientConfig() {
13693
- return this.tenantService.getRemoteClientConfig();
13694
- }
13695
- /**
13696
- * Admin: Update tenant data
13697
- *
13698
- * @param tenantData - Updated tenant data
13699
- * @returns Promise resolving to updated tenant
13700
- */
13701
- async updateTenant(tenantData) {
13702
- return this.tenantService.updateRemoteTenant(tenantData);
13703
- }
13704
- /**
13705
- * Admin: Get all admins
13706
- *
13707
- * @param options - Pagination options
13708
- * @returns Promise resolving to paginated admins
13709
- */
13710
- async getAdmins(options) {
13711
- return this.tenantService.getAdmins(options);
13712
- }
13713
- /**
13714
- * Admin: Create new admin
13715
- *
13716
- * @param adminData - Admin data
13717
- * @returns Promise resolving to created admin
13718
- */
13719
- async createAdmin(adminData) {
13720
- return this.tenantService.postAdmin(adminData);
13721
- }
13722
- /**
13723
- * Admin: Update existing admin
13724
- *
13725
- * @param adminId - ID of the admin to update
13726
- * @param adminData - Updated admin data
13727
- * @returns Promise resolving to updated admin
13728
- */
13729
- async updateAdmin(adminId, adminData) {
13730
- return this.tenantService.putAdmin(adminId, adminData);
13731
- }
13732
- /**
13733
- * Get the tenant service for advanced operations
13734
- *
13735
- * @returns TenantService instance
13736
- */
13737
- getTenantService() {
13738
- return this.tenantService;
13739
- }
13740
- }
13741
-
13742
14789
  /**
13743
14790
  * Platform-Agnostic API Key API Client
13744
14791
  *
@@ -14333,6 +15380,36 @@ class AnalyticsManager {
14333
15380
  async getRetentionAnalytics(request = {}) {
14334
15381
  return this.analyticsService.getRetentionAnalytics(request);
14335
15382
  }
15383
+ /**
15384
+ * Get tag usage analytics across entities
15385
+ *
15386
+ * Aggregates tag usage across all taggable entities (campaigns, redemptions,
15387
+ * businesses, token metadata, user status types). Perfect for:
15388
+ * - Tag autocomplete/suggestions
15389
+ * - Tag management dashboards
15390
+ * - Usage analytics
15391
+ *
15392
+ * @param request - Optional filters for entity types
15393
+ * @returns Promise resolving to aggregated tag usage data
15394
+ *
15395
+ * @example Get all tags for autocomplete
15396
+ * ```typescript
15397
+ * const allTags = await sdk.analytics.getTagAnalytics();
15398
+ * const suggestions = allTags.tags.map(t => t.tag);
15399
+ * ```
15400
+ *
15401
+ * @example Get tags only from campaigns
15402
+ * ```typescript
15403
+ * import { TaggableEntityType } from '@explorins/pers-sdk';
15404
+ *
15405
+ * const campaignTags = await sdk.analytics.getTagAnalytics({
15406
+ * entityTypes: [TaggableEntityType.CAMPAIGN]
15407
+ * });
15408
+ * ```
15409
+ */
15410
+ async getTagAnalytics(request = {}) {
15411
+ return this.analyticsService.getTagAnalytics(request);
15412
+ }
14336
15413
  /**
14337
15414
  * Get the full analytics service for advanced operations
14338
15415
  *
@@ -14643,6 +15720,1411 @@ class TriggerSourceManager {
14643
15720
  }
14644
15721
  }
14645
15722
 
15723
+ /**
15724
+ * Webhook API - Low-level API client for webhook operations
15725
+ *
15726
+ * Backend routes:
15727
+ * - Admin CRUD: /hooks (requires tenant auth)
15728
+ * - Proxy/Trigger: /hooks/:projectKey/:hookId (public, identified by projectKey)
15729
+ * - Callback: /hooks/:projectKey/executions/:executionId/callback (public)
15730
+ *
15731
+ * @internal Use WebhookService or WebhookManager for higher-level operations
15732
+ */
15733
+ class WebhookApi {
15734
+ constructor(apiClient) {
15735
+ this.apiClient = apiClient;
15736
+ this.basePath = '/hooks';
15737
+ }
15738
+ // ================================
15739
+ // ADMIN: Webhook Configuration CRUD
15740
+ // All require tenant auth (handled by apiClient)
15741
+ // ================================
15742
+ /**
15743
+ * List all webhooks (Admin)
15744
+ * GET /hooks
15745
+ */
15746
+ async listWebhooks(options) {
15747
+ const params = new URLSearchParams();
15748
+ if (options?.active !== undefined)
15749
+ params.append('active', String(options.active));
15750
+ if (options?.page)
15751
+ params.append('page', String(options.page));
15752
+ if (options?.limit)
15753
+ params.append('limit', String(options.limit));
15754
+ if (options?.search)
15755
+ params.append('search', options.search);
15756
+ const queryString = params.toString();
15757
+ const url = queryString ? `${this.basePath}?${queryString}` : this.basePath;
15758
+ return this.apiClient.get(url);
15759
+ }
15760
+ /**
15761
+ * Get webhook by ID (Admin)
15762
+ * GET /hooks/:hookId
15763
+ */
15764
+ async getWebhookById(webhookId) {
15765
+ return this.apiClient.get(`${this.basePath}/${webhookId}`);
15766
+ }
15767
+ /**
15768
+ * Create a new webhook (Admin)
15769
+ * POST /hooks
15770
+ */
15771
+ async createWebhook(webhook) {
15772
+ return this.apiClient.post(this.basePath, webhook);
15773
+ }
15774
+ /**
15775
+ * Update a webhook (Admin)
15776
+ * PUT /hooks/:hookId
15777
+ */
15778
+ async updateWebhook(webhookId, webhook) {
15779
+ return this.apiClient.put(`${this.basePath}/${webhookId}`, webhook);
15780
+ }
15781
+ /**
15782
+ * Delete a webhook (Admin)
15783
+ * DELETE /hooks/:hookId
15784
+ */
15785
+ async deleteWebhook(webhookId) {
15786
+ return this.apiClient.delete(`${this.basePath}/${webhookId}`);
15787
+ }
15788
+ // ================================
15789
+ // Webhook Triggering via Proxy
15790
+ // Route: /hooks/:projectKey/:hookId
15791
+ // Public endpoint identified by projectKey in URL
15792
+ // ================================
15793
+ /**
15794
+ * Trigger a webhook programmatically
15795
+ *
15796
+ * Uses the proxy endpoint which requires projectKey in the URL path.
15797
+ * The projectKey is obtained from SDK config.
15798
+ *
15799
+ * ALL /hooks/:projectKey/:hookId
15800
+ */
15801
+ async triggerWebhook(request, projectKey) {
15802
+ const { hookId, method = 'POST', body, queryParams, waitForCallback, callbackTimeoutMs } = request;
15803
+ // Build query string from queryParams
15804
+ const params = new URLSearchParams();
15805
+ if (queryParams) {
15806
+ Object.entries(queryParams).forEach(([key, value]) => params.append(key, value));
15807
+ }
15808
+ if (waitForCallback)
15809
+ params.append('waitForCallback', 'true');
15810
+ if (callbackTimeoutMs)
15811
+ params.append('callbackTimeoutMs', String(callbackTimeoutMs));
15812
+ const queryString = params.toString();
15813
+ // Use proxy path with projectKey
15814
+ const proxyPath = `${this.basePath}/${projectKey}/${hookId}`;
15815
+ const url = queryString ? `${proxyPath}?${queryString}` : proxyPath;
15816
+ // Use appropriate HTTP method - backend returns WebhookTriggerResponseDTO
15817
+ switch (method) {
15818
+ case 'GET':
15819
+ return this.apiClient.get(url);
15820
+ case 'PUT':
15821
+ return this.apiClient.put(url, body);
15822
+ case 'DELETE':
15823
+ return this.apiClient.delete(url);
15824
+ case 'POST':
15825
+ default:
15826
+ return this.apiClient.post(url, body);
15827
+ }
15828
+ }
15829
+ // ================================
15830
+ // Execution History (Admin)
15831
+ // GET /hooks/executions
15832
+ // ================================
15833
+ /**
15834
+ * List webhook executions (Admin)
15835
+ * GET /hooks/executions
15836
+ */
15837
+ async listExecutions(options) {
15838
+ const params = new URLSearchParams();
15839
+ if (options?.webhookId)
15840
+ params.append('hookId', options.webhookId);
15841
+ if (options?.status)
15842
+ params.append('status', options.status);
15843
+ if (options?.fromDate)
15844
+ params.append('dateFrom', options.fromDate);
15845
+ if (options?.toDate)
15846
+ params.append('dateTo', options.toDate);
15847
+ if (options?.page)
15848
+ params.append('page', String(options.page));
15849
+ if (options?.limit)
15850
+ params.append('limit', String(options.limit));
15851
+ const queryString = params.toString();
15852
+ const url = queryString ? `${this.basePath}/executions?${queryString}` : `${this.basePath}/executions`;
15853
+ return this.apiClient.get(url);
15854
+ }
15855
+ /**
15856
+ * Get execution by ID (Admin)
15857
+ * Note: This endpoint doesn't exist in current backend - would need to filter by ID
15858
+ */
15859
+ async getExecutionById(executionId) {
15860
+ // Filter executions by ID since direct endpoint doesn't exist
15861
+ const result = await this.listExecutions({ page: 1, limit: 1 });
15862
+ const execution = result.data.find(e => e.id === executionId);
15863
+ if (!execution) {
15864
+ throw new Error(`Execution ${executionId} not found`);
15865
+ }
15866
+ return execution;
15867
+ }
15868
+ // ================================
15869
+ // Callback (Public - called by external systems)
15870
+ // POST /hooks/:projectKey/executions/:executionId/callback
15871
+ // ================================
15872
+ /**
15873
+ * Send callback result (for testing or manual callback trigger)
15874
+ * This is normally called by external systems like n8n
15875
+ *
15876
+ * POST /hooks/:projectKey/executions/:executionId/callback
15877
+ */
15878
+ async sendCallback(executionId, callback, projectKey) {
15879
+ return this.apiClient.post(`${this.basePath}/${projectKey}/executions/${executionId}/callback`, callback);
15880
+ }
15881
+ }
15882
+
15883
+ /**
15884
+ * Webhook Service - Business logic layer for webhook operations
15885
+ *
15886
+ * Provides higher-level operations on top of WebhookApi including:
15887
+ * - Validation and error handling
15888
+ * - Convenience methods for common patterns
15889
+ * - Async workflow support with callbacks
15890
+ *
15891
+ * Note: Trigger operations require projectKey which is obtained from SDK config.
15892
+ *
15893
+ * @group Services
15894
+ * @category Webhook
15895
+ */
15896
+ class WebhookService {
15897
+ constructor(webhookApi, projectKey) {
15898
+ this.webhookApi = webhookApi;
15899
+ this.projectKey = projectKey;
15900
+ }
15901
+ // ================================
15902
+ // Admin: Webhook Configuration
15903
+ // ================================
15904
+ /**
15905
+ * Get all webhooks with optional filters
15906
+ */
15907
+ async getWebhooks(options) {
15908
+ return this.webhookApi.listWebhooks(options);
15909
+ }
15910
+ /**
15911
+ * Get active webhooks only
15912
+ */
15913
+ async getActiveWebhooks() {
15914
+ return this.webhookApi.listWebhooks({ active: true });
15915
+ }
15916
+ /**
15917
+ * Get webhook by ID
15918
+ */
15919
+ async getWebhookById(webhookId) {
15920
+ return this.webhookApi.getWebhookById(webhookId);
15921
+ }
15922
+ /**
15923
+ * Create a new webhook
15924
+ */
15925
+ async createWebhook(webhook) {
15926
+ // Validate required fields
15927
+ if (!webhook.name?.trim()) {
15928
+ throw new Error('Webhook name is required');
15929
+ }
15930
+ if (!webhook.targetUrl?.trim()) {
15931
+ throw new Error('Target URL is required');
15932
+ }
15933
+ // Validate URL format
15934
+ try {
15935
+ new URL(webhook.targetUrl);
15936
+ }
15937
+ catch {
15938
+ throw new Error('Invalid target URL format');
15939
+ }
15940
+ return this.webhookApi.createWebhook(webhook);
15941
+ }
15942
+ /**
15943
+ * Update a webhook
15944
+ */
15945
+ async updateWebhook(webhookId, webhook) {
15946
+ // Validate URL if provided (cast to access optional property from PartialType)
15947
+ const update = webhook;
15948
+ if (update.targetUrl) {
15949
+ try {
15950
+ new URL(update.targetUrl);
15951
+ }
15952
+ catch {
15953
+ throw new Error('Invalid target URL format');
15954
+ }
15955
+ }
15956
+ return this.webhookApi.updateWebhook(webhookId, webhook);
15957
+ }
15958
+ /**
15959
+ * Enable a webhook
15960
+ */
15961
+ async enableWebhook(webhookId) {
15962
+ return this.webhookApi.updateWebhook(webhookId, { isActive: true });
15963
+ }
15964
+ /**
15965
+ * Disable a webhook
15966
+ */
15967
+ async disableWebhook(webhookId) {
15968
+ return this.webhookApi.updateWebhook(webhookId, { isActive: false });
15969
+ }
15970
+ /**
15971
+ * Delete a webhook
15972
+ */
15973
+ async deleteWebhook(webhookId) {
15974
+ return this.webhookApi.deleteWebhook(webhookId);
15975
+ }
15976
+ // ================================
15977
+ // Webhook Triggering (via Proxy)
15978
+ // Uses /hooks/:projectKey/:hookId
15979
+ // ================================
15980
+ /**
15981
+ * Trigger a webhook with full control over request parameters
15982
+ */
15983
+ async triggerWebhook(request) {
15984
+ return this.webhookApi.triggerWebhook(request, this.projectKey);
15985
+ }
15986
+ /**
15987
+ * Simple POST trigger with JSON body
15988
+ */
15989
+ async post(hookId, body) {
15990
+ return this.webhookApi.triggerWebhook({
15991
+ hookId,
15992
+ method: exports.WebhookMethod.POST,
15993
+ body
15994
+ }, this.projectKey);
15995
+ }
15996
+ /**
15997
+ * Simple GET trigger
15998
+ */
15999
+ async get(hookId, queryParams) {
16000
+ return this.webhookApi.triggerWebhook({
16001
+ hookId,
16002
+ method: exports.WebhookMethod.GET,
16003
+ queryParams
16004
+ }, this.projectKey);
16005
+ }
16006
+ /**
16007
+ * Trigger and wait for async callback (for workflow systems like n8n)
16008
+ *
16009
+ * @param hookId - Webhook ID to trigger
16010
+ * @param body - Request body
16011
+ * @param timeoutMs - How long to wait for callback (default: 30000ms)
16012
+ */
16013
+ async triggerAndWait(hookId, body, timeoutMs = 30000) {
16014
+ return this.webhookApi.triggerWebhook({
16015
+ hookId,
16016
+ method: exports.WebhookMethod.POST,
16017
+ body,
16018
+ waitForCallback: true,
16019
+ callbackTimeoutMs: timeoutMs
16020
+ }, this.projectKey);
16021
+ }
16022
+ // ================================
16023
+ // Execution History (Admin)
16024
+ // ================================
16025
+ /**
16026
+ * Get execution history with filters
16027
+ */
16028
+ async getExecutions(options) {
16029
+ return this.webhookApi.listExecutions(options);
16030
+ }
16031
+ /**
16032
+ * Get executions for a specific webhook
16033
+ */
16034
+ async getWebhookExecutions(webhookId, options) {
16035
+ return this.webhookApi.listExecutions({ ...options, webhookId });
16036
+ }
16037
+ /**
16038
+ * Get failed executions
16039
+ */
16040
+ async getFailedExecutions(options) {
16041
+ return this.webhookApi.listExecutions({ ...options, status: exports.WebhookExecutionStatus.FAILED });
16042
+ }
16043
+ /**
16044
+ * Get execution details by ID
16045
+ */
16046
+ async getExecutionById(executionId) {
16047
+ return this.webhookApi.getExecutionById(executionId);
16048
+ }
16049
+ // ================================
16050
+ // Callback (for testing/manual triggers)
16051
+ // ================================
16052
+ /**
16053
+ * Send a callback result for an execution
16054
+ * Normally this is called by external systems like n8n
16055
+ */
16056
+ async sendCallback(executionId, callback) {
16057
+ return this.webhookApi.sendCallback(executionId, callback, this.projectKey);
16058
+ }
16059
+ }
16060
+
16061
+ /**
16062
+ * Webhook Manager - Clean, high-level interface for webhook operations
16063
+ *
16064
+ * Webhooks enable integration with external systems (n8n, Zapier, custom backends):
16065
+ * - **Admin operations**: Create, configure, and manage webhook endpoints
16066
+ * - **Trigger operations**: Programmatically trigger webhooks with payloads
16067
+ * - **Async workflows**: Wait for callbacks from workflow systems
16068
+ * - **Monitoring**: Track execution history
16069
+ *
16070
+ * ## Security Model
16071
+ * - Admin endpoints require tenant authentication
16072
+ * - Trigger endpoints work with user/business/tenant auth
16073
+ * - Caller identity is passed to webhook target
16074
+ *
16075
+ * @group Managers
16076
+ * @category Webhook Management
16077
+ *
16078
+ * @example Basic Webhook Operations
16079
+ * ```typescript
16080
+ * // Admin: Create a webhook for order processing
16081
+ * const webhook = await sdk.webhooks.create({
16082
+ * name: 'Process Order',
16083
+ * targetUrl: 'https://n8n.example.com/webhook/orders',
16084
+ * method: 'POST',
16085
+ * headers: { 'X-Custom-Header': 'value' }
16086
+ * });
16087
+ *
16088
+ * // Trigger webhook with order data
16089
+ * const result = await sdk.webhooks.trigger(webhook.id, {
16090
+ * orderId: 'order-123',
16091
+ * items: [{ productId: 'prod-1', quantity: 2 }]
16092
+ * });
16093
+ *
16094
+ * console.log('Execution ID:', result.executionId);
16095
+ * ```
16096
+ *
16097
+ * @example Async Workflow with Callback
16098
+ * ```typescript
16099
+ * // Trigger and wait for workflow completion (up to 30s)
16100
+ * const result = await sdk.webhooks.triggerAndWait(
16101
+ * 'ai-processing-webhook',
16102
+ * { prompt: 'Generate report for Q1' },
16103
+ * 30000
16104
+ * );
16105
+ *
16106
+ * if (result.status === 'COMPLETED') {
16107
+ * console.log('AI Response:', result.body);
16108
+ * }
16109
+ * ```
16110
+ */
16111
+ class WebhookManager {
16112
+ constructor(apiClient, projectKey, events) {
16113
+ this.apiClient = apiClient;
16114
+ this.projectKey = projectKey;
16115
+ this.events = events;
16116
+ const webhookApi = new WebhookApi(apiClient);
16117
+ this.webhookService = new WebhookService(webhookApi, projectKey);
16118
+ }
16119
+ // ================================
16120
+ // Admin: Webhook Configuration
16121
+ // ================================
16122
+ /**
16123
+ * Get all webhooks with optional filters (Admin)
16124
+ *
16125
+ * @param options - Filter and pagination options
16126
+ * @returns Promise resolving to paginated webhooks
16127
+ *
16128
+ * @example
16129
+ * ```typescript
16130
+ * // Get all webhooks
16131
+ * const webhooks = await sdk.webhooks.getAll();
16132
+ *
16133
+ * // Get only active webhooks
16134
+ * const active = await sdk.webhooks.getAll({ active: true });
16135
+ *
16136
+ * // Search by name
16137
+ * const found = await sdk.webhooks.getAll({ search: 'order' });
16138
+ * ```
16139
+ */
16140
+ async getAll(options) {
16141
+ return this.webhookService.getWebhooks(options);
16142
+ }
16143
+ /**
16144
+ * Get active webhooks only (Admin)
16145
+ */
16146
+ async getActive() {
16147
+ return this.webhookService.getActiveWebhooks();
16148
+ }
16149
+ /**
16150
+ * Get webhook by ID (Admin)
16151
+ *
16152
+ * @param webhookId - UUID of the webhook
16153
+ * @returns Promise resolving to webhook details
16154
+ */
16155
+ async getById(webhookId) {
16156
+ return this.webhookService.getWebhookById(webhookId);
16157
+ }
16158
+ /**
16159
+ * Create a new webhook (Admin)
16160
+ *
16161
+ * @param webhook - Webhook configuration
16162
+ * @returns Promise resolving to created webhook
16163
+ *
16164
+ * @example
16165
+ * ```typescript
16166
+ * const webhook = await sdk.webhooks.create({
16167
+ * name: 'Order Notifications',
16168
+ * targetUrl: 'https://api.example.com/webhooks/orders',
16169
+ * method: 'POST',
16170
+ * headers: {
16171
+ * 'Authorization': 'Bearer secret-token'
16172
+ * },
16173
+ * rateLimitPerMinute: 60
16174
+ * });
16175
+ * ```
16176
+ */
16177
+ async create(webhook) {
16178
+ const result = await this.webhookService.createWebhook(webhook);
16179
+ this.events?.emitSuccess({
16180
+ domain: 'webhook',
16181
+ type: 'WEBHOOK_CREATED',
16182
+ userMessage: 'Webhook created successfully',
16183
+ details: { webhookId: result.id, name: result.name }
16184
+ });
16185
+ return result;
16186
+ }
16187
+ /**
16188
+ * Update a webhook (Admin)
16189
+ *
16190
+ * @param webhookId - UUID of the webhook to update
16191
+ * @param webhook - Updated configuration (partial)
16192
+ * @returns Promise resolving to updated webhook
16193
+ */
16194
+ async update(webhookId, webhook) {
16195
+ const result = await this.webhookService.updateWebhook(webhookId, webhook);
16196
+ this.events?.emitSuccess({
16197
+ domain: 'webhook',
16198
+ type: 'WEBHOOK_UPDATED',
16199
+ userMessage: 'Webhook updated successfully',
16200
+ details: { webhookId }
16201
+ });
16202
+ return result;
16203
+ }
16204
+ /**
16205
+ * Enable a webhook (Admin)
16206
+ */
16207
+ async enable(webhookId) {
16208
+ return this.webhookService.enableWebhook(webhookId);
16209
+ }
16210
+ /**
16211
+ * Disable a webhook (Admin)
16212
+ */
16213
+ async disable(webhookId) {
16214
+ return this.webhookService.disableWebhook(webhookId);
16215
+ }
16216
+ /**
16217
+ * Delete a webhook (Admin)
16218
+ *
16219
+ * @param webhookId - UUID of the webhook to delete
16220
+ * @returns Promise resolving to deletion confirmation
16221
+ */
16222
+ async delete(webhookId) {
16223
+ const result = await this.webhookService.deleteWebhook(webhookId);
16224
+ this.events?.emitSuccess({
16225
+ domain: 'webhook',
16226
+ type: 'WEBHOOK_DELETED',
16227
+ userMessage: 'Webhook deleted successfully',
16228
+ details: { webhookId }
16229
+ });
16230
+ return result;
16231
+ }
16232
+ // ================================
16233
+ // Webhook Triggering
16234
+ // ================================
16235
+ /**
16236
+ * Trigger a webhook with GET method
16237
+ *
16238
+ * Sends a GET request through the webhook proxy. Returns the full response
16239
+ * including execution metadata and raw data from the target.
16240
+ *
16241
+ * @param hookId - Webhook ID to trigger
16242
+ * @param queryParams - Optional query parameters
16243
+ * @returns Promise resolving to trigger response with metadata and data
16244
+ *
16245
+ * @example
16246
+ * ```typescript
16247
+ * // Fetch data from external API via webhook proxy
16248
+ * const result = await sdk.webhooks.get('user-directory-webhook');
16249
+ * console.log('Success:', result.success);
16250
+ * console.log('Data:', result.data); // Raw data from target
16251
+ * ```
16252
+ */
16253
+ async get(hookId, queryParams) {
16254
+ return this.webhookService.get(hookId, queryParams);
16255
+ }
16256
+ /**
16257
+ * Trigger a webhook with POST method
16258
+ *
16259
+ * Sends data to the configured webhook target. The caller's identity
16260
+ * (user/business/tenant) is included in the request context.
16261
+ *
16262
+ * @param hookId - Webhook ID to trigger
16263
+ * @param body - Payload to send
16264
+ * @returns Promise resolving to trigger response with execution metadata
16265
+ *
16266
+ * @example
16267
+ * ```typescript
16268
+ * const result = await sdk.webhooks.trigger('order-webhook', {
16269
+ * orderId: 'order-123',
16270
+ * action: 'created',
16271
+ * items: [...]
16272
+ * });
16273
+ *
16274
+ * console.log('Success:', result.success);
16275
+ * console.log('Execution ID:', result.executionId);
16276
+ * console.log('Response:', result.data);
16277
+ * ```
16278
+ */
16279
+ async trigger(hookId, body) {
16280
+ const result = await this.webhookService.post(hookId, body);
16281
+ this.events?.emitSuccess({
16282
+ domain: 'webhook',
16283
+ type: 'WEBHOOK_TRIGGERED',
16284
+ userMessage: 'Webhook triggered',
16285
+ details: { hookId, executionId: result.executionId }
16286
+ });
16287
+ return result;
16288
+ }
16289
+ /**
16290
+ * Trigger a webhook and wait for async callback
16291
+ *
16292
+ * Use this for workflow systems (n8n, Zapier) that need time to process
16293
+ * and return results via callback. The SDK will wait for the callback
16294
+ * up to the specified timeout.
16295
+ *
16296
+ * @param hookId - Webhook ID to trigger
16297
+ * @param body - Payload to send
16298
+ * @param timeoutMs - Max time to wait for callback (default: 30s)
16299
+ * @returns Promise resolving when callback received or timeout
16300
+ *
16301
+ * @example
16302
+ * ```typescript
16303
+ * // Trigger AI workflow and wait for result
16304
+ * const result = await sdk.webhooks.triggerAndWait(
16305
+ * 'ai-analysis-webhook',
16306
+ * { documentId: 'doc-123', analysisType: 'sentiment' },
16307
+ * 60000 // Wait up to 60 seconds
16308
+ * );
16309
+ *
16310
+ * if (result.success) {
16311
+ * console.log('Analysis:', result.data);
16312
+ * }
16313
+ * ```
16314
+ */
16315
+ async triggerAndWait(hookId, body, timeoutMs = 30000) {
16316
+ return this.webhookService.triggerAndWait(hookId, body, timeoutMs);
16317
+ }
16318
+ // ================================
16319
+ // Execution History & Monitoring
16320
+ // ================================
16321
+ /**
16322
+ * Get webhook execution history (Admin)
16323
+ *
16324
+ * @param options - Filter and pagination options
16325
+ * @returns Promise resolving to paginated executions
16326
+ *
16327
+ * @example
16328
+ * ```typescript
16329
+ * // Get recent failed executions
16330
+ * const failed = await sdk.webhooks.getExecutions({ status: 'FAILED' });
16331
+ *
16332
+ * // Get executions for specific webhook
16333
+ * const executions = await sdk.webhooks.getExecutions({
16334
+ * webhookId: 'webhook-123',
16335
+ * fromDate: '2024-01-01'
16336
+ * });
16337
+ * ```
16338
+ */
16339
+ async getExecutions(options) {
16340
+ return this.webhookService.getExecutions(options);
16341
+ }
16342
+ /**
16343
+ * Get execution details by ID (Admin)
16344
+ */
16345
+ async getExecutionById(executionId) {
16346
+ return this.webhookService.getExecutionById(executionId);
16347
+ }
16348
+ /**
16349
+ * Get the full webhook service for advanced operations
16350
+ *
16351
+ * @returns WebhookService instance with full API access
16352
+ */
16353
+ getWebhookService() {
16354
+ return this.webhookService;
16355
+ }
16356
+ }
16357
+
16358
+ /**
16359
+ * PERS Events Client
16360
+ *
16361
+ * Lightweight WebSocket client for real-time blockchain event streaming.
16362
+ * Connects to the PERS WS Relay server to receive events for user's wallets.
16363
+ *
16364
+ * ## v1.2.0 Subscription Model
16365
+ *
16366
+ * JWT is used for authentication only. After connecting, you must explicitly
16367
+ * subscribe to wallets (user SDK) or chains (admin dashboard).
16368
+ *
16369
+ * @example User SDK - Subscribe to specific wallets
16370
+ * ```typescript
16371
+ * const client = new PersEventsClient({
16372
+ * wsUrl: 'wss://events.pers.ninja',
16373
+ * autoReconnect: true
16374
+ * });
16375
+ *
16376
+ * await client.connect(jwtToken);
16377
+ *
16378
+ * // Subscribe to user's wallets
16379
+ * await client.subscribeWallets([
16380
+ * { address: '0x123...', chainId: 39123 }
16381
+ * ]);
16382
+ *
16383
+ * client.on('Transfer', (event) => {
16384
+ * console.log(`Received ${event.data.value} tokens`);
16385
+ * });
16386
+ * ```
16387
+ *
16388
+ * @example Admin Dashboard - Subscribe to all events on chains
16389
+ * ```typescript
16390
+ * await client.connect(adminJwtToken);
16391
+ *
16392
+ * // Subscribe to all events on specific chains
16393
+ * await client.subscribeChains([39123, 137]);
16394
+ *
16395
+ * client.on('*', (event) => {
16396
+ * console.log(`Chain ${event.chainId}: ${event.type}`);
16397
+ * });
16398
+ * ```
16399
+ */
16400
+ const DEFAULT_CONFIG = {
16401
+ autoReconnect: true,
16402
+ maxReconnectAttempts: 10,
16403
+ reconnectDelay: 1000,
16404
+ connectionTimeout: 30000,
16405
+ debug: false,
16406
+ tokenRefresher: undefined,
16407
+ };
16408
+ class PersEventsClient {
16409
+ constructor(config) {
16410
+ this.ws = null;
16411
+ this.state = 'disconnected';
16412
+ this.reconnectAttempts = 0;
16413
+ this.reconnectTimeout = null;
16414
+ this.token = null;
16415
+ // Event handlers by type
16416
+ this.handlers = new Map();
16417
+ this.stateHandlers = new Set();
16418
+ // Connection info from server
16419
+ this.connectionInfo = null;
16420
+ // Current subscription state
16421
+ this.subscriptionState = { wallets: [], chains: [], activeChains: [] };
16422
+ // Pending subscription promise resolver
16423
+ this.pendingSubscription = null;
16424
+ // Subscriptions to restore on reconnect
16425
+ this.savedSubscriptions = { wallets: [], chains: [] };
16426
+ this.config = { ...DEFAULT_CONFIG, ...config };
16427
+ }
16428
+ /**
16429
+ * Connect to the WS relay server
16430
+ * @param token - JWT token for authentication (wallets no longer required in JWT)
16431
+ */
16432
+ async connect(token) {
16433
+ if (this.state === 'connected' || this.state === 'connecting') {
16434
+ this.log('Already connected or connecting');
16435
+ return;
16436
+ }
16437
+ this.token = token;
16438
+ this.setState('connecting');
16439
+ return new Promise((resolve, reject) => {
16440
+ // Connection timeout
16441
+ const connectionTimeout = setTimeout(() => {
16442
+ this.log('Connection timeout');
16443
+ this.cleanup();
16444
+ this.setState('error');
16445
+ reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`));
16446
+ }, this.config.connectionTimeout);
16447
+ const clearTimeoutAndResolve = () => {
16448
+ clearTimeout(connectionTimeout);
16449
+ resolve();
16450
+ };
16451
+ const clearTimeoutAndReject = (err) => {
16452
+ clearTimeout(connectionTimeout);
16453
+ reject(err);
16454
+ };
16455
+ try {
16456
+ const url = new URL(this.config.wsUrl);
16457
+ url.searchParams.set('token', token);
16458
+ this.ws = new WebSocket(url.toString());
16459
+ this.ws.onopen = () => {
16460
+ // Wait for server 'connected' message
16461
+ };
16462
+ this.ws.onmessage = (event) => {
16463
+ this.handleMessage(event.data, clearTimeoutAndResolve);
16464
+ };
16465
+ this.ws.onerror = (error) => {
16466
+ this.log('WebSocket error:', error);
16467
+ this.setState('error');
16468
+ clearTimeoutAndReject(new Error('Connection failed'));
16469
+ };
16470
+ this.ws.onclose = (event) => {
16471
+ this.log(`WebSocket closed: ${event.code} ${event.reason}`);
16472
+ // Only reject if we were still connecting
16473
+ if (this.state === 'connecting') {
16474
+ clearTimeoutAndReject(new Error(`Connection closed during handshake: ${event.code} ${event.reason}`));
16475
+ }
16476
+ this.handleDisconnect();
16477
+ };
16478
+ }
16479
+ catch (err) {
16480
+ this.log('Error creating WebSocket:', err);
16481
+ this.setState('error');
16482
+ clearTimeoutAndReject(err instanceof Error ? err : new Error(String(err)));
16483
+ }
16484
+ });
16485
+ }
16486
+ /**
16487
+ * Disconnect from the server
16488
+ */
16489
+ disconnect() {
16490
+ this.config.autoReconnect = false; // Prevent reconnect
16491
+ this.cleanup();
16492
+ this.setState('disconnected');
16493
+ }
16494
+ // ─────────────────────────────────────────────────────────────────────────────
16495
+ // Subscription Methods (v1.2.0)
16496
+ // ─────────────────────────────────────────────────────────────────────────────
16497
+ /**
16498
+ * Subscribe to wallet events (User SDK)
16499
+ *
16500
+ * Receives events only for the specified wallet addresses.
16501
+ * Can be called multiple times to add more wallets.
16502
+ *
16503
+ * @param wallets - Array of wallet info (address + chainId)
16504
+ * @returns Promise that resolves when subscription is confirmed
16505
+ */
16506
+ async subscribeWallets(wallets) {
16507
+ return this.sendSubscription({ type: 'subscribe', wallets });
16508
+ }
16509
+ /**
16510
+ * Subscribe to chain events (Admin Dashboard)
16511
+ *
16512
+ * Receives ALL events on the specified chains.
16513
+ * Use for admin dashboards that need to monitor all activity.
16514
+ *
16515
+ * @param chains - Array of chain IDs to subscribe to
16516
+ * @returns Promise that resolves when subscription is confirmed
16517
+ */
16518
+ async subscribeChains(chains) {
16519
+ return this.sendSubscription({ type: 'subscribe', chains });
16520
+ }
16521
+ /**
16522
+ * Unsubscribe from wallet events
16523
+ *
16524
+ * @param wallets - Array of wallet info to unsubscribe from
16525
+ * @returns Promise that resolves when unsubscription is confirmed
16526
+ */
16527
+ async unsubscribeWallets(wallets) {
16528
+ return this.sendSubscription({ type: 'unsubscribe', wallets });
16529
+ }
16530
+ /**
16531
+ * Unsubscribe from chain events
16532
+ *
16533
+ * @param chains - Array of chain IDs to unsubscribe from
16534
+ * @returns Promise that resolves when unsubscription is confirmed
16535
+ */
16536
+ async unsubscribeChains(chains) {
16537
+ return this.sendSubscription({ type: 'unsubscribe', chains });
16538
+ }
16539
+ /**
16540
+ * Get current subscription state
16541
+ */
16542
+ getSubscriptionState() {
16543
+ return { ...this.subscriptionState };
16544
+ }
16545
+ // ─────────────────────────────────────────────────────────────────────────────
16546
+ // Event Handlers
16547
+ // ─────────────────────────────────────────────────────────────────────────────
16548
+ /**
16549
+ * Subscribe to blockchain events
16550
+ * @param eventType - Event type to listen for, or '*' for all events
16551
+ * @param handler - Event handler function
16552
+ * @returns Unsubscribe function
16553
+ */
16554
+ on(eventType, handler) {
16555
+ if (!this.handlers.has(eventType)) {
16556
+ this.handlers.set(eventType, new Set());
16557
+ }
16558
+ this.handlers.get(eventType).add(handler);
16559
+ return () => {
16560
+ this.handlers.get(eventType)?.delete(handler);
16561
+ };
16562
+ }
16563
+ /**
16564
+ * Subscribe to connection state changes
16565
+ */
16566
+ onStateChange(handler) {
16567
+ this.stateHandlers.add(handler);
16568
+ return () => this.stateHandlers.delete(handler);
16569
+ }
16570
+ /**
16571
+ * Get current connection state
16572
+ */
16573
+ getState() {
16574
+ return this.state;
16575
+ }
16576
+ /**
16577
+ * Get connection info (userId, initial wallets, activeChains)
16578
+ */
16579
+ getConnectionInfo() {
16580
+ return this.connectionInfo;
16581
+ }
16582
+ // ─────────────────────────────────────────────────────────────────────────────
16583
+ // Private methods
16584
+ // ─────────────────────────────────────────────────────────────────────────────
16585
+ async sendSubscription(message) {
16586
+ if (this.state !== 'connected' || !this.ws) {
16587
+ throw new Error('Not connected to server');
16588
+ }
16589
+ // Save subscriptions for reconnect
16590
+ if (message.type === 'subscribe') {
16591
+ if (message.wallets) {
16592
+ this.savedSubscriptions.wallets = [
16593
+ ...this.savedSubscriptions.wallets,
16594
+ ...message.wallets.filter(w => !this.savedSubscriptions.wallets.some(sw => sw.address === w.address && sw.chainId === w.chainId))
16595
+ ];
16596
+ }
16597
+ if (message.chains) {
16598
+ this.savedSubscriptions.chains = [
16599
+ ...new Set([...this.savedSubscriptions.chains, ...message.chains])
16600
+ ];
16601
+ }
16602
+ }
16603
+ else if (message.type === 'unsubscribe') {
16604
+ if (message.wallets) {
16605
+ this.savedSubscriptions.wallets = this.savedSubscriptions.wallets.filter(sw => !message.wallets.some(w => w.address === sw.address && w.chainId === sw.chainId));
16606
+ }
16607
+ if (message.chains) {
16608
+ this.savedSubscriptions.chains = this.savedSubscriptions.chains.filter(c => !message.chains.includes(c));
16609
+ }
16610
+ }
16611
+ return new Promise((resolve, reject) => {
16612
+ // Set up timeout
16613
+ const timeout = setTimeout(() => {
16614
+ this.pendingSubscription = null;
16615
+ reject(new Error('Subscription timeout'));
16616
+ }, 10000);
16617
+ this.pendingSubscription = {
16618
+ resolve: () => {
16619
+ clearTimeout(timeout);
16620
+ this.pendingSubscription = null;
16621
+ resolve(this.subscriptionState);
16622
+ },
16623
+ reject: (err) => {
16624
+ clearTimeout(timeout);
16625
+ this.pendingSubscription = null;
16626
+ reject(err);
16627
+ }
16628
+ };
16629
+ this.ws.send(JSON.stringify(message));
16630
+ this.log('Sent subscription message:', message);
16631
+ });
16632
+ }
16633
+ handleMessage(data, onConnected) {
16634
+ try {
16635
+ const message = JSON.parse(data);
16636
+ switch (message.type) {
16637
+ case 'connected':
16638
+ this.connectionInfo = message.payload;
16639
+ this.reconnectAttempts = 0;
16640
+ this.setState('connected');
16641
+ this.log('Connected:', message.payload);
16642
+ onConnected?.();
16643
+ // Restore subscriptions on reconnect
16644
+ this.restoreSubscriptions();
16645
+ break;
16646
+ case 'subscribed':
16647
+ this.subscriptionState = message.payload;
16648
+ this.log('Subscription confirmed:', message.payload);
16649
+ this.pendingSubscription?.resolve();
16650
+ break;
16651
+ case 'unsubscribed':
16652
+ this.subscriptionState = message.payload;
16653
+ this.log('Unsubscription confirmed:', message.payload);
16654
+ this.pendingSubscription?.resolve();
16655
+ break;
16656
+ case 'event':
16657
+ this.routeEvent(message.payload);
16658
+ break;
16659
+ case 'ping':
16660
+ // Respond to server ping with pong
16661
+ this.sendPong();
16662
+ break;
16663
+ case 'error':
16664
+ this.log('Server error:', message.payload);
16665
+ this.pendingSubscription?.reject(new Error(message.payload.message));
16666
+ break;
16667
+ }
16668
+ }
16669
+ catch (err) {
16670
+ this.log('Failed to parse message:', err);
16671
+ }
16672
+ }
16673
+ sendPong() {
16674
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
16675
+ this.ws.send(JSON.stringify({ type: 'pong' }));
16676
+ this.log('Sent pong');
16677
+ }
16678
+ }
16679
+ async restoreSubscriptions() {
16680
+ // Restore wallet subscriptions
16681
+ if (this.savedSubscriptions.wallets.length > 0) {
16682
+ this.log('Restoring wallet subscriptions:', this.savedSubscriptions.wallets);
16683
+ try {
16684
+ await this.subscribeWallets(this.savedSubscriptions.wallets);
16685
+ }
16686
+ catch (err) {
16687
+ this.log('Failed to restore wallet subscriptions:', err);
16688
+ }
16689
+ }
16690
+ // Restore chain subscriptions
16691
+ if (this.savedSubscriptions.chains.length > 0) {
16692
+ this.log('Restoring chain subscriptions:', this.savedSubscriptions.chains);
16693
+ try {
16694
+ await this.subscribeChains(this.savedSubscriptions.chains);
16695
+ }
16696
+ catch (err) {
16697
+ this.log('Failed to restore chain subscriptions:', err);
16698
+ }
16699
+ }
16700
+ }
16701
+ routeEvent(event) {
16702
+ this.log('Event received:', event.type, event);
16703
+ // Call specific type handlers
16704
+ const typeHandlers = this.handlers.get(event.type);
16705
+ typeHandlers?.forEach(handler => {
16706
+ try {
16707
+ handler(event);
16708
+ }
16709
+ catch (err) {
16710
+ console.error('[PERS-Events] Handler error:', err);
16711
+ }
16712
+ });
16713
+ // Call wildcard handlers
16714
+ const wildcardHandlers = this.handlers.get('*');
16715
+ wildcardHandlers?.forEach(handler => {
16716
+ try {
16717
+ handler(event);
16718
+ }
16719
+ catch (err) {
16720
+ console.error('[PERS-Events] Handler error:', err);
16721
+ }
16722
+ });
16723
+ }
16724
+ handleDisconnect() {
16725
+ this.cleanup();
16726
+ this.setState('disconnected');
16727
+ if (this.config.autoReconnect && this.token) {
16728
+ if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
16729
+ this.attemptReconnect();
16730
+ }
16731
+ else {
16732
+ this.log(`Max reconnect attempts (${this.config.maxReconnectAttempts}) reached. Call connect() manually to retry.`);
16733
+ // Reset counter so manual connect() will work
16734
+ this.reconnectAttempts = 0;
16735
+ }
16736
+ }
16737
+ }
16738
+ attemptReconnect() {
16739
+ const delay = Math.min(this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000 // Max 30 second delay
16740
+ );
16741
+ this.reconnectAttempts++;
16742
+ this.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
16743
+ this.setState('reconnecting');
16744
+ this.reconnectTimeout = setTimeout(async () => {
16745
+ try {
16746
+ // Try to get fresh token if tokenRefresher is provided
16747
+ let tokenToUse = this.token;
16748
+ if (this.config.tokenRefresher) {
16749
+ try {
16750
+ this.log('🔄 Refreshing token before reconnect...');
16751
+ tokenToUse = await this.config.tokenRefresher();
16752
+ this.token = tokenToUse; // Update stored token
16753
+ }
16754
+ catch (refreshErr) {
16755
+ this.log('⚠️ Token refresh failed, using existing token:', refreshErr);
16756
+ }
16757
+ }
16758
+ if (tokenToUse) {
16759
+ this.log(`🔄 Attempting reconnection...`);
16760
+ await this.connect(tokenToUse);
16761
+ }
16762
+ }
16763
+ catch (err) {
16764
+ this.log('❌ Reconnect failed:', err);
16765
+ }
16766
+ }, delay);
16767
+ }
16768
+ cleanup() {
16769
+ if (this.reconnectTimeout) {
16770
+ clearTimeout(this.reconnectTimeout);
16771
+ this.reconnectTimeout = null;
16772
+ }
16773
+ if (this.ws) {
16774
+ this.ws.onopen = null;
16775
+ this.ws.onmessage = null;
16776
+ this.ws.onerror = null;
16777
+ this.ws.onclose = null;
16778
+ if (this.ws.readyState === WebSocket.OPEN) {
16779
+ this.ws.close();
16780
+ }
16781
+ this.ws = null;
16782
+ }
16783
+ }
16784
+ setState(state) {
16785
+ if (this.state !== state) {
16786
+ this.state = state;
16787
+ this.stateHandlers.forEach(handler => handler(state));
16788
+ }
16789
+ }
16790
+ log(...args) {
16791
+ if (this.config.debug) {
16792
+ console.log('[PERS-Events]', ...args);
16793
+ }
16794
+ }
16795
+ }
16796
+
16797
+ /**
16798
+ * Wallet Events Manager - Real-time blockchain events for user's wallets
16799
+ *
16800
+ * Provides automatic connection management and event routing integrated
16801
+ * with the SDK's authentication flow and event system.
16802
+ *
16803
+ * Events are routed through the SDK's PersEventEmitter, so you can either:
16804
+ * 1. Subscribe via `sdk.walletEvents.on()` for wallet-specific handling
16805
+ * 2. Subscribe via `sdk.events.subscribe()` for unified event stream
16806
+ *
16807
+ * ## v1.2.0 - Subscription Model
16808
+ *
16809
+ * After connecting, you must explicitly subscribe to wallets or chains:
16810
+ * - User SDK: `subscribeWallets([{ address, chainId }])`
16811
+ * - Admin Dashboard: `subscribeChains([chainId1, chainId2])`
16812
+ *
16813
+ * For convenience, use `sdk.connectWalletEvents()` which auto-subscribes
16814
+ * based on auth type.
16815
+ *
16816
+ * @example Manual subscription
16817
+ * ```typescript
16818
+ * await sdk.walletEvents.connect();
16819
+ * await sdk.walletEvents.subscribeWallets([
16820
+ * { address: user.wallets[0].address, chainId: 39123 }
16821
+ * ]);
16822
+ *
16823
+ * sdk.walletEvents.on('Transfer', (event) => {
16824
+ * console.log(`Received ${event.data.value} tokens`);
16825
+ * });
16826
+ * ```
16827
+ *
16828
+ * @example Auto-subscription (recommended)
16829
+ * ```typescript
16830
+ * // Auto-subscribes based on auth type (user/business/admin)
16831
+ * await sdk.connectWalletEvents();
16832
+ * ```
16833
+ */
16834
+ /**
16835
+ * Wallet Events Manager - Manages real-time blockchain event subscriptions
16836
+ *
16837
+ * Low-level WS client wrapper. For auto-subscription based on auth type,
16838
+ * use `sdk.connectWalletEvents()` which handles subscription automatically.
16839
+ */
16840
+ class WalletEventsManager {
16841
+ constructor(apiClient, eventEmitter, config) {
16842
+ this.apiClient = apiClient;
16843
+ this.eventEmitter = eventEmitter;
16844
+ this.client = null;
16845
+ this.pendingHandlers = [];
16846
+ this.unsubscribes = [];
16847
+ this.config = {
16848
+ autoReconnect: true,
16849
+ connectionTimeout: 30000,
16850
+ debug: false,
16851
+ ...config,
16852
+ };
16853
+ }
16854
+ /**
16855
+ * Connect to real-time wallet events
16856
+ *
16857
+ * Establishes WebSocket connection to the WS relay server.
16858
+ * After connecting, call subscribeWallets() or subscribeChains() to start
16859
+ * receiving events.
16860
+ *
16861
+ * For auto-subscription, use `sdk.connectWalletEvents()` instead.
16862
+ */
16863
+ async connect() {
16864
+ // Already connected?
16865
+ if (this.client?.getState() === 'connected') {
16866
+ return;
16867
+ }
16868
+ const sdkConfig = this.apiClient.getConfig();
16869
+ const authProvider = sdkConfig.authProvider;
16870
+ if (!authProvider) {
16871
+ throw new Error('Not authenticated. Call sdk.auth.login() first.');
16872
+ }
16873
+ const token = await authProvider.getToken();
16874
+ if (!token) {
16875
+ throw new Error('No authentication token available. Call sdk.auth.login() first.');
16876
+ }
16877
+ // Resolve wsUrl: config override > SDK config > environment default
16878
+ const wsUrl = this.config.wsUrl
16879
+ || sdkConfig.walletEventsWsUrl
16880
+ || buildWalletEventsWsUrl(sdkConfig.environment);
16881
+ // Create token refresher that fetches fresh token from auth provider
16882
+ const tokenRefresher = async () => {
16883
+ const freshToken = await authProvider.getToken();
16884
+ if (!freshToken) {
16885
+ throw new Error('Failed to refresh token');
16886
+ }
16887
+ return freshToken;
16888
+ };
16889
+ this.client = new PersEventsClient({
16890
+ wsUrl,
16891
+ autoReconnect: this.config.autoReconnect,
16892
+ connectionTimeout: this.config.connectionTimeout,
16893
+ debug: this.config.debug,
16894
+ tokenRefresher,
16895
+ });
16896
+ await this.client.connect(token);
16897
+ // Clear previous unsubscribes on new connection (prevent memory leak)
16898
+ this.unsubscribes.forEach(unsub => unsub());
16899
+ this.unsubscribes = [];
16900
+ // Route all events through PersEventEmitter for unified stream
16901
+ // Note: Cast needed because web3-types uses string for event.type while pers-shared uses strict union
16902
+ this.client.on('*', (event) => this.emitToPersEvents(event));
16903
+ // Re-attach any handlers registered before connect
16904
+ for (const { type, handler } of this.pendingHandlers) {
16905
+ this.unsubscribes.push(this.client.on(type, handler));
16906
+ }
16907
+ this.pendingHandlers = [];
16908
+ }
16909
+ /**
16910
+ * Disconnect from real-time events
16911
+ */
16912
+ disconnect() {
16913
+ this.unsubscribes.forEach(unsub => unsub());
16914
+ this.unsubscribes = [];
16915
+ this.client?.disconnect();
16916
+ this.client = null;
16917
+ }
16918
+ // ─────────────────────────────────────────────────────────────────────────────
16919
+ // Subscription Methods (v1.2.0)
16920
+ // ─────────────────────────────────────────────────────────────────────────────
16921
+ /**
16922
+ * Subscribe to wallet events
16923
+ *
16924
+ * Receives events only for the specified wallet addresses.
16925
+ * Use this for user-facing SDK where you want to monitor specific wallets.
16926
+ *
16927
+ * @param wallets - Array of wallet info (address + chainId)
16928
+ * @returns Promise that resolves when subscription is confirmed
16929
+ *
16930
+ * @example
16931
+ * ```typescript
16932
+ * await sdk.walletEvents.connect();
16933
+ * await sdk.walletEvents.subscribeWallets([
16934
+ * { address: '0x123...', chainId: 39123 }
16935
+ * ]);
16936
+ * ```
16937
+ */
16938
+ async subscribeWallets(wallets) {
16939
+ if (!this.client) {
16940
+ throw new Error('Not connected. Call connect() first.');
16941
+ }
16942
+ return this.client.subscribeWallets(wallets);
16943
+ }
16944
+ /**
16945
+ * Subscribe to chain events (Admin Dashboard)
16946
+ *
16947
+ * Receives ALL events on the specified chains.
16948
+ * Use for admin dashboards that need to monitor all activity.
16949
+ *
16950
+ * @param chains - Array of chain IDs to subscribe to
16951
+ * @returns Promise that resolves when subscription is confirmed
16952
+ *
16953
+ * @example
16954
+ * ```typescript
16955
+ * await sdk.walletEvents.connect();
16956
+ * await sdk.walletEvents.subscribeChains([39123, 137]);
16957
+ * ```
16958
+ */
16959
+ async subscribeChains(chains) {
16960
+ if (!this.client) {
16961
+ throw new Error('Not connected. Call connect() first.');
16962
+ }
16963
+ return this.client.subscribeChains(chains);
16964
+ }
16965
+ /**
16966
+ * Unsubscribe from wallet events
16967
+ *
16968
+ * @param wallets - Array of wallet info to unsubscribe from
16969
+ * @returns Promise that resolves when unsubscription is confirmed
16970
+ */
16971
+ async unsubscribeWallets(wallets) {
16972
+ if (!this.client) {
16973
+ throw new Error('Not connected. Call connect() first.');
16974
+ }
16975
+ return this.client.unsubscribeWallets(wallets);
16976
+ }
16977
+ /**
16978
+ * Unsubscribe from chain events
16979
+ *
16980
+ * @param chains - Array of chain IDs to unsubscribe from
16981
+ * @returns Promise that resolves when unsubscription is confirmed
16982
+ */
16983
+ async unsubscribeChains(chains) {
16984
+ if (!this.client) {
16985
+ throw new Error('Not connected. Call connect() first.');
16986
+ }
16987
+ return this.client.unsubscribeChains(chains);
16988
+ }
16989
+ /**
16990
+ * Get current subscription state
16991
+ */
16992
+ getSubscriptionState() {
16993
+ return this.client?.getSubscriptionState() ?? { wallets: [], chains: [], activeChains: [] };
16994
+ }
16995
+ // ─────────────────────────────────────────────────────────────────────────────
16996
+ // Event Handlers
16997
+ // ─────────────────────────────────────────────────────────────────────────────
16998
+ /**
16999
+ * Subscribe to blockchain events
17000
+ *
17001
+ * @param eventType - Event type ('Transfer', 'Approval', etc.) or '*' for all
17002
+ * @param handler - Callback function when event is received
17003
+ * @returns Unsubscribe function
17004
+ *
17005
+ * @example
17006
+ * ```typescript
17007
+ * const unsub = sdk.walletEvents.on('Transfer', (event) => {
17008
+ * console.log(`Transfer: ${event.data.from} -> ${event.data.to}`);
17009
+ * });
17010
+ *
17011
+ * // Later: stop listening
17012
+ * unsub();
17013
+ * ```
17014
+ */
17015
+ on(eventType, handler) {
17016
+ if (this.client) {
17017
+ const unsub = this.client.on(eventType, handler);
17018
+ this.unsubscribes.push(unsub);
17019
+ return unsub;
17020
+ }
17021
+ else {
17022
+ // Store for when connect() is called
17023
+ this.pendingHandlers.push({ type: eventType, handler });
17024
+ return () => {
17025
+ const idx = this.pendingHandlers.findIndex(h => h.handler === handler);
17026
+ if (idx !== -1)
17027
+ this.pendingHandlers.splice(idx, 1);
17028
+ };
17029
+ }
17030
+ }
17031
+ /**
17032
+ * Subscribe to connection state changes
17033
+ */
17034
+ onStateChange(handler) {
17035
+ if (this.client) {
17036
+ return this.client.onStateChange(handler);
17037
+ }
17038
+ return () => { };
17039
+ }
17040
+ /**
17041
+ * Get current connection state
17042
+ */
17043
+ getState() {
17044
+ return this.client?.getState() ?? 'disconnected';
17045
+ }
17046
+ /**
17047
+ * Check if connected
17048
+ */
17049
+ isConnected() {
17050
+ return this.getState() === 'connected';
17051
+ }
17052
+ /**
17053
+ * Get connection info (wallets, active chains)
17054
+ */
17055
+ getConnectionInfo() {
17056
+ return this.client?.getConnectionInfo();
17057
+ }
17058
+ // ─────────────────────────────────────────────────────────────────────────────
17059
+ // Private Methods
17060
+ // ─────────────────────────────────────────────────────────────────────────────
17061
+ /**
17062
+ * Build a descriptive message for blockchain events
17063
+ * Neutral tone - frontend will handle user-facing presentation
17064
+ */
17065
+ buildUserMessage(event) {
17066
+ const { type, data } = event;
17067
+ // Use string for switch since event types may extend beyond strict BlockchainEventType union
17068
+ const eventType = type;
17069
+ const from = data.from?.toLowerCase();
17070
+ const to = data.to?.toLowerCase();
17071
+ const shortFrom = from ? `${from.slice(0, 6)}...${from.slice(-4)}` : 'unknown';
17072
+ const shortTo = to ? `${to.slice(0, 6)}...${to.slice(-4)}` : 'unknown';
17073
+ // Get user's wallets to determine direction
17074
+ const subscriptionState = this.getSubscriptionState();
17075
+ const userWallets = subscriptionState.wallets.map(w => w.address.toLowerCase());
17076
+ const isIncoming = to && userWallets.includes(to);
17077
+ const isOutgoing = from && userWallets.includes(from);
17078
+ const isMint = from === '0x0000000000000000000000000000000000000000';
17079
+ const isBurn = to === '0x0000000000000000000000000000000000000000';
17080
+ switch (eventType) {
17081
+ case 'Transfer':
17082
+ if (isMint) {
17083
+ return `Token minted to ${shortTo}`;
17084
+ }
17085
+ else if (isBurn) {
17086
+ return `Token burned from ${shortFrom}`;
17087
+ }
17088
+ else if (isIncoming) {
17089
+ return `Transfer received from ${shortFrom}`;
17090
+ }
17091
+ else if (isOutgoing) {
17092
+ return `Transfer sent to ${shortTo}`;
17093
+ }
17094
+ return `Transfer from ${shortFrom} to ${shortTo}`;
17095
+ case 'Approval':
17096
+ return `Approval from ${shortFrom}`;
17097
+ case 'SmartWalletCreated':
17098
+ return `Smart wallet created: ${shortTo}`;
17099
+ case 'TransactionExecuted':
17100
+ return `Transaction executed by ${shortFrom}`;
17101
+ case 'OwnershipTransferred':
17102
+ return `Ownership transferred from ${shortFrom} to ${shortTo}`;
17103
+ default:
17104
+ return `${eventType} event`;
17105
+ }
17106
+ }
17107
+ /**
17108
+ * Route blockchain event to PersEventEmitter for unified event stream
17109
+ */
17110
+ emitToPersEvents(event) {
17111
+ const userMessage = this.buildUserMessage(event);
17112
+ this.eventEmitter.emitSuccess({
17113
+ type: `wallet_${event.type.toLowerCase()}`,
17114
+ domain: 'wallet',
17115
+ userMessage,
17116
+ details: {
17117
+ chainId: event.chainId,
17118
+ txHash: event.txHash,
17119
+ blockNumber: event.blockNumber,
17120
+ timestamp: event.timestamp,
17121
+ contractAddress: event.contractAddress,
17122
+ ...event.data,
17123
+ },
17124
+ });
17125
+ }
17126
+ }
17127
+
14646
17128
  /**
14647
17129
  * @fileoverview PERS SDK - Platform-agnostic TypeScript SDK with High-Level Managers
14648
17130
  *
@@ -14756,6 +17238,81 @@ class PersSDK {
14756
17238
  // Initialize event emitter and wire to API client for error events
14757
17239
  this._events = new PersEventEmitter();
14758
17240
  this.apiClient.setEvents(this._events);
17241
+ // Auto-connect to wallet events on successful login (if enabled)
17242
+ this.setupWalletEventsAutoConnect();
17243
+ }
17244
+ /**
17245
+ * Setup auto-connect for wallet events on authentication
17246
+ * @internal
17247
+ */
17248
+ setupWalletEventsAutoConnect() {
17249
+ const config = this.apiClient.getConfig();
17250
+ // Check if captureWalletEvents is enabled (default: true)
17251
+ if (config.captureWalletEvents === false) {
17252
+ return;
17253
+ }
17254
+ // Listen for login success events (both fresh login and session restoration)
17255
+ this._events.subscribe((event) => {
17256
+ if (event.level === 'success' && (event.type === 'LOGIN_SUCCESS' || event.type === 'session_restored')) {
17257
+ // Auto-connect and subscribe to wallet events (fire and forget, log errors)
17258
+ this.connectWalletEvents().catch((err) => {
17259
+ console.warn('[PersSDK] Failed to auto-connect wallet events:', err.message);
17260
+ });
17261
+ }
17262
+ // Disconnect on logout
17263
+ if (event.level === 'success' && event.type === 'logout_success') {
17264
+ this.walletEvents.disconnect();
17265
+ }
17266
+ });
17267
+ }
17268
+ /**
17269
+ * Connect to wallet events and auto-subscribe based on auth type
17270
+ *
17271
+ * Connects to the WebSocket relay and automatically subscribes to relevant
17272
+ * blockchain events based on the current authentication type:
17273
+ * - **USER**: Subscribes to all user's wallets
17274
+ * - **BUSINESS**: Subscribes to all business's wallets
17275
+ * - **TENANT**: Subscribes to all chains where tokens are deployed
17276
+ *
17277
+ * This method is called automatically on login when `captureWalletEvents` is enabled.
17278
+ * Call manually if you need to reconnect or refresh subscriptions.
17279
+ *
17280
+ * @example Manual connection
17281
+ * ```typescript
17282
+ * await sdk.connectWalletEvents();
17283
+ * ```
17284
+ */
17285
+ async connectWalletEvents() {
17286
+ await this.walletEvents.connect();
17287
+ // Get authType from auth provider (where it's stored during login)
17288
+ const authProvider = this.apiClient.getConfig().authProvider;
17289
+ const authType = authProvider?.getAuthType ? await authProvider.getAuthType() : undefined;
17290
+ switch (authType) {
17291
+ case exports.AccountOwnerType.USER: {
17292
+ const user = await this.users.getCurrentUser();
17293
+ const wallets = user.wallets?.map(w => ({ address: w.address, chainId: w.chainId })) || [];
17294
+ if (wallets.length > 0) {
17295
+ await this.walletEvents.subscribeWallets(wallets);
17296
+ }
17297
+ break;
17298
+ }
17299
+ case exports.AccountOwnerType.BUSINESS: {
17300
+ const business = await this.auth.getCurrentBusiness();
17301
+ const wallets = business.wallets?.map(w => ({ address: w.address, chainId: w.chainId })) || [];
17302
+ if (wallets.length > 0) {
17303
+ await this.walletEvents.subscribeWallets(wallets);
17304
+ }
17305
+ break;
17306
+ }
17307
+ case exports.AccountOwnerType.TENANT: {
17308
+ const tokens = await this.tokens.getTokens();
17309
+ const chains = [...new Set(tokens.data.map(t => t.chainId))];
17310
+ if (chains.length > 0) {
17311
+ await this.walletEvents.subscribeChains(chains);
17312
+ }
17313
+ break;
17314
+ }
17315
+ }
14759
17316
  }
14760
17317
  /**
14761
17318
  * Restore user session from stored tokens
@@ -15163,6 +17720,103 @@ class PersSDK {
15163
17720
  }
15164
17721
  return this._triggerSources;
15165
17722
  }
17723
+ /**
17724
+ * Webhook manager - High-level webhook operations
17725
+ *
17726
+ * Provides methods for creating webhooks, triggering them programmatically,
17727
+ * and monitoring execution history. Supports async workflows with callbacks.
17728
+ *
17729
+ * @returns WebhookManager instance
17730
+ *
17731
+ * @example Webhook Operations
17732
+ * ```typescript
17733
+ * // Admin: Create a webhook
17734
+ * const webhook = await sdk.webhooks.create({
17735
+ * name: 'Order Processing',
17736
+ * targetUrl: 'https://n8n.example.com/webhook/orders',
17737
+ * method: 'POST'
17738
+ * });
17739
+ *
17740
+ * // Trigger webhook with payload
17741
+ * const result = await sdk.webhooks.trigger(webhook.id, {
17742
+ * orderId: 'order-123',
17743
+ * action: 'created'
17744
+ * });
17745
+ *
17746
+ * // Trigger and wait for async workflow completion
17747
+ * const asyncResult = await sdk.webhooks.triggerAndWait(
17748
+ * 'ai-webhook',
17749
+ * { prompt: 'Analyze this data' },
17750
+ * 30000 // Wait up to 30s
17751
+ * );
17752
+ * ```
17753
+ *
17754
+ * @see {@link WebhookManager} for detailed documentation
17755
+ */
17756
+ get webhooks() {
17757
+ if (!this._webhooks) {
17758
+ const projectKey = this.apiClient.getConfig().apiProjectKey || '';
17759
+ this._webhooks = new WebhookManager(this.apiClient, projectKey, this._events);
17760
+ }
17761
+ return this._webhooks;
17762
+ }
17763
+ /**
17764
+ * Wallet Events Manager - Real-time blockchain events for user's wallets
17765
+ *
17766
+ * Provides real-time WebSocket connection to receive blockchain events
17767
+ * for the user's wallets (transfers, approvals, NFT mints, etc.).
17768
+ *
17769
+ * Events are also routed through `sdk.events` for unified event handling.
17770
+ *
17771
+ * **Important:** Requires `walletEventsWsUrl` configuration and authentication.
17772
+ *
17773
+ * @returns WalletEventsManager instance
17774
+ *
17775
+ * @example Basic Usage
17776
+ * ```typescript
17777
+ * // Configure SDK with events URL
17778
+ * sdk.configureWalletEvents({ wsUrl: 'wss://events.pers.ninja' });
17779
+ *
17780
+ * // Connect after authentication
17781
+ * await sdk.auth.loginWithToken(jwt, 'user');
17782
+ * await sdk.walletEvents.connect();
17783
+ *
17784
+ * // Listen for token transfers
17785
+ * sdk.walletEvents.on('Transfer', (event) => {
17786
+ * if (event.data.to === myWallet) {
17787
+ * showNotification(`Received ${event.data.value} tokens!`);
17788
+ * }
17789
+ * });
17790
+ * ```
17791
+ *
17792
+ * @example Unified Event Stream
17793
+ * ```typescript
17794
+ * // Wallet events also flow through sdk.events
17795
+ * sdk.events.subscribe((event) => {
17796
+ * if (event.domain === 'wallet') {
17797
+ * console.log('Wallet event:', event.type);
17798
+ * }
17799
+ * });
17800
+ * ```
17801
+ *
17802
+ * @see {@link WalletEventsManager} for detailed documentation
17803
+ */
17804
+ get walletEvents() {
17805
+ if (!this._walletEvents) {
17806
+ this._walletEvents = new WalletEventsManager(this.apiClient, this._events, this.walletEventsConfig);
17807
+ }
17808
+ return this._walletEvents;
17809
+ }
17810
+ /**
17811
+ * Configure wallet events (call before accessing walletEvents)
17812
+ *
17813
+ * @param config - Events configuration including wsUrl
17814
+ */
17815
+ configureWalletEvents(config) {
17816
+ this.walletEventsConfig = config;
17817
+ // Reset manager so it picks up new config
17818
+ this._walletEvents = undefined;
17819
+ }
15166
17820
  /**
15167
17821
  * Gets the API client for direct PERS API requests
15168
17822
  *
@@ -15233,281 +17887,6 @@ function detectEnvironment() {
15233
17887
  */
15234
17888
  detectEnvironment();
15235
17889
 
15236
- /**
15237
- * Centralized Cache Service for PERS SDK
15238
- * Simple, efficient caching with memory management
15239
- */
15240
- const DEFAULT_CACHE_CONFIG = {
15241
- defaultTtl: 30 * 60 * 1000, // 30 minutes
15242
- maxMemoryMB: 50, // 50MB cache limit
15243
- cleanupInterval: 5 * 60 * 1000, // Cleanup every 5 minutes
15244
- maxEntries: 10000, // Entry count limit
15245
- evictionBatchPercent: 0.2, // Remove 20% when evicting
15246
- };
15247
- // Constants for memory estimation
15248
- const ENTRY_OVERHEAD_BYTES = 64;
15249
- const DEFAULT_OBJECT_SIZE_BYTES = 1024;
15250
- const MEMORY_CHECK_INTERVAL_MS = 60000; // Lazy check every 60s
15251
- class CacheService {
15252
- constructor(config) {
15253
- this.cache = new Map();
15254
- this.cleanupTimer = null;
15255
- this.lastMemoryCheck = 0;
15256
- this.pendingFetches = new Map();
15257
- this.accessOrder = new Set();
15258
- this.stats = {
15259
- hits: 0,
15260
- misses: 0,
15261
- errors: 0
15262
- };
15263
- this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
15264
- this.startCleanupTimer();
15265
- }
15266
- /**
15267
- * Set a value in cache with optional TTL
15268
- */
15269
- set(key, value, ttl) {
15270
- if (!key || value === undefined)
15271
- return;
15272
- const entry = {
15273
- value,
15274
- timestamp: Date.now(),
15275
- ttl: ttl || this.config.defaultTtl,
15276
- lastAccessed: Date.now()
15277
- };
15278
- this.cache.set(key, entry);
15279
- this.accessOrder.add(key);
15280
- // Fast entry count check first
15281
- if (this.cache.size > this.config.maxEntries) {
15282
- this.evictOldestEntries();
15283
- }
15284
- // Lazy memory check (only every 10s)
15285
- const now = Date.now();
15286
- if (now - this.lastMemoryCheck > MEMORY_CHECK_INTERVAL_MS) {
15287
- this.lastMemoryCheck = now;
15288
- this.enforceMemoryLimit();
15289
- }
15290
- }
15291
- /**
15292
- * Get a value from cache
15293
- */
15294
- get(key) {
15295
- const entry = this.cache.get(key);
15296
- if (!entry) {
15297
- this.stats.misses++;
15298
- return null;
15299
- }
15300
- // Check if expired
15301
- const now = Date.now();
15302
- if (now - entry.timestamp > entry.ttl) {
15303
- this.cache.delete(key);
15304
- this.accessOrder.delete(key);
15305
- this.stats.misses++;
15306
- return null;
15307
- }
15308
- // Update access order efficiently
15309
- this.accessOrder.delete(key);
15310
- this.accessOrder.add(key);
15311
- entry.lastAccessed = now;
15312
- this.stats.hits++;
15313
- return entry.value;
15314
- }
15315
- /**
15316
- * Get or set pattern - common caching pattern with race condition protection
15317
- */
15318
- async getOrSet(key, fetcher, ttl) {
15319
- const cached = this.get(key);
15320
- if (cached !== null)
15321
- return cached;
15322
- // Prevent duplicate fetches
15323
- if (this.pendingFetches.has(key)) {
15324
- return this.pendingFetches.get(key);
15325
- }
15326
- const fetchPromise = fetcher()
15327
- .then(value => {
15328
- this.pendingFetches.delete(key);
15329
- if (value !== undefined) {
15330
- this.set(key, value, ttl);
15331
- }
15332
- return value;
15333
- })
15334
- .catch(error => {
15335
- this.pendingFetches.delete(key);
15336
- this.stats.errors++;
15337
- throw error;
15338
- });
15339
- this.pendingFetches.set(key, fetchPromise);
15340
- return fetchPromise;
15341
- }
15342
- /**
15343
- * Delete a specific key
15344
- */
15345
- delete(key) {
15346
- this.accessOrder.delete(key);
15347
- return this.cache.delete(key);
15348
- }
15349
- /**
15350
- * Clear all keys matching a prefix
15351
- */
15352
- clearByPrefix(prefix) {
15353
- const keysToDelete = Array.from(this.cache.keys()).filter(key => key.startsWith(prefix));
15354
- keysToDelete.forEach(key => {
15355
- this.cache.delete(key);
15356
- this.accessOrder.delete(key);
15357
- });
15358
- return keysToDelete.length;
15359
- }
15360
- /**
15361
- * Clear all cache
15362
- */
15363
- clear() {
15364
- this.cache.clear();
15365
- this.accessOrder.clear();
15366
- this.pendingFetches.clear();
15367
- this.stats = { hits: 0, misses: 0, errors: 0 };
15368
- }
15369
- /**
15370
- * Force cleanup of expired entries
15371
- */
15372
- cleanup() {
15373
- const now = Date.now();
15374
- const keysToDelete = [];
15375
- for (const [key, entry] of this.cache.entries()) {
15376
- if (now - entry.timestamp > entry.ttl) {
15377
- keysToDelete.push(key);
15378
- }
15379
- }
15380
- keysToDelete.forEach(key => {
15381
- this.cache.delete(key);
15382
- this.accessOrder.delete(key);
15383
- });
15384
- return keysToDelete.length;
15385
- }
15386
- /**
15387
- * Stop cleanup timer and clear cache
15388
- */
15389
- destroy() {
15390
- this.stopCleanupTimer();
15391
- this.clear();
15392
- }
15393
- /**
15394
- * Create a namespaced cache interface
15395
- */
15396
- createNamespace(namespace) {
15397
- if (!namespace || namespace.includes(':')) {
15398
- throw new Error('Namespace must be non-empty and cannot contain ":"');
15399
- }
15400
- return {
15401
- set: (key, value, ttl) => this.set(`${namespace}:${key}`, value, ttl),
15402
- get: (key) => this.get(`${namespace}:${key}`),
15403
- getOrSet: (key, fetcher, ttl) => this.getOrSet(`${namespace}:${key}`, fetcher, ttl),
15404
- delete: (key) => this.delete(`${namespace}:${key}`),
15405
- clear: () => this.clearByPrefix(`${namespace}:`)
15406
- };
15407
- }
15408
- startCleanupTimer() {
15409
- this.stopCleanupTimer();
15410
- this.cleanupTimer = setInterval(() => {
15411
- this.safeCleanup();
15412
- }, this.config.cleanupInterval);
15413
- // Platform-agnostic: Prevent hanging process in Node.js environments
15414
- if (this.cleanupTimer && typeof this.cleanupTimer.unref === 'function') {
15415
- this.cleanupTimer.unref();
15416
- }
15417
- }
15418
- stopCleanupTimer() {
15419
- if (this.cleanupTimer) {
15420
- clearInterval(this.cleanupTimer);
15421
- this.cleanupTimer = null;
15422
- }
15423
- }
15424
- safeCleanup() {
15425
- try {
15426
- this.cleanup();
15427
- this.enforceMemoryLimit();
15428
- }
15429
- catch (error) {
15430
- this.stats.errors++;
15431
- // Platform-agnostic error logging
15432
- if (typeof console !== 'undefined' && console.warn) {
15433
- console.warn('Cache cleanup error:', error);
15434
- }
15435
- }
15436
- }
15437
- estimateValueSize(value) {
15438
- if (typeof value === 'string') {
15439
- return value.length * 2; // UTF-16 encoding
15440
- }
15441
- if (typeof value === 'number' || typeof value === 'boolean') {
15442
- return 8; // Primitive values
15443
- }
15444
- if (value === null || value === undefined) {
15445
- return 4;
15446
- }
15447
- try {
15448
- // More accurate JSON-based estimation for serializable objects
15449
- const jsonString = JSON.stringify(value);
15450
- return jsonString.length * 2;
15451
- }
15452
- catch {
15453
- // Fallback for non-serializable objects
15454
- if (Array.isArray(value)) {
15455
- return value.length * 100; // Better estimate for arrays
15456
- }
15457
- if (value && typeof value === 'object') {
15458
- return Object.keys(value).length * 50; // Better estimate for objects
15459
- }
15460
- return DEFAULT_OBJECT_SIZE_BYTES;
15461
- }
15462
- }
15463
- estimateMemoryUsage() {
15464
- let totalSize = 0;
15465
- for (const [key, entry] of this.cache.entries()) {
15466
- totalSize += key.length * 2; // Key size
15467
- totalSize += this.estimateValueSize(entry.value);
15468
- totalSize += ENTRY_OVERHEAD_BYTES;
15469
- }
15470
- return totalSize / (1024 * 1024);
15471
- }
15472
- enforceMemoryLimit() {
15473
- const memoryUsage = this.estimateMemoryUsage();
15474
- if (memoryUsage <= this.config.maxMemoryMB || this.cache.size === 0)
15475
- return;
15476
- this.evictOldestEntries();
15477
- }
15478
- evictOldestEntries() {
15479
- if (this.cache.size === 0)
15480
- return;
15481
- const batchSize = Math.floor(this.cache.size * this.config.evictionBatchPercent);
15482
- const toRemove = Math.max(batchSize, 1);
15483
- // Efficiently remove oldest entries using access order
15484
- const iterator = this.accessOrder.values();
15485
- for (let i = 0; i < toRemove; i++) {
15486
- const result = iterator.next();
15487
- if (result.done)
15488
- break;
15489
- const key = result.value;
15490
- this.cache.delete(key);
15491
- this.accessOrder.delete(key);
15492
- }
15493
- }
15494
- }
15495
- // Singleton instance
15496
- const globalCacheService = new CacheService();
15497
-
15498
- /**
15499
- * Cache module exports
15500
- */
15501
- // Predefined cache TTL constants
15502
- const CacheTTL = {
15503
- SHORT: 5 * 60 * 1000, // 5 minutes
15504
- MEDIUM: 30 * 60 * 1000, // 30 minutes
15505
- LONG: 24 * 60 * 60 * 1000, // 24 hours
15506
- METADATA: 24 * 60 * 60 * 1000, // 24 hours - IPFS metadata is immutable
15507
- GATEWAY: 30 * 60 * 1000, // 30 minutes - Gateway config can change
15508
- PROVIDER: 60 * 60 * 1000, // 1 hour - Provider connections with auth
15509
- };
15510
-
15511
17890
  /**
15512
17891
  * AsyncStorage Token Storage for React Native Mobile Platforms
15513
17892
  *
@@ -16052,7 +18431,35 @@ class ReactNativeHttpClient {
16052
18431
 
16053
18432
  // Create the context
16054
18433
  const SDKContext = react.createContext(null);
16055
- // Provider component
18434
+ /**
18435
+ * PERS SDK Provider for React Native
18436
+ *
18437
+ * Wraps your app to provide SDK context to all child components.
18438
+ * Handles platform-specific initialization (DPoP, storage, etc.).
18439
+ *
18440
+ * @param config - SDK configuration (see PersConfig)
18441
+ * @param config.apiProjectKey - Your PERS project key (required)
18442
+ * @param config.environment - 'staging' | 'production' (default: 'staging')
18443
+ * @param config.captureWalletEvents - Enable real-time blockchain events (default: true)
18444
+ * @param config.dpop - DPoP configuration for enhanced security
18445
+ *
18446
+ * @example Basic usage
18447
+ * ```tsx
18448
+ * <PersSDKProvider config={{ apiProjectKey: 'my-project' }}>
18449
+ * <App />
18450
+ * </PersSDKProvider>
18451
+ * ```
18452
+ *
18453
+ * @example Disable wallet events
18454
+ * ```tsx
18455
+ * <PersSDKProvider config={{
18456
+ * apiProjectKey: 'my-project',
18457
+ * captureWalletEvents: false // Disable auto-connect to blockchain events
18458
+ * }}>
18459
+ * <App />
18460
+ * </PersSDKProvider>
18461
+ * ```
18462
+ */
16056
18463
  const PersSDKProvider = ({ children, config }) => {
16057
18464
  const initializingRef = react.useRef(false);
16058
18465
  // State refs for stable functions to read current values
@@ -36196,6 +38603,7 @@ class Web3ChainApi {
36196
38603
  // ==========================================
36197
38604
  /**
36198
38605
  * PUBLIC: Get chain data by chain ID
38606
+ * Returns ChainDataDTO from the API (rpcUrl may be optional)
36199
38607
  */
36200
38608
  async getChainData(chainId) {
36201
38609
  try {
@@ -36423,7 +38831,7 @@ class TokenDomainService {
36423
38831
  const abi = convertAbiToInterface(params.abi);
36424
38832
  const tokenUri = await this.web3Api.getTokenUri({ ...params, abi });
36425
38833
  const metadata = tokenUri
36426
- ? await this.metadataService.fetchAndProcessMetadata(tokenUri, params.chainId)
38834
+ ? await this.metadataService.fetchAndProcessMetadata(tokenUri)
36427
38835
  : null;
36428
38836
  return { tokenId: params.tokenId, tokenUri, metadata };
36429
38837
  }
@@ -36614,17 +39022,19 @@ class TokenDomainService {
36614
39022
 
36615
39023
  /**
36616
39024
  * MetadataDomainService - Clean IPFS metadata resolution with centralized caching
39025
+ *
39026
+ * IPFS gateway is resolved from tenant configuration (chain-agnostic).
36617
39027
  */
36618
39028
  class MetadataDomainService {
36619
39029
  constructor(ipfsApi) {
36620
39030
  this.ipfsApi = ipfsApi;
36621
39031
  this.cache = globalCacheService.createNamespace('metadata');
36622
39032
  }
36623
- async fetchAndProcessMetadata(tokenUri, chainId) {
36624
- const cacheKey = `fetch:${tokenUri}:${chainId}`;
39033
+ async fetchAndProcessMetadata(tokenUri) {
39034
+ const cacheKey = `fetch:${tokenUri}`;
36625
39035
  return this.cache.getOrSet(cacheKey, async () => {
36626
39036
  try {
36627
- return await this.ipfsApi.fetchAndProcessMetadata(tokenUri, chainId);
39037
+ return await this.ipfsApi.fetchAndProcessMetadata(tokenUri);
36628
39038
  }
36629
39039
  catch (error) {
36630
39040
  console.error(`Error fetching metadata for ${tokenUri}:`, error);
@@ -36632,9 +39042,9 @@ class MetadataDomainService {
36632
39042
  }
36633
39043
  }, CacheTTL.METADATA);
36634
39044
  }
36635
- async resolveIPFSUrl(url, chainId) {
36636
- const cacheKey = `resolve:${url}:${chainId}`;
36637
- return this.cache.getOrSet(cacheKey, () => this.ipfsApi.resolveIPFSUrl(url, chainId), CacheTTL.GATEWAY);
39045
+ async resolveIPFSUrl(url) {
39046
+ const cacheKey = `resolve:${url}`;
39047
+ return this.cache.getOrSet(cacheKey, () => this.ipfsApi.resolveIPFSUrl(url), CacheTTL.GATEWAY);
36638
39048
  }
36639
39049
  /**
36640
39050
  * Clear all cached metadata and URLs for this service
@@ -36734,19 +39144,22 @@ class Web3ApplicationService {
36734
39144
  });
36735
39145
  }
36736
39146
  /**
36737
- * Resolve IPFS URLs to HTTPS if needed
39147
+ * Resolve IPFS URLs to HTTPS if needed.
39148
+ *
39149
+ * IPFS gateway is resolved from tenant configuration (chain-agnostic).
36738
39150
  */
36739
- async resolveIPFSUrl(url, chainId) {
36740
- return this.metadataDomainService.resolveIPFSUrl(url, chainId);
39151
+ async resolveIPFSUrl(url) {
39152
+ return this.metadataDomainService.resolveIPFSUrl(url);
36741
39153
  }
36742
39154
  /**
36743
39155
  * Fetch and process metadata from any URI with IPFS conversion.
36744
39156
  *
36745
39157
  * Use this for ad-hoc metadata fetching when you have a tokenURI.
36746
39158
  * Normalization happens at infrastructure layer.
39159
+ * IPFS gateway is resolved from tenant configuration (chain-agnostic).
36747
39160
  */
36748
- async fetchAndProcessMetadata(tokenUri, chainId) {
36749
- return this.metadataDomainService.fetchAndProcessMetadata(tokenUri, chainId);
39161
+ async fetchAndProcessMetadata(tokenUri) {
39162
+ return this.metadataDomainService.fetchAndProcessMetadata(tokenUri);
36750
39163
  }
36751
39164
  }
36752
39165
 
@@ -36800,30 +39213,42 @@ class Web3InfrastructureApi {
36800
39213
 
36801
39214
  /**
36802
39215
  * IPFSInfrastructureApi - Infrastructure implementation for IPFS operations
36803
- * Uses Web3ChainService for IPFS gateway resolution with centralized caching
39216
+ * Uses TenantManager for IPFS gateway resolution with centralized caching
39217
+ *
39218
+ * IMPORTANT: IPFS gateway domain is now fetched from tenant configuration (TenantClientConfigDTO),
39219
+ * not from chain data. This is the correct architecture since IPFS is chain-agnostic.
36804
39220
  */
36805
39221
  class IPFSInfrastructureApi {
36806
- constructor(web3ChainService) {
36807
- this.web3ChainService = web3ChainService;
36808
- this.defaultIpfsGatewayDomain = 'pers.mypinata.cloud';
39222
+ constructor(tenantManager) {
39223
+ this.tenantManager = tenantManager;
36809
39224
  this.cache = globalCacheService.createNamespace('ipfs');
36810
39225
  }
36811
- async getIpfsGatewayDomain(chainId) {
36812
- const cacheKey = `gateway:${chainId}`;
39226
+ /**
39227
+ * Get IPFS gateway domain from tenant configuration.
39228
+ *
39229
+ * @returns The IPFS gateway domain from tenant configuration
39230
+ * @throws Error if tenant config is not found or ipfsGatewayDomain is not configured
39231
+ */
39232
+ async getIpfsGatewayDomain() {
39233
+ const cacheKey = 'gateway';
36813
39234
  return this.cache.getOrSet(cacheKey, async () => {
36814
- try {
36815
- const chainData = await this.web3ChainService.getChainDataWithCache(chainId);
36816
- return chainData.ipfsGatewayDomain || this.defaultIpfsGatewayDomain;
36817
- }
36818
- catch (error) {
36819
- console.warn(`Failed to get chain data for chainId ${chainId}, using default IPFS gateway:`, error);
36820
- return this.defaultIpfsGatewayDomain;
39235
+ const clientConfig = await this.tenantManager.getClientConfig();
39236
+ if (!clientConfig.ipfsGatewayDomain) {
39237
+ throw new Error(`IPFS gateway domain not configured for tenant. ` +
39238
+ `Please configure ipfsGatewayDomain in the tenant configuration.`);
36821
39239
  }
39240
+ return clientConfig.ipfsGatewayDomain;
36822
39241
  }, CacheTTL.GATEWAY);
36823
39242
  }
36824
- async resolveIPFSUrl(url, chainId) {
39243
+ /**
39244
+ * Resolve IPFS URL to HTTPS URL using tenant's configured gateway.
39245
+ *
39246
+ * @param url - The URL to resolve (can be ipfs:// or https://)
39247
+ * @returns Resolved HTTPS URL
39248
+ */
39249
+ async resolveIPFSUrl(url) {
36825
39250
  if (url.startsWith('ipfs://')) {
36826
- const gateway = await this.getIpfsGatewayDomain(chainId);
39251
+ const gateway = await this.getIpfsGatewayDomain();
36827
39252
  return url.replace('ipfs://', `https://${gateway}/ipfs/`);
36828
39253
  }
36829
39254
  return url;
@@ -36836,12 +39261,11 @@ class IPFSInfrastructureApi {
36836
39261
  * All downstream consumers receive normalized SDK format with resolved HTTPS URLs.
36837
39262
  *
36838
39263
  * @param tokenUri - Token URI (can be ipfs:// or https://)
36839
- * @param chainId - Chain ID for IPFS gateway selection
36840
39264
  * @returns Normalized TokenMetadata with resolved HTTPS URLs, or null on error
36841
39265
  */
36842
- async fetchAndProcessMetadata(tokenUri, chainId) {
39266
+ async fetchAndProcessMetadata(tokenUri) {
36843
39267
  try {
36844
- const resolvedUri = await this.resolveIPFSUrl(tokenUri, chainId);
39268
+ const resolvedUri = await this.resolveIPFSUrl(tokenUri);
36845
39269
  const response = await fetch(resolvedUri);
36846
39270
  if (!response.ok) {
36847
39271
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -36849,10 +39273,10 @@ class IPFSInfrastructureApi {
36849
39273
  const rawMetadata = await response.json();
36850
39274
  // Resolve IPFS URLs for media fields
36851
39275
  const resolvedImageUrl = rawMetadata.image
36852
- ? await this.resolveIPFSUrl(rawMetadata.image, chainId)
39276
+ ? await this.resolveIPFSUrl(rawMetadata.image)
36853
39277
  : '';
36854
39278
  const resolvedAnimationUrl = rawMetadata.animation_url
36855
- ? await this.resolveIPFSUrl(rawMetadata.animation_url, chainId)
39279
+ ? await this.resolveIPFSUrl(rawMetadata.animation_url)
36856
39280
  : undefined;
36857
39281
  // Extract custom properties (anything not in ERC standard)
36858
39282
  const customProperties = Object.fromEntries(Object.entries(rawMetadata).filter(([key]) => !['name', 'description', 'image', 'animation_url', 'external_url', 'attributes'].includes(key)));
@@ -36985,6 +39409,11 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
36985
39409
  * tokenIds // Required for ERC-1155, optional for ERC-721
36986
39410
  * });
36987
39411
  * ```
39412
+ *
39413
+ * ## IPFS Resolution
39414
+ *
39415
+ * For IPFS URL resolution, use `sdk.tenant.resolveIPFSUrl()` instead.
39416
+ * IPFS is chain-agnostic and configured at the tenant level.
36988
39417
  */
36989
39418
  class Web3Manager {
36990
39419
  // TODO: Add PersEventEmitter support for blockchain events
@@ -36994,14 +39423,16 @@ class Web3Manager {
36994
39423
  // 3. Emit via: this.events?.emitSuccess({ domain: 'web3', type: 'NFT_TRANSFERRED', ... })
36995
39424
  // 4. Filter to only user's address (not all transfers)
36996
39425
  // 5. Add cleanup for event listeners on SDK destroy
36997
- constructor(apiClient) {
39426
+ constructor(apiClient, tenantManager) {
36998
39427
  this.apiClient = apiClient;
39428
+ // Use provided TenantManager or create one
39429
+ this.tenantManager = tenantManager || new TenantManager(apiClient);
36999
39430
  // Initialize Web3 Chain service
37000
39431
  const web3ChainApi = new Web3ChainApi(apiClient);
37001
39432
  this.web3ChainService = new Web3ChainService(web3ChainApi);
37002
- // Initialize Web3 Application service
39433
+ // Initialize Web3 Application service with TenantManager for IPFS
37003
39434
  const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
37004
- const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.web3ChainService);
39435
+ const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
37005
39436
  this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
37006
39437
  }
37007
39438
  /**
@@ -37031,25 +39462,14 @@ class Web3Manager {
37031
39462
  async getTokenCollection(request) {
37032
39463
  return this.web3ApplicationService.getTokenCollection(request);
37033
39464
  }
37034
- /**
37035
- * Resolve IPFS URL to accessible URL
37036
- *
37037
- * @param url - IPFS URL to resolve
37038
- * @param chainId - Chain ID for context
37039
- * @returns Promise resolving to accessible URL
37040
- */
37041
- async resolveIPFSUrl(url, chainId) {
37042
- return this.web3ApplicationService.resolveIPFSUrl(url, chainId);
37043
- }
37044
39465
  /**
37045
39466
  * Fetch and process token metadata
37046
39467
  *
37047
39468
  * @param tokenUri - Token URI to fetch metadata from
37048
- * @param chainId - Chain ID for context
37049
39469
  * @returns Promise resolving to processed metadata or null if not found
37050
39470
  */
37051
- async fetchAndProcessMetadata(tokenUri, chainId) {
37052
- return this.web3ApplicationService.fetchAndProcessMetadata(tokenUri, chainId);
39471
+ async fetchAndProcessMetadata(tokenUri) {
39472
+ return this.web3ApplicationService.fetchAndProcessMetadata(tokenUri);
37053
39473
  }
37054
39474
  /**
37055
39475
  * Get blockchain chain data by chain ID
@@ -37133,7 +39553,7 @@ class Web3Manager {
37133
39553
  *
37134
39554
  * @param accountAddress - Any valid blockchain address (wallet, contract, etc.)
37135
39555
  * @param token - Token definition (from getRewardTokens, getStatusTokens, etc.)
37136
- * @param maxTokens - Maximum tokens to retrieve (default: 50)
39556
+ * @param maxTokens - Maximum tokens to retrieve (default: 100, max: 100)
37137
39557
  * @returns Promise resolving to collection result with owned tokens
37138
39558
  *
37139
39559
  * @example Query user's wallet
@@ -37153,7 +39573,7 @@ class Web3Manager {
37153
39573
  * @see {@link extractTokenIds} - Low-level helper used internally for ERC-1155
37154
39574
  * @see {@link buildCollectionRequest} - For manual request building
37155
39575
  */
37156
- async getAccountOwnedTokensFromContract(accountAddress, token, maxTokens = 50) {
39576
+ async getAccountOwnedTokensFromContract(accountAddress, token, maxTokens = 100) {
37157
39577
  // For ERC-1155, extract tokenIds from metadata
37158
39578
  const tokenIds = token.type === NativeTokenTypes.ERC1155 ? this.extractTokenIds(token) : undefined;
37159
39579
  const collection = await this.getTokenCollection({
@@ -37354,25 +39774,38 @@ const useWeb3 = () => {
37354
39774
  throw error;
37355
39775
  }
37356
39776
  }, [web3, isInitialized]);
37357
- const resolveIPFSUrl = react.useCallback(async (url, chainId) => {
37358
- if (!isInitialized || !web3) {
39777
+ /**
39778
+ * Resolve IPFS URL to HTTP gateway URL
39779
+ *
39780
+ * @deprecated Use `sdk.tenant.resolveIPFSUrl()` directly - IPFS is chain-agnostic
39781
+ * @param url - IPFS URL to resolve (ipfs://...)
39782
+ * @returns Promise resolving to HTTP gateway URL
39783
+ */
39784
+ const resolveIPFSUrl = react.useCallback(async (url) => {
39785
+ if (!isInitialized || !sdk) {
37359
39786
  throw new Error('SDK not initialized. Call initialize() first.');
37360
39787
  }
37361
39788
  try {
37362
- const result = await web3.resolveIPFSUrl(url, chainId);
39789
+ const result = await sdk.tenants.resolveIPFSUrl(url);
37363
39790
  return result;
37364
39791
  }
37365
39792
  catch (error) {
37366
39793
  console.error('Failed to resolve IPFS URL:', error);
37367
39794
  throw error;
37368
39795
  }
37369
- }, [web3, isInitialized]);
37370
- const fetchAndProcessMetadata = react.useCallback(async (tokenUri, chainId) => {
39796
+ }, [sdk, isInitialized]);
39797
+ /**
39798
+ * Fetch and process token metadata from a URI
39799
+ *
39800
+ * @param tokenUri - Token URI to fetch metadata from
39801
+ * @returns Promise resolving to processed metadata or null
39802
+ */
39803
+ const fetchAndProcessMetadata = react.useCallback(async (tokenUri) => {
37371
39804
  if (!isInitialized || !web3) {
37372
39805
  throw new Error('SDK not initialized. Call initialize() first.');
37373
39806
  }
37374
39807
  try {
37375
- const result = await web3.fetchAndProcessMetadata(tokenUri, chainId);
39808
+ const result = await web3.fetchAndProcessMetadata(tokenUri);
37376
39809
  return result;
37377
39810
  }
37378
39811
  catch (error) {
@@ -37442,7 +39875,7 @@ const useWeb3 = () => {
37442
39875
  *
37443
39876
  * @param accountAddress - Any valid blockchain address (wallet, contract, etc.)
37444
39877
  * @param token - Token definition (from getRewardTokens, getStatusTokens, etc.)
37445
- * @param maxTokens - Maximum tokens to retrieve (default: 50)
39878
+ * @param maxTokens - Maximum tokens to retrieve (default: 100, max: 100)
37446
39879
  * @returns Promise resolving to result with owned tokens
37447
39880
  * @throws Error if SDK is not initialized
37448
39881
  *
@@ -37461,7 +39894,7 @@ const useWeb3 = () => {
37461
39894
  * @see {@link extractTokenIds} - Low-level helper used internally for ERC-1155
37462
39895
  * @see {@link buildCollectionRequest} - For manual request building
37463
39896
  */
37464
- const getAccountOwnedTokensFromContract = react.useCallback(async (accountAddress, token, maxTokens = 50) => {
39897
+ const getAccountOwnedTokensFromContract = react.useCallback(async (accountAddress, token, maxTokens = 100) => {
37465
39898
  if (!isInitialized || !web3) {
37466
39899
  throw new Error('SDK not initialized. Call initialize() first.');
37467
39900
  }
@@ -37476,10 +39909,10 @@ const useWeb3 = () => {
37476
39909
  *
37477
39910
  * @param accountAddress - Any valid blockchain address (wallet, contract, etc.)
37478
39911
  * @param token - Token definition
37479
- * @param maxTokens - Maximum tokens to retrieve (default: 50)
39912
+ * @param maxTokens - Maximum tokens to retrieve (default: 100, max: 100)
37480
39913
  * @returns TokenCollectionRequest ready for getTokenCollection()
37481
39914
  */
37482
- const buildCollectionRequest = react.useCallback((accountAddress, token, maxTokens = 50) => {
39915
+ const buildCollectionRequest = react.useCallback((accountAddress, token, maxTokens = 100) => {
37483
39916
  if (!web3) {
37484
39917
  throw new Error('SDK not initialized. Call initialize() first.');
37485
39918
  }
@@ -37571,6 +40004,18 @@ const useWeb3 = () => {
37571
40004
  * ```
37572
40005
  *
37573
40006
  * @example
40007
+ * **With Wallet Events (Real-time):**
40008
+ * ```typescript
40009
+ * // Auto-refresh on Transfer, Approval, and other blockchain events
40010
+ * const { tokenBalances } = useTokenBalances({
40011
+ * accountAddress: walletAddress!,
40012
+ * availableTokens,
40013
+ * refreshOnWalletEvents: true, // Enable real-time refresh (default: true)
40014
+ * walletEventDebounceMs: 1000 // Debounce rapid events (default: 1000ms)
40015
+ * });
40016
+ * ```
40017
+ *
40018
+ * @example
37574
40019
  * **Multi-Wallet Support:**
37575
40020
  * ```typescript
37576
40021
  * function MultiWalletBalances() {
@@ -37589,12 +40034,14 @@ const useWeb3 = () => {
37589
40034
  * ```
37590
40035
  */
37591
40036
  function useTokenBalances(options) {
37592
- const { accountAddress, availableTokens = [], autoLoad = true, refreshInterval = 0 } = options;
40037
+ const { accountAddress, availableTokens = [], autoLoad = true, refreshInterval = 0, refreshOnWalletEvents = true, walletEventDebounceMs = 1000 } = options;
37593
40038
  const { isAuthenticated, sdk } = usePersSDK();
37594
40039
  const web3 = useWeb3();
37595
40040
  const [tokenBalances, setTokenBalances] = react.useState([]);
37596
40041
  const [isLoading, setIsLoading] = react.useState(false);
37597
40042
  const [error, setError] = react.useState(null);
40043
+ // Debounce ref for wallet events
40044
+ const walletEventDebounceRef = react.useRef(null);
37598
40045
  // Check if the hook is available for use
37599
40046
  const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
37600
40047
  /**
@@ -37714,6 +40161,32 @@ function useTokenBalances(options) {
37714
40161
  clearInterval(intervalId);
37715
40162
  };
37716
40163
  }, [sdk, refreshInterval, isAvailable, loadBalances]);
40164
+ // Wallet events refresh: listen for real-time blockchain events
40165
+ // Refreshes on ANY wallet domain event (Transfer, TransferSingle, etc.)
40166
+ // Debouncing prevents excessive API calls during rapid events
40167
+ react.useEffect(() => {
40168
+ if (!sdk || !refreshOnWalletEvents || !isAvailable)
40169
+ return;
40170
+ const unsubscribe = sdk.events.subscribe((event) => {
40171
+ if (event.domain === 'wallet') {
40172
+ // Debounce rapid events (batch transfers, etc.)
40173
+ if (walletEventDebounceRef.current) {
40174
+ clearTimeout(walletEventDebounceRef.current);
40175
+ }
40176
+ walletEventDebounceRef.current = setTimeout(() => {
40177
+ console.log(`[useTokenBalances] Wallet event (${event.type}), refreshing balances...`);
40178
+ loadBalances();
40179
+ walletEventDebounceRef.current = null;
40180
+ }, walletEventDebounceMs);
40181
+ }
40182
+ }, { domains: ['wallet'] });
40183
+ return () => {
40184
+ unsubscribe();
40185
+ if (walletEventDebounceRef.current) {
40186
+ clearTimeout(walletEventDebounceRef.current);
40187
+ }
40188
+ };
40189
+ }, [sdk, refreshOnWalletEvents, walletEventDebounceMs, isAvailable, loadBalances]);
37717
40190
  return {
37718
40191
  tokenBalances,
37719
40192
  isLoading,
@@ -39315,7 +41788,23 @@ const useTenants = () => {
39315
41788
 
39316
41789
  const useUsers = () => {
39317
41790
  const { sdk, isInitialized, isAuthenticated } = usePersSDK();
39318
- const getCurrentUser = react.useCallback(async () => {
41791
+ /**
41792
+ * Get the current authenticated user with optional include relations
41793
+ *
41794
+ * @param options - Query options including include relations
41795
+ * @returns Current user data
41796
+ *
41797
+ * @example
41798
+ * ```typescript
41799
+ * // Basic
41800
+ * const user = await getCurrentUser();
41801
+ *
41802
+ * // With wallets included
41803
+ * const userWithWallets = await getCurrentUser({ include: ['wallets'] });
41804
+ * console.log('Wallets:', userWithWallets.included?.wallets);
41805
+ * ```
41806
+ */
41807
+ const getCurrentUser = react.useCallback(async (options) => {
39319
41808
  if (!isInitialized || !sdk) {
39320
41809
  throw new Error('SDK not initialized. Call initialize() first.');
39321
41810
  }
@@ -39323,7 +41812,7 @@ const useUsers = () => {
39323
41812
  throw new Error('SDK not authenticated. getCurrentUser requires authentication.');
39324
41813
  }
39325
41814
  try {
39326
- const result = await sdk.users.getCurrentUser();
41815
+ const result = await sdk.users.getCurrentUser(options);
39327
41816
  return result;
39328
41817
  }
39329
41818
  catch (error) {
@@ -39347,12 +41836,28 @@ const useUsers = () => {
39347
41836
  throw error;
39348
41837
  }
39349
41838
  }, [sdk, isInitialized, isAuthenticated]);
39350
- const getUserById = react.useCallback(async (userId) => {
41839
+ /**
41840
+ * Get user by ID with optional include relations
41841
+ *
41842
+ * @param userId - User identifier (id, email, externalId, etc.)
41843
+ * @param options - Query options including include relations
41844
+ * @returns User data
41845
+ *
41846
+ * @example
41847
+ * ```typescript
41848
+ * // Basic
41849
+ * const user = await getUserById('user-123');
41850
+ *
41851
+ * // With wallets included
41852
+ * const userWithWallets = await getUserById('user-123', { include: ['wallets'] });
41853
+ * ```
41854
+ */
41855
+ const getUserById = react.useCallback(async (userId, options) => {
39351
41856
  if (!isInitialized || !sdk) {
39352
41857
  throw new Error('SDK not initialized. Call initialize() first.');
39353
41858
  }
39354
41859
  try {
39355
- const result = await sdk.users.getUserById(userId);
41860
+ const result = await sdk.users.getUserById(userId, options);
39356
41861
  return result;
39357
41862
  }
39358
41863
  catch (error) {
@@ -39400,16 +41905,23 @@ const useUsers = () => {
39400
41905
  throw error;
39401
41906
  }
39402
41907
  }, [sdk, isInitialized]);
39403
- const toggleUserStatus = react.useCallback(async (user) => {
41908
+ /**
41909
+ * Toggle or set a user's active status (admin only)
41910
+ *
41911
+ * @param userId - User ID to update
41912
+ * @param isActive - Optional explicit status. If omitted, toggles current status.
41913
+ * @returns Updated user
41914
+ */
41915
+ const setUserActiveStatus = react.useCallback(async (userId, isActive) => {
39404
41916
  if (!isInitialized || !sdk) {
39405
41917
  throw new Error('SDK not initialized. Call initialize() first.');
39406
41918
  }
39407
41919
  try {
39408
- const result = await sdk.users.toggleUserStatus(user);
41920
+ const result = await sdk.users.setUserActiveStatus(userId, isActive);
39409
41921
  return result;
39410
41922
  }
39411
41923
  catch (error) {
39412
- console.error('Failed to toggle user status:', error);
41924
+ console.error('Failed to set user active status:', error);
39413
41925
  throw error;
39414
41926
  }
39415
41927
  }, [sdk, isInitialized]);
@@ -39420,7 +41932,7 @@ const useUsers = () => {
39420
41932
  getAllUsersPublic,
39421
41933
  getAllUsers,
39422
41934
  updateUser,
39423
- toggleUserStatus,
41935
+ setUserActiveStatus,
39424
41936
  isAvailable: isInitialized && !!sdk?.users,
39425
41937
  };
39426
41938
  };
@@ -39968,23 +42480,35 @@ const useDonations = () => {
39968
42480
  * React Native hook for PERS SDK event system
39969
42481
  *
39970
42482
  * This hook provides access to the platform-agnostic event system for subscribing to
39971
- * transaction, authentication, campaign, and system events. All events include a
39972
- * `userMessage` field ready for display to end users.
42483
+ * transaction, authentication, campaign, blockchain, and system events. All events
42484
+ * include a `userMessage` field ready for display to end users.
39973
42485
  *
39974
42486
  * **Event Domains:**
39975
42487
  * - `auth` - Authentication events (login, logout, token refresh)
39976
42488
  * - `user` - User profile events (update, create)
39977
- * - `transaction` - Transaction events (created, completed, failed)
42489
+ * - `transaction` - Transaction events (created, submitted, confirmed)
39978
42490
  * - `campaign` - Campaign events (claimed, activated)
39979
42491
  * - `redemption` - Redemption events (redeemed, expired)
39980
42492
  * - `business` - Business events (created, updated, membership)
42493
+ * - `wallet` - Real-time blockchain events (Transfer, Approval, etc.) - Auto-enabled on auth
39981
42494
  * - `api` - API error events (network, validation, server errors)
39982
42495
  *
42496
+ * **Blockchain Events (Wallet Domain):**
42497
+ * When `sdk.connectWalletEvents()` is called (or auto-enabled via `captureWalletEvents: true`),
42498
+ * real-time blockchain events are automatically routed through the same event system:
42499
+ * ```typescript
42500
+ * const { subscribe } = useEvents();
42501
+ *
42502
+ * subscribe((event) => {
42503
+ * if (event.domain === 'wallet') {
42504
+ * console.log('Blockchain event:', event.type); // wallet_transfer, wallet_approval, etc.
42505
+ * }
42506
+ * });
42507
+ * ```
42508
+ *
39983
42509
  * **Notification Levels:**
39984
42510
  * - `success` - Operation completed successfully
39985
42511
  * - `error` - Operation failed
39986
- * - `warning` - Operation completed with warnings
39987
- * - `info` - Informational event
39988
42512
  *
39989
42513
  * **Cleanup:**
39990
42514
  * All subscriptions created through this hook are automatically cleaned up when
@@ -40619,9 +43143,13 @@ function getMetadataFromTokenUnitResponse(tokenUnit, incrementalId) {
40619
43143
  exports.ANALYTICS_GROUP_BY_TYPES = ANALYTICS_GROUP_BY_TYPES;
40620
43144
  exports.ANALYTICS_METRIC_TYPES = ANALYTICS_METRIC_TYPES;
40621
43145
  exports.AsyncStorageTokenStorage = AsyncStorageTokenStorage;
43146
+ exports.AuditEntityTypes = AuditEntityTypes;
40622
43147
  exports.AuthenticationError = AuthenticationError;
43148
+ exports.ChainTypes = ChainTypes;
40623
43149
  exports.ClientTransactionType = ClientTransactionType;
40624
43150
  exports.Domains = Domains;
43151
+ exports.EntityTypes = EntityTypes;
43152
+ exports.ErrorDomains = ErrorDomains;
40625
43153
  exports.ErrorUtils = ErrorUtils;
40626
43154
  exports.MEMBERSHIP_ROLE_HIERARCHY = MEMBERSHIP_ROLE_HIERARCHY;
40627
43155
  exports.NativeTokenTypes = NativeTokenTypes;
@@ -40634,15 +43162,19 @@ exports.ReactNativeSecureStorage = ReactNativeSecureStorage;
40634
43162
  exports.SOURCE_LOGIC_TYPES = SOURCE_LOGIC_TYPES;
40635
43163
  exports.SOURCE_LOGIC_TYPE_VALUES = SOURCE_LOGIC_TYPE_VALUES;
40636
43164
  exports.SigningStatus = SigningStatus;
43165
+ exports.TOKEN_VALIDITY_TYPE_VALUES = TOKEN_VALIDITY_TYPE_VALUES;
40637
43166
  exports.TRANSACTION_FORMATS = TRANSACTION_FORMATS;
40638
43167
  exports.TRANSACTION_FORMAT_DESCRIPTIONS = TRANSACTION_FORMAT_DESCRIPTIONS;
40639
43168
  exports.TRIGGER_SOURCE_TYPES = TRIGGER_SOURCE_TYPES;
40640
43169
  exports.TRIGGER_SOURCE_TYPE_VALUES = TRIGGER_SOURCE_TYPE_VALUES;
43170
+ exports.TokenValidityTypes = TokenValidityTypes;
43171
+ exports.VALID_BUSINESS_MEMBERSHIP_RELATIONS = VALID_BUSINESS_MEMBERSHIP_RELATIONS;
40641
43172
  exports.VALID_CAMPAIGN_CLAIM_RELATIONS = VALID_CAMPAIGN_CLAIM_RELATIONS;
40642
43173
  exports.VALID_CAMPAIGN_RELATIONS = VALID_CAMPAIGN_RELATIONS;
40643
43174
  exports.VALID_REDEMPTION_REDEEM_RELATIONS = VALID_REDEMPTION_REDEEM_RELATIONS;
40644
43175
  exports.VALID_RELATIONS = VALID_RELATIONS;
40645
43176
  exports.VALID_TRANSACTION_RELATIONS = VALID_TRANSACTION_RELATIONS;
43177
+ exports.VALID_USER_RELATIONS = VALID_USER_RELATIONS;
40646
43178
  exports.apiPublicKeyTestPrefix = apiPublicKeyTestPrefix;
40647
43179
  exports.buildBurnRequest = buildBurnRequest;
40648
43180
  exports.buildMintRequest = buildMintRequest;
@@ -40662,13 +43194,22 @@ exports.fetchAllPages = fetchAllPages;
40662
43194
  exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
40663
43195
  exports.hasMinimumRole = hasMinimumRole;
40664
43196
  exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
43197
+ exports.isConnectedMessage = isConnectedMessage;
43198
+ exports.isErrorMessage = isErrorMessage;
43199
+ exports.isEventMessage = isEventMessage;
40665
43200
  exports.isPaginatedResponse = isPaginatedResponse;
43201
+ exports.isPingMessage = isPingMessage;
43202
+ exports.isServerMessage = isServerMessage;
43203
+ exports.isSubscribedMessage = isSubscribedMessage;
43204
+ exports.isUnsubscribedMessage = isUnsubscribedMessage;
40666
43205
  exports.isUserIdentifierObject = isUserIdentifierObject;
43206
+ exports.isValidBusinessMembershipRelation = isValidBusinessMembershipRelation;
40667
43207
  exports.isValidCampaignClaimRelation = isValidCampaignClaimRelation;
40668
43208
  exports.isValidCampaignRelation = isValidCampaignRelation;
40669
43209
  exports.isValidRedemptionRedeemRelation = isValidRedemptionRedeemRelation;
40670
43210
  exports.isValidRelation = isValidRelation;
40671
43211
  exports.isValidTransactionRelation = isValidTransactionRelation;
43212
+ exports.isValidUserRelation = isValidUserRelation;
40672
43213
  exports.normalizeToPaginated = normalizeToPaginated;
40673
43214
  exports.registerIdentifierType = registerIdentifierType;
40674
43215
  exports.testnetPrefix = testnetPrefix;