@explorins/pers-sdk 1.2.6 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +416 -0
  2. package/dist/business/api/business-api.d.ts.map +1 -1
  3. package/dist/business.cjs +6 -8
  4. package/dist/business.cjs.map +1 -1
  5. package/dist/business.js +6 -8
  6. package/dist/business.js.map +1 -1
  7. package/dist/campaign/api/campaign-api.d.ts +19 -65
  8. package/dist/campaign/api/campaign-api.d.ts.map +1 -1
  9. package/dist/campaign.cjs +51 -105
  10. package/dist/campaign.cjs.map +1 -1
  11. package/dist/campaign.js +49 -103
  12. package/dist/campaign.js.map +1 -1
  13. package/dist/chunks/base-token-service-BA81_Ouq.js +532 -0
  14. package/dist/chunks/base-token-service-BA81_Ouq.js.map +1 -0
  15. package/dist/chunks/base-token-service-BQ6uFoki.cjs +537 -0
  16. package/dist/chunks/base-token-service-BQ6uFoki.cjs.map +1 -0
  17. package/dist/chunks/environment-C2AkkLPd.js +46 -0
  18. package/dist/chunks/environment-C2AkkLPd.js.map +1 -0
  19. package/dist/chunks/environment-CRROnwAY.cjs +50 -0
  20. package/dist/chunks/environment-CRROnwAY.cjs.map +1 -0
  21. package/dist/chunks/jwt.function-BYiyl-z_.cjs +25 -0
  22. package/dist/chunks/jwt.function-BYiyl-z_.cjs.map +1 -0
  23. package/dist/chunks/jwt.function-d6jPtBqI.js +23 -0
  24. package/dist/chunks/jwt.function-d6jPtBqI.js.map +1 -0
  25. package/dist/chunks/pers-sdk-Ct_uUMJl.cjs +1424 -0
  26. package/dist/chunks/pers-sdk-Ct_uUMJl.cjs.map +1 -0
  27. package/dist/chunks/pers-sdk-tKHGQr5x.js +1417 -0
  28. package/dist/chunks/pers-sdk-tKHGQr5x.js.map +1 -0
  29. package/dist/core/auth/api/auth-api.d.ts +5 -2
  30. package/dist/core/auth/api/auth-api.d.ts.map +1 -1
  31. package/dist/core/auth/auth-constants.d.ts +33 -0
  32. package/dist/core/auth/auth-constants.d.ts.map +1 -0
  33. package/dist/core/auth/auth-errors.d.ts +8 -0
  34. package/dist/core/auth/auth-errors.d.ts.map +1 -0
  35. package/dist/core/auth/auth-provider.interface.d.ts +49 -3
  36. package/dist/core/auth/auth-provider.interface.d.ts.map +1 -1
  37. package/dist/core/auth/create-auth-provider.d.ts.map +1 -1
  38. package/dist/core/auth/default-auth-provider.d.ts +71 -0
  39. package/dist/core/auth/default-auth-provider.d.ts.map +1 -0
  40. package/dist/core/auth/index.d.ts +1 -22
  41. package/dist/core/auth/index.d.ts.map +1 -1
  42. package/dist/core/auth/services/auth-service.d.ts +10 -1
  43. package/dist/core/auth/services/auth-service.d.ts.map +1 -1
  44. package/dist/core/auth/token-refresh.d.ts +91 -0
  45. package/dist/core/auth/token-refresh.d.ts.map +1 -0
  46. package/dist/core/auth/token-storage.d.ts +74 -0
  47. package/dist/core/auth/token-storage.d.ts.map +1 -0
  48. package/dist/core/environment.d.ts +26 -0
  49. package/dist/core/environment.d.ts.map +1 -0
  50. package/dist/core/errors/index.d.ts +80 -0
  51. package/dist/core/errors/index.d.ts.map +1 -0
  52. package/dist/core/index.d.ts +2 -1
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/pers-api-client.d.ts +184 -19
  55. package/dist/core/pers-api-client.d.ts.map +1 -1
  56. package/dist/core/pers-config.d.ts +36 -1
  57. package/dist/core/pers-config.d.ts.map +1 -1
  58. package/dist/core/utils/jwt.function.d.ts.map +1 -1
  59. package/dist/core.cjs +12 -814
  60. package/dist/core.cjs.map +1 -1
  61. package/dist/core.js +3 -803
  62. package/dist/core.js.map +1 -1
  63. package/dist/index.cjs +82 -4912
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +21 -4857
  68. package/dist/index.js.map +1 -1
  69. package/dist/package.json +147 -129
  70. package/dist/pers-sdk.d.ts +49 -7
  71. package/dist/pers-sdk.d.ts.map +1 -1
  72. package/dist/redemption/api/redemption-api.d.ts +12 -13
  73. package/dist/redemption/api/redemption-api.d.ts.map +1 -1
  74. package/dist/redemption.cjs +24 -24
  75. package/dist/redemption.cjs.map +1 -1
  76. package/dist/redemption.js +24 -24
  77. package/dist/redemption.js.map +1 -1
  78. package/dist/shared/index.d.ts +5 -0
  79. package/dist/shared/index.d.ts.map +1 -0
  80. package/dist/shared/interfaces/pers-shared-lib.interfaces.d.ts +1 -0
  81. package/dist/shared/interfaces/pers-shared-lib.interfaces.d.ts.map +1 -1
  82. package/dist/tenant/api/tenant-api.d.ts +28 -10
  83. package/dist/tenant/api/tenant-api.d.ts.map +1 -1
  84. package/dist/tenant/index.d.ts +4 -4
  85. package/dist/tenant.cjs +40 -11
  86. package/dist/tenant.cjs.map +1 -1
  87. package/dist/tenant.js +40 -11
  88. package/dist/tenant.js.map +1 -1
  89. package/dist/token.cjs +7 -534
  90. package/dist/token.cjs.map +1 -1
  91. package/dist/token.js +1 -532
  92. package/dist/token.js.map +1 -1
  93. package/dist/web3/index.d.ts.map +1 -1
  94. package/dist/web3-chain/services/getWeb3FCD.service.d.ts +1 -1
  95. package/dist/web3-chain/services/getWeb3FCD.service.d.ts.map +1 -1
  96. package/dist/web3-chain.cjs +12 -152
  97. package/dist/web3-chain.cjs.map +1 -1
  98. package/dist/web3-chain.js +8 -148
  99. package/dist/web3-chain.js.map +1 -1
  100. package/dist/web3.cjs +12 -538
  101. package/dist/web3.cjs.map +1 -1
  102. package/dist/web3.js +10 -536
  103. package/dist/web3.js.map +1 -1
  104. package/package.json +147 -129
  105. package/dist/core/auth/simple-sdk-auth-provider.d.ts +0 -27
  106. package/dist/core/auth/simple-sdk-auth-provider.d.ts.map +0 -1
@@ -0,0 +1,1424 @@
1
+ 'use strict';
2
+
3
+ var persShared = require('@explorins/pers-shared');
4
+
5
+ /**
6
+ * PERS SDK Configuration interfaces and utilities
7
+ *
8
+ * Provides type-safe configuration options for the PERS SDK
9
+ * with sensible defaults for production environments.
10
+ */
11
+ /**
12
+ * Default configuration values
13
+ */
14
+ const DEFAULT_PERS_CONFIG = {
15
+ environment: 'production',
16
+ apiVersion: 'v2',
17
+ timeout: 30000,
18
+ retries: 3,
19
+ tokenRefreshMargin: 60, // Refresh tokens 60 seconds before expiry
20
+ backgroundRefreshThreshold: 30 // Use background refresh if >30s remaining
21
+ };
22
+ /**
23
+ * Internal function to construct API root from environment
24
+ * Now defaults to production and v2
25
+ */
26
+ function buildApiRoot(environment = 'production', version = 'v2') {
27
+ const baseUrls = {
28
+ development: 'https://explorins-loyalty.ngrok.io',
29
+ staging: `https://dev.api.pers.ninja/${version}`,
30
+ production: `https://api.pers.ninja/${version}`
31
+ };
32
+ return `${baseUrls[environment]}`;
33
+ }
34
+ /**
35
+ * Merge user config with defaults
36
+ */
37
+ function mergeWithDefaults(config) {
38
+ return {
39
+ ...DEFAULT_PERS_CONFIG,
40
+ ...config,
41
+ environment: config.environment ?? DEFAULT_PERS_CONFIG.environment,
42
+ apiVersion: config.apiVersion ?? DEFAULT_PERS_CONFIG.apiVersion,
43
+ timeout: config.timeout ?? DEFAULT_PERS_CONFIG.timeout,
44
+ retries: config.retries ?? DEFAULT_PERS_CONFIG.retries
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Platform-Agnostic Auth Admin API Client
50
+ *
51
+ * Handles authentication and authorization admin operations using the PERS backend.
52
+ * Uses @explorins/pers-shared DTOs for consistency with backend.
53
+ *
54
+ * Note: Special header handling (bypass-auth-interceptor) may need to be implemented
55
+ * at the PersApiClient level or through a specialized auth client.
56
+ */
57
+ class AuthApi {
58
+ constructor(apiClient) {
59
+ this.apiClient = apiClient;
60
+ this.basePath = '/auth';
61
+ }
62
+ // ==========================================
63
+ // ADMIN AUTHENTICATION OPERATIONS
64
+ // ==========================================
65
+ /**
66
+ * ADMIN: Login tenant admin with JWT
67
+ * Note: JWT handling and auth bypass headers may need special implementation
68
+ */
69
+ async loginTenantAdmin(jwt) {
70
+ const body = {
71
+ authToken: jwt,
72
+ authType: persShared.AccountOwnerType.TENANT
73
+ };
74
+ return this.apiClient.post(`${this.basePath}/token`, body, { bypassAuth: true });
75
+ }
76
+ /**
77
+ * Login user with JWT - bypasses auth headers
78
+ */
79
+ async loginUser(jwt) {
80
+ const body = {
81
+ authToken: jwt,
82
+ authType: persShared.AccountOwnerType.USER
83
+ };
84
+ return this.apiClient.post(`${this.basePath}/token`, body, { bypassAuth: true });
85
+ }
86
+ async loginUnAuthenticated(rawLoginData) {
87
+ const body = {
88
+ authToken: '',
89
+ authType: persShared.AccountOwnerType.USER,
90
+ rawLoginData
91
+ };
92
+ return this.apiClient.post(`${this.basePath}/token`, body, { bypassAuth: true });
93
+ }
94
+ /**
95
+ * Refresh access token - bypasses auth headers to prevent circular dependency
96
+ */
97
+ async refreshAccessToken(refreshToken) {
98
+ // Bypass auth headers for refresh calls to prevent circular dependency
99
+ return this.apiClient.post(`${this.basePath}/refresh`, { refreshToken }, { bypassAuth: true });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Platform-Agnostic Auth Admin Service
105
+ *
106
+ * Contains auth admin business logic and operations that work across platforms.
107
+ * No framework dependencies - pure TypeScript business logic.
108
+ *
109
+ * Focuses only on actual backend capabilities.
110
+ */
111
+ class AuthService {
112
+ constructor(authApi, authProvider) {
113
+ this.authApi = authApi;
114
+ this.authProvider = authProvider;
115
+ }
116
+ // ==========================================
117
+ // ADMIN AUTHENTICATION OPERATIONS
118
+ // ==========================================
119
+ /**
120
+ * ADMIN: Login tenant admin with JWT
121
+ * Automatically stores tokens if auth provider supports token storage
122
+ */
123
+ async loginTenantAdmin(jwt) {
124
+ const response = await this.authApi.loginTenantAdmin(jwt);
125
+ // Store tokens if auth provider supports it
126
+ if (this.authProvider && response.accessToken) {
127
+ await this.storeTokens(response.accessToken, response.refreshToken, 'admin', jwt);
128
+ }
129
+ return response;
130
+ }
131
+ /**
132
+ * Automatically stores tokens if auth provider supports token storage
133
+ */
134
+ async loginUser(jwt) {
135
+ const response = await this.authApi.loginUser(jwt);
136
+ // Store tokens if auth provider supports it
137
+ if (this.authProvider && response.accessToken) {
138
+ await this.storeTokens(response.accessToken, response.refreshToken, 'user', jwt);
139
+ }
140
+ return response;
141
+ }
142
+ /**
143
+ * Automatically stores tokens if auth provider supports token storage
144
+ */
145
+ async loginUserWithRawData(rawLoginData) {
146
+ const loginData = {
147
+ externalId: rawLoginData?.externalId,
148
+ email: rawLoginData?.email,
149
+ firstName: rawLoginData?.firstName,
150
+ lastName: rawLoginData?.lastName,
151
+ customData: rawLoginData?.customData
152
+ };
153
+ const response = await this.authApi.loginUnAuthenticated(loginData);
154
+ // Store tokens if auth provider supports it
155
+ if (this.authProvider && response.accessToken) {
156
+ await this.storeTokens(response.accessToken, response.refreshToken, 'user');
157
+ }
158
+ return response;
159
+ }
160
+ /**
161
+ * ADMIN: Refresh access token
162
+ * Automatically stores new tokens if auth provider supports token storage
163
+ */
164
+ async refreshAccessToken(refreshToken) {
165
+ // Use provided refresh token or get from auth provider
166
+ const tokenToUse = refreshToken || (this.authProvider?.getRefreshToken ? await this.authProvider.getRefreshToken() : null);
167
+ if (!tokenToUse) {
168
+ throw new Error('No refresh token available for token refresh');
169
+ }
170
+ const response = await this.authApi.refreshAccessToken(tokenToUse);
171
+ // Store new tokens if auth provider supports it
172
+ if (this.authProvider && response.accessToken) {
173
+ await this.storeTokens(response.accessToken, response.refreshToken);
174
+ }
175
+ return response;
176
+ }
177
+ /**
178
+ * Automatic token refresh using stored refresh token
179
+ * Convenience method for 401 error handling
180
+ */
181
+ async autoRefreshToken() {
182
+ return this.refreshAccessToken(); // Uses stored refresh token
183
+ }
184
+ /**
185
+ * Clear stored tokens if auth provider supports it
186
+ */
187
+ async clearTokens() {
188
+ if (this.authProvider?.clearTokens) {
189
+ await this.authProvider.clearTokens();
190
+ }
191
+ }
192
+ /**
193
+ * Check if we have valid tokens for authentication
194
+ */
195
+ hasValidAuth() {
196
+ return this.authProvider?.hasValidToken?.() ?? false;
197
+ }
198
+ // ==========================================
199
+ // PRIVATE HELPERS
200
+ // ==========================================
201
+ /**
202
+ * Store tokens using auth provider if it supports token storage
203
+ */
204
+ async storeTokens(accessToken, refreshToken, authType, providerToken) {
205
+ if (!this.authProvider)
206
+ return;
207
+ try {
208
+ // Store access token
209
+ if (this.authProvider.setAccessToken) {
210
+ await this.authProvider.setAccessToken(accessToken);
211
+ }
212
+ // Store refresh token if provided and supported
213
+ if (refreshToken && this.authProvider.setRefreshToken) {
214
+ await this.authProvider.setRefreshToken(refreshToken);
215
+ }
216
+ // Store provider token if provided and provider supports it
217
+ if (providerToken && 'setProviderToken' in this.authProvider &&
218
+ typeof this.authProvider.setProviderToken === 'function') {
219
+ await this.authProvider.setProviderToken(providerToken);
220
+ }
221
+ // Store auth type if provided and provider supports it
222
+ if (authType && 'setAuthType' in this.authProvider &&
223
+ typeof this.authProvider.setAuthType === 'function') {
224
+ await this.authProvider.setAuthType(authType);
225
+ }
226
+ }
227
+ catch (error) {
228
+ // Don't throw - token storage failure shouldn't break authentication
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Authentication-related constants for type safety
235
+ */
236
+ /**
237
+ * Storage keys for authentication tokens
238
+ */
239
+ const AUTH_STORAGE_KEYS = {
240
+ ACCESS_TOKEN: 'pers_access_token',
241
+ REFRESH_TOKEN: 'pers_refresh_token',
242
+ PROVIDER_TOKEN: 'pers_provider_token', // Generic external JWT (Firebase, Auth0, etc.)
243
+ AUTH_TYPE: 'pers_auth_type',
244
+ };
245
+ /**
246
+ * Authentication method types
247
+ */
248
+ const AUTH_METHODS = {
249
+ GET: 'GET',
250
+ POST: 'POST',
251
+ PUT: 'PUT',
252
+ DELETE: 'DELETE',
253
+ };
254
+
255
+ /**
256
+ * Token Storage Management
257
+ *
258
+ * Handles secure token storage with different strategies
259
+ */
260
+ /**
261
+ * LocalStorage-based token storage
262
+ */
263
+ class LocalStorageTokenStorage {
264
+ async setToken(key, value) {
265
+ if (typeof localStorage !== 'undefined') {
266
+ localStorage.setItem(key, value);
267
+ }
268
+ }
269
+ async getToken(key) {
270
+ if (typeof localStorage !== 'undefined') {
271
+ return localStorage.getItem(key);
272
+ }
273
+ return null;
274
+ }
275
+ async removeToken(key) {
276
+ if (typeof localStorage !== 'undefined') {
277
+ localStorage.removeItem(key);
278
+ }
279
+ }
280
+ async clear() {
281
+ if (typeof localStorage !== 'undefined') {
282
+ Object.values(AUTH_STORAGE_KEYS).forEach(key => {
283
+ localStorage.removeItem(key);
284
+ });
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Token Manager - High-level token management
290
+ */
291
+ class TokenManager {
292
+ constructor(storage = new LocalStorageTokenStorage()) {
293
+ this.storage = storage;
294
+ }
295
+ async setAccessToken(token) {
296
+ await this.storage.setToken(AUTH_STORAGE_KEYS.ACCESS_TOKEN, token);
297
+ }
298
+ async getAccessToken() {
299
+ return this.storage.getToken(AUTH_STORAGE_KEYS.ACCESS_TOKEN);
300
+ }
301
+ async setRefreshToken(token) {
302
+ await this.storage.setToken(AUTH_STORAGE_KEYS.REFRESH_TOKEN, token);
303
+ }
304
+ async getRefreshToken() {
305
+ return this.storage.getToken(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
306
+ }
307
+ async getProviderToken() {
308
+ return await this.storage.getToken(AUTH_STORAGE_KEYS.PROVIDER_TOKEN);
309
+ }
310
+ async setTokenData(data) {
311
+ if (data.accessToken) {
312
+ await this.setAccessToken(data.accessToken);
313
+ }
314
+ if (data.refreshToken) {
315
+ await this.setRefreshToken(data.refreshToken);
316
+ }
317
+ // Could store expiration time if needed
318
+ }
319
+ async getTokenData() {
320
+ const accessToken = await this.getAccessToken();
321
+ const refreshToken = await this.getRefreshToken();
322
+ return {
323
+ accessToken: accessToken || undefined,
324
+ refreshToken: refreshToken || undefined
325
+ };
326
+ }
327
+ async clearAllTokens() {
328
+ await this.storage.clear();
329
+ }
330
+ async hasValidTokens() {
331
+ const accessToken = await this.getAccessToken();
332
+ return !!accessToken;
333
+ }
334
+ async hasRefreshToken() {
335
+ const refreshToken = await this.getRefreshToken();
336
+ return !!refreshToken;
337
+ }
338
+ async removeToken(key) {
339
+ await this.storage.removeToken(key);
340
+ }
341
+ /**
342
+ * Set auth type (user or admin)
343
+ */
344
+ async setAuthType(authType) {
345
+ await this.storage.setToken(AUTH_STORAGE_KEYS.AUTH_TYPE, authType);
346
+ }
347
+ /**
348
+ * Get stored auth type
349
+ */
350
+ async getAuthType() {
351
+ const authType = await this.storage.getToken(AUTH_STORAGE_KEYS.AUTH_TYPE);
352
+ return authType;
353
+ }
354
+ /**
355
+ * Clear auth type from storage
356
+ */
357
+ async clearAuthType() {
358
+ await this.storage.removeToken(AUTH_STORAGE_KEYS.AUTH_TYPE);
359
+ }
360
+ /**
361
+ * Set provider token (generic external JWT)
362
+ */
363
+ async setProviderToken(token) {
364
+ await this.storage.setToken(AUTH_STORAGE_KEYS.PROVIDER_TOKEN, token);
365
+ }
366
+ /**
367
+ * Clear provider token
368
+ */
369
+ async clearProviderToken() {
370
+ await this.storage.removeToken(AUTH_STORAGE_KEYS.PROVIDER_TOKEN);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * PERS SDK Error Handling - Optimized for Performance
376
+ *
377
+ * Consolidated API and auth errors for fast SDK performance
378
+ * Uses @explorins/pers-shared when available, fallback to SDK errors
379
+ */
380
+ // Fast type guards and utilities
381
+ class ErrorUtils {
382
+ /**
383
+ * Fast token expiration detection
384
+ */
385
+ static isTokenExpired(error) {
386
+ if (typeof error !== 'object' || error === null)
387
+ return false;
388
+ const err = error;
389
+ const apiError = err?.error || err?.response?.data || err;
390
+ const status = err?.status || err?.response?.status || err?.statusCode;
391
+ return apiError?.code === 'TOKEN_EXPIRED' ||
392
+ apiError?.errorCode === 'TOKEN_EXPIRED' ||
393
+ (status === 401 && apiError?.message?.toLowerCase()?.includes('token'));
394
+ }
395
+ /**
396
+ * Fast error message extraction
397
+ */
398
+ static getMessage(error) {
399
+ if (typeof error !== 'object' || error === null)
400
+ return 'Unknown error';
401
+ const err = error;
402
+ const apiError = err?.error || err?.response?.data || err;
403
+ return apiError?.message || apiError?.detail || err?.message || 'Request failed';
404
+ }
405
+ /**
406
+ * Fast status code extraction
407
+ */
408
+ static getStatus(error) {
409
+ if (typeof error !== 'object' || error === null)
410
+ return null;
411
+ const err = error;
412
+ return err?.status || err?.statusCode || err?.response?.status || null;
413
+ }
414
+ /**
415
+ * Fast retryability check
416
+ */
417
+ static isRetryable(error) {
418
+ if (typeof error !== 'object' || error === null)
419
+ return false;
420
+ const err = error;
421
+ // Check explicit retryable property first (fastest)
422
+ if (typeof err?.retryable === 'boolean')
423
+ return err.retryable;
424
+ // Fast status-based check
425
+ const status = ErrorUtils.getStatus(error);
426
+ return status === null || status >= 500 || status === 429;
427
+ }
428
+ /**
429
+ * Check if error is from PERS API (uses @explorins/pers-shared format)
430
+ */
431
+ static isPersApiError(error) {
432
+ return typeof error === 'object' && error !== null &&
433
+ 'errorCode' in error && 'domain' in error && 'category' in error;
434
+ }
435
+ }
436
+ // SDK-specific error classes for auth flows
437
+ class TokenRefreshNeeded extends Error {
438
+ constructor(refreshToken) {
439
+ super('Token refresh needed');
440
+ this.refreshToken = refreshToken;
441
+ this.errorCode = 'TOKEN_REFRESH_NEEDED';
442
+ this.domain = 'auth';
443
+ this.category = 'SECURITY';
444
+ this.retryable = true;
445
+ this.name = 'TokenRefreshNeeded';
446
+ }
447
+ }
448
+ class ProviderTokenRefreshNeeded extends Error {
449
+ constructor(providerToken) {
450
+ super('Provider token refresh needed');
451
+ this.providerToken = providerToken;
452
+ this.errorCode = 'PROVIDER_TOKEN_REFRESH_NEEDED';
453
+ this.domain = 'auth';
454
+ this.category = 'SECURITY';
455
+ this.retryable = true;
456
+ this.name = 'ProviderTokenRefreshNeeded';
457
+ }
458
+ }
459
+ class LogoutRequired extends Error {
460
+ constructor(message) {
461
+ super(message);
462
+ this.errorCode = 'LOGOUT_REQUIRED';
463
+ this.domain = 'auth';
464
+ this.category = 'SECURITY';
465
+ this.retryable = false;
466
+ this.name = 'LogoutRequired';
467
+ }
468
+ }
469
+ class PersApiError extends Error {
470
+ constructor(message, endpoint, method, status, retryable = false) {
471
+ super(`API request failed: ${message}`);
472
+ this.endpoint = endpoint;
473
+ this.method = method;
474
+ this.status = status;
475
+ this.errorCode = 'PERS_API_ERROR';
476
+ this.domain = 'api';
477
+ this.category = 'TECHNICAL';
478
+ this.name = 'PersApiError';
479
+ this.retryable = retryable;
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Token Refresh Management
485
+ *
486
+ * Handles the 6-step authentication process:
487
+ * 1. Check for provider token → get complete token set from PERS if missing
488
+ * 2. Store all tokens (access, refresh, provider)
489
+ * 3. Use access token for requests
490
+ * 4. Use refresh token if access expires → get new token set, keep provider
491
+ * 5. Fall back to provider token if refresh fails → get fresh token set from PERS
492
+ * 6. Clear all tokens if provider also fails
493
+ */
494
+ /**
495
+ * Token Refresh Manager
496
+ *
497
+ * Implements the 6-step authentication process:
498
+ * 1. Use provider token to retrieve complete token set from PERS if not present
499
+ * 2. Store all 3 tokens (access, refresh, provider)
500
+ * 3. Use access token for API requests
501
+ * 4. Use refresh token if access expires → get new token set, preserve provider token
502
+ * 5. Fall back to provider token if refresh fails → get fresh token set from PERS
503
+ * 6. Clear all tokens if provider also fails → force logout
504
+ */
505
+ class TokenRefreshManager {
506
+ constructor(tokenManager, refreshStrategy) {
507
+ this.refreshAttempts = new Map();
508
+ this.MAX_REFRESH_ATTEMPTS = 1;
509
+ this.loginRequiredListeners = [];
510
+ this.tokenManager = tokenManager;
511
+ this.refreshStrategy = refreshStrategy;
512
+ }
513
+ /**
514
+ * Add listener for login required events
515
+ */
516
+ onLoginRequired(listener) {
517
+ this.loginRequiredListeners.push(listener);
518
+ }
519
+ /**
520
+ * Remove listener for login required events
521
+ */
522
+ removeLoginRequiredListener(listener) {
523
+ const index = this.loginRequiredListeners.indexOf(listener);
524
+ if (index > -1) {
525
+ this.loginRequiredListeners.splice(index, 1);
526
+ }
527
+ }
528
+ /**
529
+ * Emit login required event to all listeners
530
+ */
531
+ emitLoginRequired(reason) {
532
+ const event = {
533
+ reason,
534
+ timestamp: new Date()
535
+ };
536
+ this.loginRequiredListeners.forEach(listener => {
537
+ try {
538
+ listener(event);
539
+ }
540
+ catch (error) {
541
+ // Listener error - continuing with other listeners
542
+ }
543
+ });
544
+ }
545
+ /**
546
+ * Handle token expiration - orchestrates the 6-step authentication process
547
+ * 1. Check for provider token → get complete token set from PERS if missing
548
+ * 2. Store all 3 tokens (access, refresh, provider)
549
+ * 3. Use access token for requests
550
+ * 4. Use refresh token if access expires → get new token set, keep provider
551
+ * 5. Fall back to provider token if refresh fails → get fresh token set from PERS
552
+ * 6. Clear all tokens if provider also fails
553
+ */
554
+ async handleTokenExpiration() {
555
+ try {
556
+ const accessToken = await this.tokenManager.getAccessToken();
557
+ const refreshToken = await this.tokenManager.getRefreshToken();
558
+ const providerToken = await this.tokenManager.getProviderToken();
559
+ // If we have no PERS tokens but have a provider token, use it to get the complete set
560
+ if (!accessToken && !refreshToken && providerToken) {
561
+ await this.executeProviderTokenFlow(providerToken);
562
+ return;
563
+ }
564
+ // Try refresh token if we have one
565
+ if (refreshToken) {
566
+ await this.executeRefreshTokenFlow(refreshToken);
567
+ return;
568
+ }
569
+ // No refresh token, try provider token
570
+ if (providerToken) {
571
+ await this.executeProviderTokenFlow(providerToken);
572
+ return;
573
+ }
574
+ // No tokens available, require login
575
+ await this.executeAuthCleanup('No authentication tokens available');
576
+ throw new LogoutRequired('No authentication tokens available');
577
+ }
578
+ catch (error) {
579
+ if (error instanceof TokenRefreshNeeded || error instanceof ProviderTokenRefreshNeeded || error instanceof LogoutRequired) {
580
+ throw error;
581
+ }
582
+ // Convert unexpected errors to login requirement
583
+ await this.executeAuthCleanup('Authentication process failed unexpectedly');
584
+ throw new LogoutRequired('Authentication process failed unexpectedly');
585
+ }
586
+ }
587
+ /**
588
+ * Execute refresh with refresh token (Step 4)
589
+ * Use refresh token to get new access token, preserve provider token
590
+ */
591
+ async executeRefreshTokenFlow(refreshToken) {
592
+ const attempts = this.refreshAttempts.get(refreshToken) || 0;
593
+ if (attempts >= this.MAX_REFRESH_ATTEMPTS) {
594
+ await this.fallbackToProviderToken();
595
+ return;
596
+ }
597
+ try {
598
+ this.refreshAttempts.set(refreshToken, attempts + 1);
599
+ const result = await this.refreshStrategy.refreshWithRefreshToken(refreshToken);
600
+ await this.storeTokenResult(result);
601
+ this.refreshAttempts.delete(refreshToken);
602
+ }
603
+ catch (error) {
604
+ await this.fallbackToProviderToken();
605
+ }
606
+ } /**
607
+ * Execute refresh with provider token (Step 5)
608
+ * Uses provider token to get a fresh token set from PERS backend
609
+ */
610
+ async executeProviderTokenFlow(providerToken) {
611
+ try {
612
+ const result = await this.refreshStrategy.refreshWithProviderToken(providerToken);
613
+ await this.storeTokenResult(result);
614
+ this.refreshAttempts.clear();
615
+ }
616
+ catch (error) {
617
+ await this.executeAuthCleanup('Provider token authentication failed - all methods exhausted');
618
+ throw new LogoutRequired('Provider token authentication failed - all methods exhausted');
619
+ }
620
+ }
621
+ async storeTokenResult(result) {
622
+ await this.tokenManager.setAccessToken(result.accessToken);
623
+ if (result.refreshToken) {
624
+ await this.tokenManager.setRefreshToken(result.refreshToken);
625
+ }
626
+ }
627
+ async fallbackToProviderToken() {
628
+ const providerToken = await this.tokenManager.getProviderToken();
629
+ if (providerToken) {
630
+ try {
631
+ await this.executeProviderTokenFlow(providerToken);
632
+ }
633
+ catch (providerError) {
634
+ await this.executeAuthCleanup('All authentication methods exhausted');
635
+ throw new LogoutRequired('All authentication methods exhausted');
636
+ }
637
+ }
638
+ else {
639
+ await this.executeAuthCleanup('Refresh failed and no provider token available');
640
+ throw new LogoutRequired('Refresh failed and no provider token available');
641
+ }
642
+ }
643
+ async clearAuthTokens() {
644
+ await this.tokenManager.removeToken(AUTH_STORAGE_KEYS.ACCESS_TOKEN);
645
+ await this.tokenManager.removeToken(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
646
+ // Clear refresh attempts tracking
647
+ this.refreshAttempts.clear();
648
+ }
649
+ /**
650
+ * Execute authentication cleanup and notify login required (Step 6)
651
+ */
652
+ async executeAuthCleanup(reason = 'Authentication failed') {
653
+ await this.tokenManager.clearAllTokens();
654
+ this.refreshAttempts.clear();
655
+ this.emitLoginRequired(reason);
656
+ }
657
+ /**
658
+ * Check if an error should trigger token refresh (React Native compatible)
659
+ */
660
+ shouldRefreshToken(error) {
661
+ return ErrorUtils.isTokenExpired(error);
662
+ }
663
+ }
664
+
665
+ /**
666
+ * DefaultAuthRefreshStrategy - Implements the actual refresh logic
667
+ */
668
+ class DefaultAuthRefreshStrategy {
669
+ constructor(tokenManager, getProviderTokenFn, authApi) {
670
+ this.tokenManager = tokenManager;
671
+ this.getProviderTokenFn = getProviderTokenFn;
672
+ this.authApi = authApi;
673
+ }
674
+ async refreshWithRefreshToken(refreshToken) {
675
+ try {
676
+ const result = await this.authApi.refreshAccessToken(refreshToken);
677
+ if (!result.accessToken) {
678
+ throw new Error('Invalid refresh response: missing accessToken');
679
+ }
680
+ return {
681
+ accessToken: result.accessToken,
682
+ refreshToken: result.refreshToken || refreshToken
683
+ };
684
+ }
685
+ catch (error) {
686
+ throw new Error(`Refresh token invalid or expired: ${error instanceof Error ? error.message : 'Unknown error'}`);
687
+ }
688
+ }
689
+ async refreshWithProviderToken(providerToken) {
690
+ try {
691
+ const storedAuthType = await this.tokenManager.getAuthType();
692
+ let result;
693
+ if (storedAuthType === 'admin') {
694
+ result = await this.authApi.loginTenantAdmin(providerToken);
695
+ }
696
+ else if (storedAuthType === 'user') {
697
+ result = await this.authApi.loginUser(providerToken);
698
+ }
699
+ else {
700
+ try {
701
+ result = await this.authApi.loginUser(providerToken);
702
+ await this.tokenManager.setAuthType('user');
703
+ }
704
+ catch (userLoginError) {
705
+ result = await this.authApi.loginTenantAdmin(providerToken);
706
+ await this.tokenManager.setAuthType('admin');
707
+ }
708
+ }
709
+ if (!result.accessToken) {
710
+ throw new Error('Invalid provider login response: missing accessToken');
711
+ }
712
+ return {
713
+ accessToken: result.accessToken,
714
+ refreshToken: result.refreshToken
715
+ };
716
+ }
717
+ catch (error) {
718
+ throw new Error(`Provider token login failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
719
+ }
720
+ }
721
+ }
722
+ /**
723
+ * Default authentication provider with modular architecture
724
+ *
725
+ * Delegates token storage to TokenManager and refresh logic to TokenRefreshManager.
726
+ * Perfect for platform-agnostic usage with reliable error handling.
727
+ */
728
+ class DefaultAuthProvider {
729
+ constructor(projectKey, authApi) {
730
+ this.projectKey = null;
731
+ this.authApi = null;
732
+ this.authType = 'admin';
733
+ this.projectKey = projectKey || null;
734
+ this.authApi = authApi || null;
735
+ this.tokenManager = new TokenManager();
736
+ const refreshStrategy = new DefaultAuthRefreshStrategy(this.tokenManager, () => Promise.resolve(this.getProviderToken()), this.authApi);
737
+ this.tokenRefreshManager = new TokenRefreshManager(this.tokenManager, refreshStrategy);
738
+ }
739
+ async getToken() {
740
+ return await this.tokenManager.getAccessToken();
741
+ }
742
+ async getProjectKey() {
743
+ return this.projectKey;
744
+ }
745
+ /**
746
+ * Centralized token refresh handler - delegates to TokenRefreshManager
747
+ */
748
+ async onTokenExpired() {
749
+ await this.tokenRefreshManager.handleTokenExpiration();
750
+ }
751
+ /**
752
+ * Check if an error indicates token expiration (React Native compatible)
753
+ */
754
+ isTokenExpiredError(error) {
755
+ return ErrorUtils.isTokenExpired(error);
756
+ }
757
+ /**
758
+ * Get Firebase or other provider token for fresh JWT request
759
+ * Type-safe method that uses proper storage keys from AUTH_STORAGE_KEYS
760
+ */
761
+ getProviderToken() {
762
+ if (typeof localStorage !== 'undefined') {
763
+ return localStorage.getItem(AUTH_STORAGE_KEYS.PROVIDER_TOKEN);
764
+ }
765
+ return null;
766
+ }
767
+ async setAccessToken(token) {
768
+ await this.tokenManager.setAccessToken(token);
769
+ }
770
+ async setRefreshToken(token) {
771
+ await this.tokenManager.setRefreshToken(token);
772
+ }
773
+ async getRefreshToken() {
774
+ return await this.tokenManager.getRefreshToken();
775
+ }
776
+ async setProviderToken(token) {
777
+ await this.tokenManager.setProviderToken(token);
778
+ }
779
+ async clearProviderToken() {
780
+ await this.tokenManager.clearProviderToken();
781
+ }
782
+ async clearTokens() {
783
+ await this.tokenManager.clearAllTokens();
784
+ }
785
+ hasValidToken() {
786
+ if (typeof localStorage !== 'undefined') {
787
+ return !!localStorage.getItem(AUTH_STORAGE_KEYS.ACCESS_TOKEN);
788
+ }
789
+ return false;
790
+ }
791
+ hasRefreshToken() {
792
+ if (typeof localStorage !== 'undefined') {
793
+ return !!localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
794
+ }
795
+ return false;
796
+ }
797
+ /**
798
+ * Proactively check token expiry and refresh if needed BEFORE making requests
799
+ * Uses smart refresh strategy based on time remaining:
800
+ * - >backgroundThreshold seconds: Background refresh (non-blocking)
801
+ * - <backgroundThreshold seconds: Immediate refresh (blocking)
802
+ */
803
+ async ensureValidToken(marginSeconds = 60, backgroundThreshold = 30) {
804
+ try {
805
+ const currentToken = await this.getToken();
806
+ // If no token, nothing to check
807
+ if (!currentToken) {
808
+ return;
809
+ }
810
+ // Check if token is expired or will expire within margin
811
+ if (await this.isTokenExpired(marginSeconds)) {
812
+ // Determine refresh strategy based on time remaining
813
+ const timeToExpiry = await this.getTokenTimeToExpiry(currentToken);
814
+ if (timeToExpiry > backgroundThreshold) {
815
+ // Token has enough time left - start background refresh (non-blocking)
816
+ this.startBackgroundRefresh();
817
+ }
818
+ else {
819
+ // Token expiring soon or expired - block and refresh immediately
820
+ const shouldSkipRefresh = await this.isRefreshTokenExpired(marginSeconds);
821
+ if (shouldSkipRefresh) {
822
+ // Both tokens expired - use provider token directly
823
+ await this.handleExpiredRefreshToken();
824
+ }
825
+ else {
826
+ // Normal refresh with refresh token
827
+ if (this.onTokenExpired) {
828
+ await this.onTokenExpired();
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+ catch (error) {
835
+ // If token check/refresh fails, the error will be handled by the calling code
836
+ throw error;
837
+ }
838
+ }
839
+ /**
840
+ * Check if current access token is expired
841
+ */
842
+ async isTokenExpired(marginSeconds = 60) {
843
+ try {
844
+ const currentToken = await this.getToken();
845
+ if (!currentToken) {
846
+ return true;
847
+ }
848
+ // Import isTokenExpired function here to avoid circular imports
849
+ const { isTokenExpired } = await Promise.resolve().then(function () { return require('./jwt.function-BYiyl-z_.cjs'); });
850
+ return isTokenExpired(currentToken, marginSeconds);
851
+ }
852
+ catch (error) {
853
+ return true;
854
+ }
855
+ }
856
+ /**
857
+ * Check if refresh token is expired
858
+ */
859
+ async isRefreshTokenExpired(marginSeconds = 60) {
860
+ try {
861
+ const refreshToken = await this.tokenManager.getRefreshToken();
862
+ if (!refreshToken) {
863
+ return true; // No refresh token
864
+ }
865
+ // Import isTokenExpired function here to avoid circular imports
866
+ const { isTokenExpired } = await Promise.resolve().then(function () { return require('./jwt.function-BYiyl-z_.cjs'); });
867
+ return isTokenExpired(refreshToken, marginSeconds);
868
+ }
869
+ catch (error) {
870
+ return true; // Assume expired if we can't check
871
+ }
872
+ }
873
+ /**
874
+ * Check if both access and refresh tokens are expired
875
+ */
876
+ async areAllTokensExpired(marginSeconds = 60) {
877
+ const accessTokenExpired = await this.isTokenExpired(marginSeconds);
878
+ const refreshTokenExpired = await this.isRefreshTokenExpired(marginSeconds);
879
+ return accessTokenExpired && refreshTokenExpired;
880
+ }
881
+ /**
882
+ * Get seconds until token expires
883
+ */
884
+ async getTokenTimeToExpiry(token) {
885
+ try {
886
+ const { isTokenExpired } = await Promise.resolve().then(function () { return require('./jwt.function-BYiyl-z_.cjs'); });
887
+ const { jwtDecode } = await import('jwt-decode');
888
+ const decoded = jwtDecode(token);
889
+ const currentTime = Math.floor(Date.now() / 1000);
890
+ return Math.max(0, decoded.exp - currentTime);
891
+ }
892
+ catch (error) {
893
+ return 0; // Assume expired if can't decode
894
+ }
895
+ }
896
+ /**
897
+ * Start refresh in background without blocking current request
898
+ */
899
+ startBackgroundRefresh() {
900
+ // Use setTimeout to avoid blocking the current request
901
+ setTimeout(async () => {
902
+ try {
903
+ if (this.onTokenExpired) {
904
+ await this.onTokenExpired();
905
+ }
906
+ }
907
+ catch (error) {
908
+ // Background refresh failed - next request will trigger reactive refresh
909
+ }
910
+ }, 0);
911
+ }
912
+ /**
913
+ * Handle case where refresh token is also expired - use provider token directly
914
+ */
915
+ async handleExpiredRefreshToken() {
916
+ try {
917
+ // Get provider token if available
918
+ const providerToken = this.getProviderToken();
919
+ if (!providerToken) {
920
+ // No provider token available - let normal refresh handle the failure
921
+ if (this.onTokenExpired) {
922
+ await this.onTokenExpired();
923
+ }
924
+ return;
925
+ }
926
+ // Clear expired PERS tokens and use provider token to get fresh ones
927
+ await this.clearTokens();
928
+ // Trigger refresh which should now use provider token path
929
+ if (this.onTokenExpired) {
930
+ await this.onTokenExpired();
931
+ }
932
+ }
933
+ catch (error) {
934
+ // If provider token flow fails, let normal refresh handle it
935
+ if (this.onTokenExpired) {
936
+ await this.onTokenExpired();
937
+ }
938
+ }
939
+ }
940
+ }
941
+
942
+ // packages/pers-sdk/src/core/pers-api-client.ts
943
+ /**
944
+ * PERS API Client - Core platform-agnostic client for PERS backend
945
+ *
946
+ * Provides authenticated HTTP client with automatic token management,
947
+ * proactive refresh, and comprehensive error handling.
948
+ *
949
+ * Features:
950
+ * - Automatic token refresh before expiry
951
+ * - Background refresh for optimal performance
952
+ * - Provider token fallback for seamless authentication
953
+ * - Configurable retry and timeout settings
954
+ * - Platform-agnostic design
955
+ *
956
+ * @example
957
+ * ```typescript
958
+ * const client = new PersApiClient(httpClient, {
959
+ * environment: 'production',
960
+ * apiProjectKey: 'your-project-key',
961
+ * authProvider: createAuthProvider({
962
+ * tokenProvider: () => getFirebaseToken()
963
+ * })
964
+ * });
965
+ *
966
+ * // Make authenticated requests
967
+ * const data = await client.get('/users/me');
968
+ * ```
969
+ */
970
+ class PersApiClient {
971
+ /**
972
+ * Creates a new PERS API Client instance
973
+ *
974
+ * @param httpClient - Platform-specific HTTP client implementation
975
+ * @param config - Configuration options for the API client
976
+ */
977
+ constructor(httpClient, config) {
978
+ this.httpClient = httpClient;
979
+ this.config = config;
980
+ // Merge user config with defaults (production + v2)
981
+ this.mergedConfig = mergeWithDefaults(config);
982
+ // Build API root from merged environment and version
983
+ this.apiRoot = buildApiRoot(this.mergedConfig.environment, this.mergedConfig.apiVersion);
984
+ // Initialize auth services for direct authentication
985
+ this.authApi = new AuthApi(this);
986
+ // Auto-create auth provider if none provided
987
+ if (!this.mergedConfig.authProvider) {
988
+ this.mergedConfig.authProvider = new DefaultAuthProvider(this.mergedConfig.apiProjectKey, this.authApi);
989
+ }
990
+ this.authService = new AuthService(this.authApi, this.mergedConfig.authProvider);
991
+ }
992
+ /**
993
+ * Ensures valid authentication token before making requests
994
+ *
995
+ * Implements intelligent refresh strategy:
996
+ * - Tokens with sufficient time remaining: Background refresh (non-blocking)
997
+ * - Tokens expiring soon or expired: Immediate refresh (blocking)
998
+ *
999
+ * @private
1000
+ * @returns Promise that resolves when token validation is complete
1001
+ */
1002
+ async ensureValidToken() {
1003
+ if (!this.mergedConfig.authProvider?.ensureValidToken) {
1004
+ return; // Auth provider doesn't support proactive validation
1005
+ }
1006
+ try {
1007
+ const refreshMargin = this.mergedConfig.tokenRefreshMargin || 60;
1008
+ const backgroundThreshold = this.mergedConfig.backgroundRefreshThreshold || 30;
1009
+ await this.mergedConfig.authProvider.ensureValidToken(refreshMargin, backgroundThreshold);
1010
+ }
1011
+ catch (error) {
1012
+ // If token check/refresh fails, continue with request
1013
+ // The reactive error handling will catch any auth issues
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Get request headers including auth token and project key
1018
+ */
1019
+ async getHeaders() {
1020
+ const headers = {
1021
+ 'Content-Type': 'application/json',
1022
+ };
1023
+ // Add authentication token
1024
+ if (this.mergedConfig.authProvider) {
1025
+ const token = await this.mergedConfig.authProvider.getToken();
1026
+ if (token) {
1027
+ headers['Authorization'] = `Bearer ${token}`;
1028
+ }
1029
+ }
1030
+ // Add project key
1031
+ if (this.mergedConfig.authProvider) {
1032
+ const projectKey = await this.mergedConfig.authProvider.getProjectKey();
1033
+ if (projectKey) {
1034
+ headers['x-project-key'] = projectKey;
1035
+ }
1036
+ }
1037
+ else if (this.mergedConfig.apiProjectKey) {
1038
+ // Fallback to config project key if no auth provider
1039
+ headers['x-project-key'] = this.mergedConfig.apiProjectKey;
1040
+ }
1041
+ return headers;
1042
+ }
1043
+ /**
1044
+ * Make a request with proper headers, auth, and error handling
1045
+ */
1046
+ async request(method, endpoint, body, options) {
1047
+ const { retryCount = 0, responseType = 'json', bypassAuth = false } = options || {};
1048
+ const url = `${this.apiRoot}${endpoint}`;
1049
+ // Proactive token expiry check and refresh BEFORE making the request
1050
+ if (!bypassAuth && this.mergedConfig.authProvider && retryCount === 0) {
1051
+ await this.ensureValidToken();
1052
+ }
1053
+ const requestOptions = {
1054
+ headers: bypassAuth ? await this.getHeadersWithoutAuth() : await this.getHeaders(),
1055
+ timeout: this.mergedConfig.timeout,
1056
+ responseType
1057
+ };
1058
+ // Log API request with auth info
1059
+ // const hasAuth = !!this.mergedConfig.authProvider;
1060
+ endpoint.includes('/export/csv');
1061
+ try {
1062
+ let result;
1063
+ switch (method) {
1064
+ case AUTH_METHODS.GET:
1065
+ result = await this.httpClient.get(url, requestOptions);
1066
+ break;
1067
+ case AUTH_METHODS.POST:
1068
+ result = await this.httpClient.post(url, body, requestOptions);
1069
+ break;
1070
+ case AUTH_METHODS.PUT:
1071
+ result = await this.httpClient.put(url, body, requestOptions);
1072
+ break;
1073
+ case AUTH_METHODS.DELETE:
1074
+ result = await this.httpClient.delete(url, requestOptions);
1075
+ break;
1076
+ default:
1077
+ throw new Error(`Unsupported HTTP method: ${method}`);
1078
+ }
1079
+ return result;
1080
+ }
1081
+ catch (error) {
1082
+ // Error handling - proactive token refresh should prevent most 401s
1083
+ const status = ErrorUtils.getStatus(error);
1084
+ const errorMessage = ErrorUtils.getMessage(error);
1085
+ // Fallback: reactive token refresh only if proactive check missed something
1086
+ if (retryCount === 0 && this.mergedConfig.authProvider && ErrorUtils.isTokenExpired(error)) {
1087
+ try {
1088
+ // Fallback token refresh delegation
1089
+ const result = await this.handleTokenRefreshDelegation(method, endpoint, body, options);
1090
+ if (result !== null) {
1091
+ return result;
1092
+ }
1093
+ }
1094
+ catch (refreshError) {
1095
+ throw new PersApiError(`Auth failed: ${refreshError.message || refreshError}`, endpoint, method, 401);
1096
+ }
1097
+ }
1098
+ throw new PersApiError(errorMessage, endpoint, method, status || undefined, ErrorUtils.isRetryable(error));
1099
+ }
1100
+ }
1101
+ /**
1102
+ * Delegate token refresh to auth provider and handle the results
1103
+ */
1104
+ async handleTokenRefreshDelegation(method, endpoint, body, options) {
1105
+ try {
1106
+ // Let auth provider handle the refresh process
1107
+ const authProvider = this.mergedConfig.authProvider;
1108
+ if (authProvider?.onTokenExpired) {
1109
+ await authProvider.onTokenExpired();
1110
+ }
1111
+ // If we get here, tokens should be refreshed - retry the request
1112
+ // Auth provider refresh succeeded, retrying...
1113
+ return this.request(method, endpoint, body, { ...options, retryCount: 1 });
1114
+ }
1115
+ catch (refreshError) {
1116
+ // Auth provider handled all refresh attempts and failed
1117
+ // Re-throw the error for the caller to handle
1118
+ throw refreshError;
1119
+ }
1120
+ }
1121
+ /**
1122
+ * Performs an authenticated GET request
1123
+ *
1124
+ * @template T - Expected response type
1125
+ * @param endpoint - API endpoint path (without base URL)
1126
+ * @param responseType - Expected response format
1127
+ * @returns Promise resolving to typed response data
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * const user = await client.get<User>('/users/123');
1132
+ * const csvData = await client.get('/export/data', 'blob');
1133
+ * ```
1134
+ */
1135
+ async get(endpoint, responseType) {
1136
+ return this.request(AUTH_METHODS.GET, endpoint, undefined, { responseType });
1137
+ }
1138
+ /**
1139
+ * Performs an authenticated POST request
1140
+ *
1141
+ * @template T - Expected response type
1142
+ * @param endpoint - API endpoint path (without base URL)
1143
+ * @param body - Request payload data
1144
+ * @param options - Request options including auth bypass
1145
+ * @returns Promise resolving to typed response data
1146
+ *
1147
+ * @example
1148
+ * ```typescript
1149
+ * const user = await client.post<User>('/users', userData);
1150
+ * const publicData = await client.post('/public/contact', formData, { bypassAuth: true });
1151
+ * ```
1152
+ */
1153
+ async post(endpoint, body, options) {
1154
+ return this.request(AUTH_METHODS.POST, endpoint, body, options);
1155
+ }
1156
+ /**
1157
+ * Generic PUT request
1158
+ */
1159
+ async put(endpoint, body) {
1160
+ return this.request(AUTH_METHODS.PUT, endpoint, body);
1161
+ }
1162
+ /**
1163
+ * Generic DELETE request
1164
+ */
1165
+ async delete(endpoint) {
1166
+ return this.request(AUTH_METHODS.DELETE, endpoint);
1167
+ }
1168
+ /**
1169
+ * Get request headers WITHOUT auth token (for auth operations like refresh/login)
1170
+ */
1171
+ async getHeadersWithoutAuth() {
1172
+ const headers = {
1173
+ 'Content-Type': 'application/json',
1174
+ };
1175
+ // Add project key only (no auth token)
1176
+ if (this.mergedConfig.authProvider) {
1177
+ const projectKey = await this.mergedConfig.authProvider.getProjectKey();
1178
+ if (projectKey) {
1179
+ headers['x-project-key'] = projectKey;
1180
+ }
1181
+ }
1182
+ else if (this.mergedConfig.apiProjectKey) {
1183
+ headers['x-project-key'] = this.mergedConfig.apiProjectKey;
1184
+ }
1185
+ return headers;
1186
+ }
1187
+ // ==========================================
1188
+ // AUTHENTICATION METHODS
1189
+ // ==========================================
1190
+ /**
1191
+ * Authenticates an admin user using external JWT token
1192
+ *
1193
+ * Exchanges external provider token (Firebase, Auth0, etc.) for PERS access tokens.
1194
+ * Automatically stores received tokens for subsequent requests.
1195
+ *
1196
+ * @param externalJwt - JWT token from external authentication provider
1197
+ * @returns Promise resolving to session context with admin permissions
1198
+ *
1199
+ * @example
1200
+ * ```typescript
1201
+ * const firebaseToken = await getIdToken();
1202
+ * const session = await client.loginAdmin(firebaseToken);
1203
+ * console.log('Admin authenticated:', session.user.email);
1204
+ * ```
1205
+ */
1206
+ async loginAdmin(externalJwt) {
1207
+ return this.authService.loginTenantAdmin(externalJwt);
1208
+ }
1209
+ /**
1210
+ * Authenticates a regular user using external JWT token
1211
+ *
1212
+ * Exchanges external provider token for PERS access tokens with user-level permissions.
1213
+ * Automatically stores received tokens for subsequent requests.
1214
+ *
1215
+ * @param externalJwt - JWT token from external authentication provider
1216
+ * @returns Promise resolving to session context with user permissions
1217
+ *
1218
+ * @example
1219
+ * ```typescript
1220
+ * const firebaseToken = await getIdToken();
1221
+ * const session = await client.loginUser(firebaseToken);
1222
+ * console.log('User authenticated:', session.user.email);
1223
+ * ```
1224
+ */
1225
+ async loginUser(externalJwt) {
1226
+ return this.authService.loginUser(externalJwt);
1227
+ }
1228
+ /**
1229
+ * Authenticates a user using raw login data (no external JWT)
1230
+ *
1231
+ * Useful for custom authentication flows where user data is provided directly.
1232
+ * Automatically stores received tokens for subsequent requests.
1233
+ *
1234
+ * @param rawLoginData - Object containing user login data (email, name, etc.)
1235
+ * @return Promise resolving to session context with user permissions
1236
+ */
1237
+ async loginUserWithRawData(rawLoginData) {
1238
+ return this.authService.loginUserWithRawData(rawLoginData);
1239
+ }
1240
+ /**
1241
+ * Checks if current user has a valid authentication token
1242
+ *
1243
+ * Performs basic token availability check without network requests.
1244
+ * For comprehensive validation including expiry, use isTokenExpired().
1245
+ *
1246
+ * @returns True if valid token exists, false otherwise
1247
+ *
1248
+ * @example
1249
+ * ```typescript
1250
+ * if (client.hasValidAuth()) {
1251
+ * // User is authenticated, proceed with API calls
1252
+ * const data = await client.get('/protected-data');
1253
+ * } else {
1254
+ * // Redirect to login
1255
+ * redirectToLogin();
1256
+ * }
1257
+ * ```
1258
+ */
1259
+ hasValidAuth() {
1260
+ return this.mergedConfig.authProvider?.hasValidToken?.() || false;
1261
+ }
1262
+ /**
1263
+ * Checks if current access token is expired or expiring soon
1264
+ *
1265
+ * @param marginSeconds - Seconds before expiry to consider token as expired (default: 60)
1266
+ * @returns Promise resolving to true if token is expired/expiring, false if valid
1267
+ *
1268
+ * @example
1269
+ * ```typescript
1270
+ * if (await client.isTokenExpired(120)) {
1271
+ * console.log('Token expires within 2 minutes');
1272
+ * // Optionally trigger manual refresh
1273
+ * }
1274
+ * ```
1275
+ */
1276
+ async isTokenExpired(marginSeconds = 60) {
1277
+ if (!this.mergedConfig.authProvider?.isTokenExpired) {
1278
+ return true; // No auth provider or doesn't support expiry checking
1279
+ }
1280
+ try {
1281
+ return await this.mergedConfig.authProvider.isTokenExpired(marginSeconds);
1282
+ }
1283
+ catch (error) {
1284
+ return true;
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Checks if both access and refresh tokens are expired
1289
+ *
1290
+ * Useful for determining if full re-authentication is required.
1291
+ *
1292
+ * @param marginSeconds - Seconds before expiry to consider tokens as expired (default: 60)
1293
+ * @returns Promise resolving to true if both tokens expired, false otherwise
1294
+ *
1295
+ * @example
1296
+ * ```typescript
1297
+ * if (await client.areAllTokensExpired()) {
1298
+ * // Full re-authentication required
1299
+ * await redirectToLogin();
1300
+ * }
1301
+ * ```
1302
+ */
1303
+ async areAllTokensExpired(marginSeconds = 60) {
1304
+ if (!this.mergedConfig.authProvider?.areAllTokensExpired) {
1305
+ // Fallback to checking access token only
1306
+ return await this.isTokenExpired(marginSeconds);
1307
+ }
1308
+ return await this.mergedConfig.authProvider.areAllTokensExpired(marginSeconds);
1309
+ }
1310
+ /**
1311
+ * Refresh access token using stored refresh token
1312
+ *
1313
+ * @param refreshToken - Optional refresh token, uses stored token if not provided
1314
+ * @returns Promise resolving to new auth tokens
1315
+ *
1316
+ * @example
1317
+ * ```typescript
1318
+ * try {
1319
+ * const tokens = await client.refreshTokens();
1320
+ * console.log('Tokens refreshed successfully');
1321
+ * } catch (error) {
1322
+ * console.error('Token refresh failed:', error);
1323
+ * }
1324
+ * ```
1325
+ */
1326
+ async refreshTokens(refreshToken) {
1327
+ return this.authService.refreshAccessToken(refreshToken);
1328
+ }
1329
+ /**
1330
+ * Get current configuration (returns merged config)
1331
+ */
1332
+ getConfig() {
1333
+ return this.mergedConfig;
1334
+ }
1335
+ /**
1336
+ * Get original user configuration
1337
+ */
1338
+ getOriginalConfig() {
1339
+ return this.config;
1340
+ }
1341
+ }
1342
+
1343
+ /**
1344
+ * PERS SDK - Platform-agnostic TypeScript SDK for PERS API
1345
+ *
1346
+ * Provides a simple wrapper around the core API client with
1347
+ * intelligent authentication and token management.
1348
+ */
1349
+ /**
1350
+ * Main PERS SDK class
1351
+ *
1352
+ * Minimal wrapper around PersApiClient providing a clean interface
1353
+ * for platform-specific implementations.
1354
+ *
1355
+ * @example
1356
+ * ```typescript
1357
+ * import { createPersSDK, createAuthProvider } from '@explorins/pers-sdk/core';
1358
+ * import { BrowserHttpClient } from '@explorins/pers-sdk/browser';
1359
+ *
1360
+ * const authProvider = createAuthProvider({
1361
+ * tokenProvider: () => getFirebaseToken()
1362
+ * });
1363
+ *
1364
+ * const sdk = new PersSDK(new BrowserHttpClient(), {
1365
+ * environment: 'production',
1366
+ * apiProjectKey: 'your-project-key',
1367
+ * authProvider
1368
+ * });
1369
+ *
1370
+ * const apiClient = sdk.api();
1371
+ * const user = await apiClient.get('/users/me');
1372
+ * ```
1373
+ */
1374
+ class PersSDK {
1375
+ /**
1376
+ * Creates a new PERS SDK instance
1377
+ *
1378
+ * @param httpClient Platform-specific HTTP client implementation
1379
+ * @param config SDK configuration options
1380
+ */
1381
+ constructor(httpClient, config) {
1382
+ this.apiClient = new PersApiClient(httpClient, config);
1383
+ }
1384
+ /**
1385
+ * Gets the API client for making PERS API requests
1386
+ *
1387
+ * This is the main interface for interacting with the PERS backend.
1388
+ * The returned client handles authentication, token refresh, and error handling automatically.
1389
+ *
1390
+ * @returns Configured PersApiClient instance
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * const apiClient = sdk.api();
1395
+ * const user = await apiClient.get<User>('/users/me');
1396
+ * await apiClient.post('/users', userData);
1397
+ * ```
1398
+ */
1399
+ api() {
1400
+ return this.apiClient;
1401
+ }
1402
+ /**
1403
+ * Checks if SDK is configured for production environment
1404
+ *
1405
+ * @returns True if environment is 'production', false otherwise
1406
+ */
1407
+ isProduction() {
1408
+ return this.apiClient.getConfig().environment === 'production';
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Simple factory function
1413
+ */
1414
+ function createPersSDK(httpClient, config) {
1415
+ return new PersSDK(httpClient, config);
1416
+ }
1417
+
1418
+ exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
1419
+ exports.PersApiClient = PersApiClient;
1420
+ exports.PersSDK = PersSDK;
1421
+ exports.buildApiRoot = buildApiRoot;
1422
+ exports.createPersSDK = createPersSDK;
1423
+ exports.mergeWithDefaults = mergeWithDefaults;
1424
+ //# sourceMappingURL=pers-sdk-Ct_uUMJl.cjs.map