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