@aifabrix/miso-client 3.1.0 → 3.1.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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/services/auth.service.js +2 -2
  3. package/dist/types/data-client.types.d.ts +1 -0
  4. package/dist/types/data-client.types.d.ts.map +1 -1
  5. package/dist/types/data-client.types.js.map +1 -1
  6. package/dist/utils/auth-strategy.js +2 -2
  7. package/dist/utils/data-client-audit.d.ts +24 -0
  8. package/dist/utils/data-client-audit.d.ts.map +1 -0
  9. package/dist/utils/data-client-audit.js +127 -0
  10. package/dist/utils/data-client-audit.js.map +1 -0
  11. package/dist/utils/data-client-auth.d.ts +60 -0
  12. package/dist/utils/data-client-auth.d.ts.map +1 -0
  13. package/dist/utils/data-client-auth.js +386 -0
  14. package/dist/utils/data-client-auth.js.map +1 -0
  15. package/dist/utils/data-client-cache.d.ts +36 -0
  16. package/dist/utils/data-client-cache.d.ts.map +1 -0
  17. package/dist/utils/data-client-cache.js +55 -0
  18. package/dist/utils/data-client-cache.js.map +1 -0
  19. package/dist/utils/data-client-request.d.ts +32 -0
  20. package/dist/utils/data-client-request.d.ts.map +1 -0
  21. package/dist/utils/data-client-request.js +306 -0
  22. package/dist/utils/data-client-request.js.map +1 -0
  23. package/dist/utils/data-client-utils.d.ts +49 -0
  24. package/dist/utils/data-client-utils.d.ts.map +1 -0
  25. package/dist/utils/data-client-utils.js +139 -0
  26. package/dist/utils/data-client-utils.js.map +1 -0
  27. package/dist/utils/data-client.d.ts +14 -31
  28. package/dist/utils/data-client.d.ts.map +1 -1
  29. package/dist/utils/data-client.js +67 -677
  30. package/dist/utils/data-client.js.map +1 -1
  31. package/dist/utils/internal-http-client.d.ts.map +1 -1
  32. package/dist/utils/internal-http-client.js +7 -3
  33. package/dist/utils/internal-http-client.js.map +1 -1
  34. package/package.json +1 -1
@@ -3,135 +3,15 @@
3
3
  * DataClient - Browser-compatible HTTP client wrapper around MisoClient
4
4
  * Provides enhanced HTTP capabilities with ISO 27001 compliance, caching, retry, and more
5
5
  */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
6
  Object.defineProperty(exports, "__esModule", { value: true });
10
7
  exports.DataClient = void 0;
11
8
  exports.dataClient = dataClient;
12
9
  const index_1 = require("../index");
13
- const data_client_types_1 = require("../types/data-client.types");
14
10
  const data_masker_1 = require("./data-masker");
15
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
16
- const token_utils_1 = require("./token-utils");
17
- /**
18
- * Check if running in browser environment
19
- */
20
- function isBrowser() {
21
- return (typeof globalThis.window !== "undefined" &&
22
- typeof globalThis.localStorage !== "undefined" &&
23
- typeof globalThis.fetch !== "undefined");
24
- }
25
- /**
26
- * Get value from localStorage (browser only)
27
- */
28
- function getLocalStorage(key) {
29
- if (!isBrowser())
30
- return null;
31
- try {
32
- return globalThis.localStorage.getItem(key);
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- /**
39
- * Set value in localStorage (browser only)
40
- */
41
- function setLocalStorage(key, value) {
42
- if (!isBrowser())
43
- return;
44
- try {
45
- globalThis.localStorage.setItem(key, value);
46
- }
47
- catch {
48
- // Ignore localStorage errors (SSR, private browsing, etc.)
49
- }
50
- }
51
- /**
52
- * Remove value from localStorage (browser only)
53
- */
54
- function removeLocalStorage(key) {
55
- if (!isBrowser())
56
- return;
57
- try {
58
- globalThis.localStorage.removeItem(key);
59
- }
60
- catch {
61
- // Ignore localStorage errors (SSR, private browsing, etc.)
62
- }
63
- }
64
- /**
65
- * Extract userId from JWT token
66
- */
67
- function extractUserIdFromToken(token) {
68
- try {
69
- const decoded = jsonwebtoken_1.default.decode(token);
70
- if (!decoded)
71
- return null;
72
- return (decoded.sub || decoded.userId || decoded.user_id || decoded.id);
73
- }
74
- catch {
75
- return null;
76
- }
77
- }
78
- /**
79
- * Calculate cache key from endpoint and options
80
- */
81
- function generateCacheKey(endpoint, options) {
82
- const method = options?.method || "GET";
83
- const body = options?.body ? JSON.stringify(options.body) : "";
84
- return `data-client:${method}:${endpoint}:${body}`;
85
- }
86
- /**
87
- * Truncate large payloads before masking (performance optimization)
88
- */
89
- function truncatePayload(data, maxSize) {
90
- const json = JSON.stringify(data);
91
- if (json.length <= maxSize) {
92
- return { data, truncated: false };
93
- }
94
- return {
95
- data: { _message: "Payload truncated for performance", _size: json.length },
96
- truncated: true,
97
- };
98
- }
99
- /**
100
- * Calculate request/response sizes
101
- */
102
- function calculateSize(data) {
103
- try {
104
- return JSON.stringify(data).length;
105
- }
106
- catch {
107
- return 0;
108
- }
109
- }
110
- /**
111
- * Check if error is retryable
112
- */
113
- function isRetryableError(statusCode, _error) {
114
- if (!statusCode)
115
- return true; // Network errors are retryable
116
- if (statusCode >= 500)
117
- return true; // Server errors
118
- if (statusCode === 408)
119
- return true; // Timeout
120
- if (statusCode === 429)
121
- return true; // Rate limit
122
- if (statusCode === 401 || statusCode === 403)
123
- return false; // Auth errors
124
- if (statusCode >= 400 && statusCode < 500)
125
- return false; // Client errors
126
- return false;
127
- }
128
- /**
129
- * Calculate exponential backoff delay
130
- */
131
- function calculateBackoffDelay(attempt, baseDelay, maxDelay) {
132
- const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
133
- return delay + Math.random() * 1000; // Add jitter
134
- }
11
+ const data_client_utils_1 = require("./data-client-utils");
12
+ const data_client_cache_1 = require("./data-client-cache");
13
+ const data_client_request_1 = require("./data-client-request");
14
+ const data_client_auth_1 = require("./data-client-auth");
135
15
  class DataClient {
136
16
  constructor(config) {
137
17
  this.misoClient = null;
@@ -172,7 +52,7 @@ class DataClient {
172
52
  };
173
53
  // Security: Warn if clientSecret is provided in browser environment
174
54
  // This is a security risk as clientSecret should never be exposed in client-side code
175
- if (isBrowser() && this.config.misoConfig?.clientSecret) {
55
+ if ((0, data_client_utils_1.isBrowser)() && this.config.misoConfig?.clientSecret) {
176
56
  console.warn("⚠️ SECURITY WARNING: clientSecret detected in browser environment. " +
177
57
  "Client secrets should NEVER be exposed in client-side code. " +
178
58
  "Use the client token pattern instead (clientToken + onClientTokenRefresh). " +
@@ -180,7 +60,36 @@ class DataClient {
180
60
  }
181
61
  // Initialize MisoClient if config provided
182
62
  if (this.config.misoConfig) {
183
- this.misoClient = new index_1.MisoClient(this.config.misoConfig);
63
+ // Automatically bridge DataClient.getEnvironmentToken() to MisoClient
64
+ // This allows MisoClient's logger service to get client tokens automatically
65
+ // Users don't need to manually provide onClientTokenRefresh!
66
+ const misoConfigWithRefresh = {
67
+ ...this.config.misoConfig,
68
+ // Only auto-bridge if:
69
+ // 1. User hasn't provided onClientTokenRefresh (allow override)
70
+ // 2. We're in browser (server-side uses clientSecret)
71
+ // 3. No clientSecret provided (would use that instead)
72
+ onClientTokenRefresh: this.config.misoConfig.onClientTokenRefresh ||
73
+ ((0, data_client_utils_1.isBrowser)() && !this.config.misoConfig.clientSecret
74
+ ? async () => {
75
+ const token = await this.getEnvironmentToken();
76
+ if (!token) {
77
+ throw new Error("Failed to get client token");
78
+ }
79
+ // Get expiration from localStorage (set by getEnvironmentToken)
80
+ const expiresAtStr = (0, data_client_utils_1.getLocalStorage)("miso:client-token-expires-at");
81
+ const expiresAt = expiresAtStr
82
+ ? parseInt(expiresAtStr, 10)
83
+ : Date.now() + 3600000; // Default 1 hour
84
+ const expiresIn = Math.floor((expiresAt - Date.now()) / 1000);
85
+ return {
86
+ token,
87
+ expiresIn: expiresIn > 0 ? expiresIn : 3600, // Default 1 hour if invalid
88
+ };
89
+ }
90
+ : undefined),
91
+ };
92
+ this.misoClient = new index_1.MisoClient(misoConfigWithRefresh);
184
93
  }
185
94
  // Initialize DataMasker with config path if provided
186
95
  if (this.config.misoConfig?.sensitiveFieldsConfig) {
@@ -191,57 +100,35 @@ class DataClient {
191
100
  * Get authentication token from localStorage
192
101
  */
193
102
  getToken() {
194
- if (!isBrowser())
195
- return null;
196
- const keys = this.config.tokenKeys || ["token", "accessToken", "authToken"];
197
- for (const key of keys) {
198
- const token = getLocalStorage(key);
199
- if (token)
200
- return token;
201
- }
202
- return null;
103
+ return (0, data_client_auth_1.getToken)(this.config.tokenKeys);
203
104
  }
204
105
  /**
205
106
  * Check if client token is available (from localStorage cache or config)
206
107
  */
207
108
  hasClientToken() {
208
- if (!isBrowser()) {
209
- // Server-side: check if misoClient config has clientSecret
210
- if (this.misoClient && this.config.misoConfig?.clientSecret) {
211
- return true;
212
- }
213
- return false;
214
- }
215
- // Browser-side: check localStorage cache
216
- const cachedToken = getLocalStorage("miso:client-token");
217
- if (cachedToken) {
218
- const expiresAtStr = getLocalStorage("miso:client-token-expires-at");
219
- if (expiresAtStr) {
220
- const expiresAt = parseInt(expiresAtStr, 10);
221
- if (expiresAt > Date.now()) {
222
- return true; // Valid cached token
223
- }
224
- }
225
- }
226
- // Check config token
227
- if (this.config.misoConfig?.clientToken) {
228
- return true;
229
- }
230
- // Check if misoClient config has onClientTokenRefresh callback (browser pattern)
231
- if (this.config.misoConfig?.onClientTokenRefresh) {
232
- return true; // Has means to get client token
233
- }
234
- // Check if misoClient config has clientSecret (server-side fallback)
235
- if (this.config.misoConfig?.clientSecret) {
236
- return true;
237
- }
238
- return false;
109
+ return (0, data_client_auth_1.hasClientToken)(this.misoClient, this.config.misoConfig);
239
110
  }
240
111
  /**
241
112
  * Check if any authentication token is available (user token OR client token)
242
113
  */
243
114
  hasAnyToken() {
244
- return this.getToken() !== null || this.hasClientToken();
115
+ return (0, data_client_auth_1.hasAnyToken)(this.config.tokenKeys, this.misoClient, this.config.misoConfig);
116
+ }
117
+ /**
118
+ * Get client token for requests
119
+ * Checks localStorage cache first, then config, then calls getEnvironmentToken() if needed
120
+ * @returns Client token string or null if unavailable
121
+ */
122
+ async getClientToken() {
123
+ return (0, data_client_auth_1.getClientToken)(this.config.misoConfig, this.config.baseUrl, () => this.getEnvironmentToken());
124
+ }
125
+ /**
126
+ * Build controller URL from configuration
127
+ * Uses controllerPublicUrl (browser) or controllerUrl (fallback)
128
+ * @returns Controller base URL or null if not configured
129
+ */
130
+ getControllerUrl() {
131
+ return (0, data_client_auth_1.getControllerUrl)(this.config.misoConfig);
245
132
  }
246
133
  /**
247
134
  * Check if user is authenticated
@@ -251,95 +138,19 @@ class DataClient {
251
138
  }
252
139
  /**
253
140
  * Redirect to login page via controller
254
- * Calls the controller login endpoint with redirect parameter
141
+ * Calls the controller login endpoint with redirect parameter and x-client-token header
255
142
  * @param redirectUrl - Optional redirect URL to return to after login (defaults to current page URL)
256
143
  */
257
144
  async redirectToLogin(redirectUrl) {
258
- if (!isBrowser())
259
- return;
260
- // If misoClient is not available, fallback to static loginUrl
261
- if (!this.misoClient) {
262
- const loginUrl = this.config.loginUrl || "/login";
263
- const fullUrl = /^https?:\/\//i.test(loginUrl)
264
- ? loginUrl
265
- : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
266
- globalThis.window.location.href = fullUrl;
267
- return;
268
- }
269
- try {
270
- // Get redirect URL - use provided URL or current page URL
271
- const currentUrl = globalThis.window.location.href;
272
- const finalRedirectUrl = redirectUrl || currentUrl;
273
- // Call controller login endpoint with redirect parameter
274
- const response = await this.misoClient.login({ redirect: finalRedirectUrl });
275
- // Redirect to the login URL returned by controller
276
- if (response.data?.loginUrl) {
277
- globalThis.window.location.href = response.data.loginUrl;
278
- }
279
- else {
280
- // Fallback if loginUrl not in response
281
- const loginUrl = this.config.loginUrl || "/login";
282
- const fullUrl = /^https?:\/\//i.test(loginUrl)
283
- ? loginUrl
284
- : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
285
- globalThis.window.location.href = fullUrl;
286
- }
287
- }
288
- catch (error) {
289
- // On error, fallback to static loginUrl
290
- console.error("Failed to get login URL from controller:", error);
291
- const loginUrl = this.config.loginUrl || "/login";
292
- const fullUrl = /^https?:\/\//i.test(loginUrl)
293
- ? loginUrl
294
- : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
295
- globalThis.window.location.href = fullUrl;
296
- }
145
+ return (0, data_client_auth_1.redirectToLogin)(this.config, () => this.getClientToken(), redirectUrl);
297
146
  }
298
147
  /**
299
148
  * Logout user and redirect
300
- * Calls logout API, clears tokens from localStorage, clears cache, and redirects
149
+ * Calls logout API with x-client-token header, clears tokens from localStorage, clears cache, and redirects
301
150
  * @param redirectUrl - Optional redirect URL after logout (defaults to logoutUrl or loginUrl)
302
151
  */
303
152
  async logout(redirectUrl) {
304
- if (!isBrowser())
305
- return;
306
- const token = this.getToken();
307
- // Call logout API if misoClient available and token exists
308
- if (this.misoClient && token) {
309
- try {
310
- await this.misoClient.logout({ token });
311
- }
312
- catch (error) {
313
- // Log error but continue with cleanup (logout should always clear local state)
314
- console.error("Logout API call failed:", error);
315
- }
316
- }
317
- // Clear tokens from localStorage (always, even if API call failed)
318
- const keys = this.config.tokenKeys || ["token", "accessToken", "authToken"];
319
- keys.forEach(key => {
320
- try {
321
- const storage = globalThis.localStorage;
322
- if (storage) {
323
- storage.removeItem(key);
324
- }
325
- }
326
- catch (e) {
327
- // Ignore localStorage errors (SSR, private browsing, etc.)
328
- }
329
- });
330
- // Clear HTTP cache
331
- this.clearCache();
332
- // Determine redirect URL: redirectUrl param > logoutUrl config > loginUrl config > '/login'
333
- const finalRedirectUrl = redirectUrl ||
334
- this.config.logoutUrl ||
335
- this.config.loginUrl ||
336
- "/login";
337
- // Construct full URL
338
- const fullUrl = /^https?:\/\//i.test(finalRedirectUrl)
339
- ? finalRedirectUrl
340
- : `${globalThis.window.location.origin}${finalRedirectUrl.startsWith("/") ? finalRedirectUrl : `/${finalRedirectUrl}`}`;
341
- // Redirect
342
- globalThis.window.location.href = fullUrl;
153
+ return (0, data_client_auth_1.logout)(this.config, () => this.getToken(), () => this.getClientToken(), () => this.clearCache(), redirectUrl);
343
154
  }
344
155
  /**
345
156
  * Set interceptors
@@ -409,141 +220,21 @@ class DataClient {
409
220
  : 0,
410
221
  };
411
222
  }
412
- /**
413
- * Check if endpoint should skip audit logging
414
- */
415
- shouldSkipAudit(endpoint) {
416
- if (!this.config.audit?.enabled)
417
- return true;
418
- const skipEndpoints = this.config.audit.skipEndpoints || [];
419
- return skipEndpoints.some((skip) => endpoint.includes(skip));
420
- }
421
- /**
422
- * Log audit event (ISO 27001 compliance)
423
- * Skips audit logging if no authentication token is available (user token OR client token)
424
- */
425
- async logAuditEvent(method, url, statusCode, duration, requestSize, responseSize, error, requestHeaders, responseHeaders, requestBody, responseBody) {
426
- if (this.shouldSkipAudit(url) || !this.misoClient)
427
- return;
428
- // Skip audit logging if no authentication token is available
429
- // This prevents 401 errors when attempting to audit log unauthenticated requests
430
- if (!this.hasAnyToken()) {
431
- // Silently skip audit logging for unauthenticated requests
432
- // This is expected behavior and prevents 401 errors
433
- return;
434
- }
435
- try {
436
- const token = this.getToken();
437
- const userId = token ? extractUserIdFromToken(token) : undefined;
438
- const auditLevel = this.config.audit?.level || "standard";
439
- // Build audit context based on level
440
- const auditContext = {
441
- method,
442
- url,
443
- statusCode,
444
- duration,
445
- };
446
- if (userId) {
447
- auditContext.userId = userId;
448
- }
449
- // Minimal level: only basic info
450
- if (auditLevel === "minimal") {
451
- await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
452
- return;
453
- }
454
- // Standard/Detailed/Full levels: include headers and bodies (masked)
455
- const maxResponseSize = this.config.audit?.maxResponseSize || 10000;
456
- const maxMaskingSize = this.config.audit?.maxMaskingSize || 50000;
457
- // Truncate and mask request body
458
- let maskedRequestBody = undefined;
459
- if (requestBody !== undefined) {
460
- const truncated = truncatePayload(requestBody, maxMaskingSize);
461
- if (!truncated.truncated) {
462
- maskedRequestBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
463
- }
464
- else {
465
- maskedRequestBody = truncated.data;
466
- }
467
- }
468
- // Truncate and mask response body (for standard, detailed, full levels)
469
- let maskedResponseBody = undefined;
470
- if (responseBody !== undefined) {
471
- const truncated = truncatePayload(responseBody, maxResponseSize);
472
- if (!truncated.truncated) {
473
- maskedResponseBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
474
- }
475
- else {
476
- maskedResponseBody = truncated.data;
477
- }
478
- }
479
- // Mask headers
480
- const maskedRequestHeaders = requestHeaders
481
- ? data_masker_1.DataMasker.maskSensitiveData(requestHeaders)
482
- : undefined;
483
- const maskedResponseHeaders = responseHeaders
484
- ? data_masker_1.DataMasker.maskSensitiveData(responseHeaders)
485
- : undefined;
486
- // Add to context based on level (standard, detailed, full all include headers/bodies)
487
- if (maskedRequestHeaders)
488
- auditContext.requestHeaders = maskedRequestHeaders;
489
- if (maskedResponseHeaders)
490
- auditContext.responseHeaders = maskedResponseHeaders;
491
- if (maskedRequestBody !== undefined)
492
- auditContext.requestBody = maskedRequestBody;
493
- if (maskedResponseBody !== undefined)
494
- auditContext.responseBody = maskedResponseBody;
495
- // Add sizes for detailed/full levels
496
- if (auditLevel === "detailed" || auditLevel === "full") {
497
- if (requestSize !== undefined)
498
- auditContext.requestSize = requestSize;
499
- if (responseSize !== undefined)
500
- auditContext.responseSize = responseSize;
501
- }
502
- if (error) {
503
- const maskedError = data_masker_1.DataMasker.maskSensitiveData({
504
- message: error.message,
505
- name: error.name,
506
- stack: error.stack,
507
- });
508
- auditContext.error = maskedError;
509
- }
510
- await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
511
- }
512
- catch (auditError) {
513
- // Handle audit logging errors gracefully
514
- // Don't fail main request if audit logging fails
515
- const error = auditError;
516
- const statusCode = error.statusCode || error.response?.status;
517
- if (statusCode === 401) {
518
- // User not authenticated - this is expected for unauthenticated requests
519
- // Silently skip to avoid noise (we already check hasAnyToken() before attempting)
520
- // This catch block handles edge cases where token becomes unavailable between check and audit call
521
- }
522
- else {
523
- // Other errors - log warning but don't fail request
524
- console.warn("Failed to log audit event:", auditError);
525
- }
526
- }
527
- }
528
223
  /**
529
224
  * Make HTTP request with all features (caching, retry, deduplication, audit)
530
225
  */
531
226
  async request(method, endpoint, options) {
532
227
  const startTime = Date.now();
533
228
  const fullUrl = `${this.config.baseUrl}${endpoint}`;
534
- const cacheKey = generateCacheKey(endpoint, options);
229
+ const cacheKey = (0, data_client_cache_1.getCacheKeyForRequest)(endpoint, options);
535
230
  const isGetRequest = method.toUpperCase() === "GET";
536
- const cacheEnabled = (this.config.cache?.enabled !== false) &&
537
- isGetRequest &&
538
- (options?.cache?.enabled !== false);
231
+ const cacheEnabled = (0, data_client_cache_1.isCacheEnabled)(method, this.config.cache, options);
539
232
  // Check cache for GET requests
540
233
  if (cacheEnabled) {
541
- const cached = this.cache.get(cacheKey);
542
- if (cached && cached.expiresAt > Date.now()) {
543
- this.metrics.cacheHits++;
544
- return cached.data;
234
+ const cached = (0, data_client_cache_1.getCachedEntry)(this.cache, cacheKey, this.metrics);
235
+ if (cached !== null) {
236
+ return cached;
545
237
  }
546
- this.metrics.cacheMisses++;
547
238
  }
548
239
  // Check for duplicate concurrent requests (only for GET requests)
549
240
  if (isGetRequest) {
@@ -553,7 +244,7 @@ class DataClient {
553
244
  }
554
245
  }
555
246
  // Create request promise
556
- const requestPromise = this.executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime);
247
+ const requestPromise = (0, data_client_request_1.executeHttpRequest)(method, fullUrl, endpoint, this.config, this.cache, cacheKey, cacheEnabled, startTime, this.misoClient, () => this.hasAnyToken(), () => this.getToken(), () => this.handleAuthError(), this.interceptors, this.metrics, options);
557
248
  // Store pending request (only for GET requests)
558
249
  if (isGetRequest) {
559
250
  this.pendingRequests.set(cacheKey, requestPromise);
@@ -569,212 +260,11 @@ class DataClient {
569
260
  }
570
261
  }
571
262
  }
572
- /**
573
- * Execute HTTP request with retry logic
574
- */
575
- async executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime) {
576
- const maxRetries = options?.retries !== undefined
577
- ? options.retries
578
- : this.config.retry?.maxRetries || 3;
579
- const retryEnabled = this.config.retry?.enabled !== false;
580
- let lastError;
581
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
582
- try {
583
- const response = await this.makeFetchRequest(method, fullUrl, options);
584
- const duration = Date.now() - startTime;
585
- // Update metrics
586
- this.metrics.totalRequests++;
587
- this.metrics.responseTimes.push(duration);
588
- // Parse response
589
- const data = await this.parseResponse(response);
590
- // Cache successful GET responses
591
- if (cacheEnabled && response.ok) {
592
- const ttl = options?.cache?.ttl || this.config.cache?.defaultTTL || 300;
593
- this.cache.set(cacheKey, {
594
- data,
595
- expiresAt: Date.now() + ttl * 1000,
596
- key: cacheKey,
597
- });
598
- // Enforce max cache size
599
- if (this.cache.size > (this.config.cache?.maxSize || 100)) {
600
- const firstKey = this.cache.keys().next().value;
601
- if (firstKey)
602
- this.cache.delete(firstKey);
603
- }
604
- }
605
- // Apply response interceptor
606
- if (this.interceptors.onResponse) {
607
- return await this.interceptors.onResponse(response, data);
608
- }
609
- // Audit logging
610
- if (!options?.skipAudit) {
611
- const requestSize = options?.body
612
- ? calculateSize(options.body)
613
- : undefined;
614
- const responseSize = calculateSize(data);
615
- await this.logAuditEvent(method, endpoint, response.status, duration, requestSize, responseSize, undefined, this.extractHeaders(options?.headers), this.extractHeaders(response.headers), options?.body, data);
616
- }
617
- // Handle error responses
618
- if (!response.ok) {
619
- if (response.status === 401) {
620
- this.handleAuthError();
621
- throw new data_client_types_1.AuthenticationError("Authentication required", response);
622
- }
623
- throw new data_client_types_1.ApiError(`Request failed with status ${response.status}`, response.status, response);
624
- }
625
- return data;
626
- }
627
- catch (error) {
628
- lastError = error;
629
- const duration = Date.now() - startTime;
630
- // Check if retryable
631
- const isRetryable = retryEnabled &&
632
- attempt < maxRetries &&
633
- isRetryableError(error.statusCode, error);
634
- if (!isRetryable) {
635
- this.metrics.totalFailures++;
636
- // Audit log error
637
- if (!options?.skipAudit) {
638
- await this.logAuditEvent(method, endpoint, error.statusCode || 0, duration, options?.body ? calculateSize(options.body) : undefined, undefined, error, this.extractHeaders(options?.headers), undefined, options?.body, undefined);
639
- }
640
- // Apply error interceptor
641
- if (this.interceptors.onError) {
642
- throw await this.interceptors.onError(error);
643
- }
644
- throw error;
645
- }
646
- // Calculate backoff delay
647
- const baseDelay = this.config.retry?.baseDelay || 1000;
648
- const maxDelay = this.config.retry?.maxDelay || 10000;
649
- const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);
650
- // Wait before retry
651
- await new Promise((resolve) => setTimeout(resolve, delay));
652
- }
653
- }
654
- // All retries exhausted
655
- this.metrics.totalFailures++;
656
- if (lastError) {
657
- if (this.interceptors.onError) {
658
- throw await this.interceptors.onError(lastError);
659
- }
660
- throw lastError;
661
- }
662
- throw new Error("Request failed after retries");
663
- }
664
- /**
665
- * Make fetch request with timeout and authentication
666
- */
667
- async makeFetchRequest(method, url, options) {
668
- // Build headers
669
- const headers = new Headers(this.config.defaultHeaders);
670
- if (options?.headers) {
671
- if (options.headers instanceof Headers) {
672
- options.headers.forEach((value, key) => headers.set(key, value));
673
- }
674
- else {
675
- Object.entries(options.headers).forEach(([key, value]) => {
676
- headers.set(key, String(value));
677
- });
678
- }
679
- }
680
- // Add authentication
681
- if (!options?.skipAuth) {
682
- const token = this.getToken();
683
- if (token) {
684
- headers.set("Authorization", `Bearer ${token}`);
685
- }
686
- // Note: MisoClient client-token is handled server-side, not in browser
687
- }
688
- // Create abort controller for timeout
689
- const timeout = options?.timeout || this.config.timeout || 30000;
690
- const controller = new AbortController();
691
- const timeoutId = setTimeout(() => controller.abort(), timeout);
692
- // Merge signals
693
- const signal = options?.signal
694
- ? this.mergeSignals(controller.signal, options.signal)
695
- : controller.signal;
696
- try {
697
- const response = await fetch(url, {
698
- method,
699
- headers,
700
- body: options?.body,
701
- signal,
702
- ...options,
703
- });
704
- clearTimeout(timeoutId);
705
- return response;
706
- }
707
- catch (error) {
708
- clearTimeout(timeoutId);
709
- if (error instanceof Error && error.name === "AbortError") {
710
- throw new data_client_types_1.TimeoutError(`Request timeout after ${timeout}ms`, timeout);
711
- }
712
- throw new data_client_types_1.NetworkError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
713
- }
714
- }
715
- /**
716
- * Merge AbortSignals
717
- */
718
- mergeSignals(signal1, signal2) {
719
- const controller = new AbortController();
720
- const abort = () => {
721
- controller.abort();
722
- signal1.removeEventListener("abort", abort);
723
- signal2.removeEventListener("abort", abort);
724
- };
725
- if (signal1.aborted || signal2.aborted) {
726
- controller.abort();
727
- return controller.signal;
728
- }
729
- signal1.addEventListener("abort", abort);
730
- signal2.addEventListener("abort", abort);
731
- return controller.signal;
732
- }
733
- /**
734
- * Parse response based on content type
735
- */
736
- async parseResponse(response) {
737
- const contentType = response.headers.get("content-type") || "";
738
- if (contentType.includes("application/json")) {
739
- return (await response.json());
740
- }
741
- if (contentType.includes("text/")) {
742
- return (await response.text());
743
- }
744
- return (await response.blob());
745
- }
746
- /**
747
- * Extract headers from Headers object or Record
748
- */
749
- extractHeaders(headers) {
750
- if (!headers)
751
- return undefined;
752
- if (headers instanceof Headers) {
753
- const result = {};
754
- headers.forEach((value, key) => {
755
- result[key] = value;
756
- });
757
- return result;
758
- }
759
- if (Array.isArray(headers)) {
760
- const result = {};
761
- headers.forEach(([key, value]) => {
762
- result[key] = String(value);
763
- });
764
- return result;
765
- }
766
- // Convert Record<string, string | readonly string[]> to Record<string, string>
767
- const result = {};
768
- Object.entries(headers).forEach(([key, value]) => {
769
- result[key] = Array.isArray(value) ? value.join(", ") : String(value);
770
- });
771
- return result;
772
- }
773
263
  /**
774
264
  * Handle authentication error
775
265
  */
776
266
  handleAuthError() {
777
- if (isBrowser()) {
267
+ if ((0, data_client_utils_1.isBrowser)()) {
778
268
  // Fire and forget - redirect doesn't need to complete before throwing error
779
269
  this.redirectToLogin().catch((error) => {
780
270
  console.error("Failed to redirect to login:", error);
@@ -865,93 +355,7 @@ class DataClient {
865
355
  * @throws Error if token fetch fails
866
356
  */
867
357
  async getEnvironmentToken() {
868
- if (!isBrowser()) {
869
- throw new Error("getEnvironmentToken() is only available in browser environment");
870
- }
871
- const cacheKey = "miso:client-token";
872
- const expiresAtKey = "miso:client-token-expires-at";
873
- // Check cache first
874
- const cachedToken = getLocalStorage(cacheKey);
875
- const expiresAtStr = getLocalStorage(expiresAtKey);
876
- if (cachedToken && expiresAtStr) {
877
- const expiresAt = parseInt(expiresAtStr, 10);
878
- const now = Date.now();
879
- // If token is still valid, return cached token
880
- if (expiresAt > now) {
881
- return cachedToken;
882
- }
883
- // Token expired, remove from cache
884
- removeLocalStorage(cacheKey);
885
- removeLocalStorage(expiresAtKey);
886
- }
887
- // Cache miss or expired - fetch from backend
888
- const clientTokenUri = this.config.misoConfig?.clientTokenUri || "/api/v1/auth/client-token";
889
- // Build full URL
890
- const fullUrl = /^https?:\/\//i.test(clientTokenUri)
891
- ? clientTokenUri
892
- : `${this.config.baseUrl}${clientTokenUri}`;
893
- try {
894
- // Make request to backend endpoint
895
- const response = await fetch(fullUrl, {
896
- method: "POST",
897
- headers: {
898
- "Content-Type": "application/json",
899
- },
900
- credentials: "include", // Include cookies for CORS
901
- });
902
- if (!response.ok) {
903
- const errorText = await response.text();
904
- throw new Error(`Failed to get environment token: ${response.status} ${response.statusText}. ${errorText}`);
905
- }
906
- const data = (await response.json());
907
- // Extract token from response (support both nested and flat formats)
908
- const token = data.data?.token || data.token || data.accessToken || data.access_token;
909
- if (!token || typeof token !== "string") {
910
- throw new Error("Invalid response format: token not found in response");
911
- }
912
- // Calculate expiration time (default to 1 hour if not provided)
913
- const expiresIn = data.data?.expiresIn || data.expiresIn || data.expires_in || 3600;
914
- const expiresAt = Date.now() + expiresIn * 1000;
915
- // Cache token
916
- setLocalStorage(cacheKey, token);
917
- setLocalStorage(expiresAtKey, expiresAt.toString());
918
- // Log audit event if misoClient available
919
- if (this.misoClient && !this.shouldSkipAudit(clientTokenUri)) {
920
- try {
921
- await this.misoClient.log.audit("client.token.request.success", clientTokenUri, {
922
- method: "POST",
923
- url: clientTokenUri,
924
- statusCode: response.status,
925
- cached: false,
926
- }, {});
927
- }
928
- catch (auditError) {
929
- // Silently fail audit logging to avoid breaking requests
930
- console.warn("Failed to log audit event:", auditError);
931
- }
932
- }
933
- return token;
934
- }
935
- catch (error) {
936
- // Log audit event for error if misoClient available
937
- if (this.misoClient && !this.shouldSkipAudit(clientTokenUri)) {
938
- try {
939
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
940
- await this.misoClient.log.audit("client.token.request.failed", clientTokenUri, {
941
- method: "POST",
942
- url: clientTokenUri,
943
- statusCode: 0,
944
- error: errorMessage,
945
- cached: false,
946
- }, {});
947
- }
948
- catch (auditError) {
949
- // Silently fail audit logging to avoid breaking requests
950
- console.warn("Failed to log audit event:", auditError);
951
- }
952
- }
953
- throw error;
954
- }
358
+ return (0, data_client_auth_1.getEnvironmentToken)(this.config, this.misoClient);
955
359
  }
956
360
  /**
957
361
  * Get client token information (browser-side)
@@ -960,21 +364,7 @@ class DataClient {
960
364
  * @returns Client token info or null if token not available
961
365
  */
962
366
  getClientTokenInfo() {
963
- if (!isBrowser()) {
964
- return null;
965
- }
966
- // Try to get token from cache first
967
- const cachedToken = getLocalStorage("miso:client-token");
968
- if (cachedToken) {
969
- return (0, token_utils_1.extractClientTokenInfo)(cachedToken);
970
- }
971
- // Try to get token from config (if provided)
972
- const configToken = this.config.misoConfig?.clientToken;
973
- if (configToken) {
974
- return (0, token_utils_1.extractClientTokenInfo)(configToken);
975
- }
976
- // No token available
977
- return null;
367
+ return (0, data_client_auth_1.getClientTokenInfo)(this.config.misoConfig);
978
368
  }
979
369
  }
980
370
  exports.DataClient = DataClient;