@aifabrix/miso-client 2.1.2 → 2.2.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.
@@ -0,0 +1,743 @@
1
+ "use strict";
2
+ /**
3
+ * DataClient - Browser-compatible HTTP client wrapper around MisoClient
4
+ * Provides enhanced HTTP capabilities with ISO 27001 compliance, caching, retry, and more
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DataClient = void 0;
11
+ exports.dataClient = dataClient;
12
+ const index_1 = require("../index");
13
+ const data_client_types_1 = require("../types/data-client.types");
14
+ const data_masker_1 = require("./data-masker");
15
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
16
+ /**
17
+ * Check if running in browser environment
18
+ */
19
+ function isBrowser() {
20
+ return (typeof globalThis.window !== "undefined" &&
21
+ typeof globalThis.localStorage !== "undefined" &&
22
+ typeof globalThis.fetch !== "undefined");
23
+ }
24
+ /**
25
+ * Get value from localStorage (browser only)
26
+ */
27
+ function getLocalStorage(key) {
28
+ if (!isBrowser())
29
+ return null;
30
+ try {
31
+ return globalThis.localStorage.getItem(key);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Extract userId from JWT token
39
+ */
40
+ function extractUserIdFromToken(token) {
41
+ try {
42
+ const decoded = jsonwebtoken_1.default.decode(token);
43
+ if (!decoded)
44
+ return null;
45
+ return (decoded.sub || decoded.userId || decoded.user_id || decoded.id);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Calculate cache key from endpoint and options
53
+ */
54
+ function generateCacheKey(endpoint, options) {
55
+ const method = options?.method || "GET";
56
+ const body = options?.body ? JSON.stringify(options.body) : "";
57
+ return `data-client:${method}:${endpoint}:${body}`;
58
+ }
59
+ /**
60
+ * Truncate large payloads before masking (performance optimization)
61
+ */
62
+ function truncatePayload(data, maxSize) {
63
+ const json = JSON.stringify(data);
64
+ if (json.length <= maxSize) {
65
+ return { data, truncated: false };
66
+ }
67
+ return {
68
+ data: { _message: "Payload truncated for performance", _size: json.length },
69
+ truncated: true,
70
+ };
71
+ }
72
+ /**
73
+ * Calculate request/response sizes
74
+ */
75
+ function calculateSize(data) {
76
+ try {
77
+ return JSON.stringify(data).length;
78
+ }
79
+ catch {
80
+ return 0;
81
+ }
82
+ }
83
+ /**
84
+ * Check if error is retryable
85
+ */
86
+ function isRetryableError(statusCode, _error) {
87
+ if (!statusCode)
88
+ return true; // Network errors are retryable
89
+ if (statusCode >= 500)
90
+ return true; // Server errors
91
+ if (statusCode === 408)
92
+ return true; // Timeout
93
+ if (statusCode === 429)
94
+ return true; // Rate limit
95
+ if (statusCode === 401 || statusCode === 403)
96
+ return false; // Auth errors
97
+ if (statusCode >= 400 && statusCode < 500)
98
+ return false; // Client errors
99
+ return false;
100
+ }
101
+ /**
102
+ * Calculate exponential backoff delay
103
+ */
104
+ function calculateBackoffDelay(attempt, baseDelay, maxDelay) {
105
+ const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
106
+ return delay + Math.random() * 1000; // Add jitter
107
+ }
108
+ class DataClient {
109
+ constructor(config) {
110
+ this.misoClient = null;
111
+ this.cache = new Map();
112
+ this.pendingRequests = new Map();
113
+ this.interceptors = {};
114
+ this.metrics = {
115
+ totalRequests: 0,
116
+ totalFailures: 0,
117
+ responseTimes: [],
118
+ cacheHits: 0,
119
+ cacheMisses: 0,
120
+ };
121
+ this.config = {
122
+ tokenKeys: ["token", "accessToken", "authToken"],
123
+ loginUrl: "/login",
124
+ timeout: 30000,
125
+ cache: {
126
+ enabled: true,
127
+ defaultTTL: 300,
128
+ maxSize: 100,
129
+ },
130
+ retry: {
131
+ enabled: true,
132
+ maxRetries: 3,
133
+ baseDelay: 1000,
134
+ maxDelay: 10000,
135
+ },
136
+ audit: {
137
+ enabled: true,
138
+ level: "standard",
139
+ batchSize: 10,
140
+ maxResponseSize: 10000,
141
+ maxMaskingSize: 50000,
142
+ skipEndpoints: [],
143
+ },
144
+ ...config,
145
+ };
146
+ // Security: Warn if clientSecret is provided in browser environment
147
+ // This is a security risk as clientSecret should never be exposed in client-side code
148
+ if (isBrowser() && this.config.misoConfig?.clientSecret) {
149
+ console.warn("⚠️ SECURITY WARNING: clientSecret detected in browser environment. " +
150
+ "Client secrets should NEVER be exposed in client-side code. " +
151
+ "Use the client token pattern instead (clientToken + onClientTokenRefresh). " +
152
+ "See documentation for browser-safe configuration.");
153
+ }
154
+ // Initialize MisoClient if config provided
155
+ if (this.config.misoConfig) {
156
+ this.misoClient = new index_1.MisoClient(this.config.misoConfig);
157
+ }
158
+ // Initialize DataMasker with config path if provided
159
+ if (this.config.misoConfig?.sensitiveFieldsConfig) {
160
+ data_masker_1.DataMasker.setConfigPath(this.config.misoConfig.sensitiveFieldsConfig);
161
+ }
162
+ }
163
+ /**
164
+ * Get authentication token from localStorage
165
+ */
166
+ getToken() {
167
+ if (!isBrowser())
168
+ return null;
169
+ const keys = this.config.tokenKeys || ["token", "accessToken", "authToken"];
170
+ for (const key of keys) {
171
+ const token = getLocalStorage(key);
172
+ if (token)
173
+ return token;
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Check if user is authenticated
179
+ */
180
+ isAuthenticated() {
181
+ return this.getToken() !== null;
182
+ }
183
+ /**
184
+ * Redirect to login page via controller
185
+ * Calls the controller login endpoint with redirect parameter
186
+ * @param redirectUrl - Optional redirect URL to return to after login (defaults to current page URL)
187
+ */
188
+ async redirectToLogin(redirectUrl) {
189
+ if (!isBrowser())
190
+ return;
191
+ // If misoClient is not available, fallback to static loginUrl
192
+ if (!this.misoClient) {
193
+ const loginUrl = this.config.loginUrl || "/login";
194
+ const fullUrl = /^https?:\/\//i.test(loginUrl)
195
+ ? loginUrl
196
+ : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
197
+ globalThis.window.location.href = fullUrl;
198
+ return;
199
+ }
200
+ try {
201
+ // Get redirect URL - use provided URL or current page URL
202
+ const currentUrl = globalThis.window.location.href;
203
+ const finalRedirectUrl = redirectUrl || currentUrl;
204
+ // Call controller login endpoint with redirect parameter
205
+ const response = await this.misoClient.login({ redirect: finalRedirectUrl });
206
+ // Redirect to the login URL returned by controller
207
+ if (response.data?.loginUrl) {
208
+ globalThis.window.location.href = response.data.loginUrl;
209
+ }
210
+ else {
211
+ // Fallback if loginUrl not in response
212
+ const loginUrl = this.config.loginUrl || "/login";
213
+ const fullUrl = /^https?:\/\//i.test(loginUrl)
214
+ ? loginUrl
215
+ : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
216
+ globalThis.window.location.href = fullUrl;
217
+ }
218
+ }
219
+ catch (error) {
220
+ // On error, fallback to static loginUrl
221
+ console.error("Failed to get login URL from controller:", error);
222
+ const loginUrl = this.config.loginUrl || "/login";
223
+ const fullUrl = /^https?:\/\//i.test(loginUrl)
224
+ ? loginUrl
225
+ : `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
226
+ globalThis.window.location.href = fullUrl;
227
+ }
228
+ }
229
+ /**
230
+ * Set interceptors
231
+ */
232
+ setInterceptors(config) {
233
+ this.interceptors = { ...this.interceptors, ...config };
234
+ }
235
+ /**
236
+ * Set audit configuration
237
+ */
238
+ setAuditConfig(config) {
239
+ this.config.audit = { ...this.config.audit, ...config };
240
+ }
241
+ /**
242
+ * Set log level for MisoClient logger
243
+ * Note: This updates the MisoClient config logLevel if MisoClient is initialized
244
+ * @param level - Log level ('debug' | 'info' | 'warn' | 'error')
245
+ */
246
+ setLogLevel(level) {
247
+ if (this.misoClient && this.config.misoConfig) {
248
+ // Update the config's logLevel
249
+ // Note: TypeScript readonly doesn't prevent runtime updates
250
+ this.config.misoConfig.logLevel = level;
251
+ // Also try to update MisoClient's internal config if accessible
252
+ try {
253
+ const misoConfig = this.misoClient.config;
254
+ if (misoConfig) {
255
+ misoConfig.logLevel = level;
256
+ }
257
+ }
258
+ catch {
259
+ // Silently ignore if config is not accessible
260
+ }
261
+ }
262
+ }
263
+ /**
264
+ * Clear all cached responses
265
+ */
266
+ clearCache() {
267
+ this.cache.clear();
268
+ }
269
+ /**
270
+ * Get request metrics
271
+ */
272
+ getMetrics() {
273
+ const responseTimes = this.metrics.responseTimes.sort((a, b) => a - b);
274
+ const len = responseTimes.length;
275
+ return {
276
+ totalRequests: this.metrics.totalRequests,
277
+ totalFailures: this.metrics.totalFailures,
278
+ averageResponseTime: len > 0
279
+ ? responseTimes.reduce((a, b) => a + b, 0) / len
280
+ : 0,
281
+ responseTimeDistribution: {
282
+ min: len > 0 ? responseTimes[0] : 0,
283
+ max: len > 0 ? responseTimes[len - 1] : 0,
284
+ p50: len > 0 ? responseTimes[Math.floor(len * 0.5)] : 0,
285
+ p95: len > 0 ? responseTimes[Math.floor(len * 0.95)] : 0,
286
+ p99: len > 0 ? responseTimes[Math.floor(len * 0.99)] : 0,
287
+ },
288
+ errorRate: this.metrics.totalRequests > 0
289
+ ? this.metrics.totalFailures / this.metrics.totalRequests
290
+ : 0,
291
+ cacheHitRate: this.metrics.cacheHits + this.metrics.cacheMisses > 0
292
+ ? this.metrics.cacheHits /
293
+ (this.metrics.cacheHits + this.metrics.cacheMisses)
294
+ : 0,
295
+ };
296
+ }
297
+ /**
298
+ * Check if endpoint should skip audit logging
299
+ */
300
+ shouldSkipAudit(endpoint) {
301
+ if (!this.config.audit?.enabled)
302
+ return true;
303
+ const skipEndpoints = this.config.audit.skipEndpoints || [];
304
+ return skipEndpoints.some((skip) => endpoint.includes(skip));
305
+ }
306
+ /**
307
+ * Log audit event (ISO 27001 compliance)
308
+ */
309
+ async logAuditEvent(method, url, statusCode, duration, requestSize, responseSize, error, requestHeaders, responseHeaders, requestBody, responseBody) {
310
+ if (this.shouldSkipAudit(url) || !this.misoClient)
311
+ return;
312
+ try {
313
+ const token = this.getToken();
314
+ const userId = token ? extractUserIdFromToken(token) : undefined;
315
+ const auditLevel = this.config.audit?.level || "standard";
316
+ // Build audit context based on level
317
+ const auditContext = {
318
+ method,
319
+ url,
320
+ statusCode,
321
+ duration,
322
+ };
323
+ if (userId) {
324
+ auditContext.userId = userId;
325
+ }
326
+ // Minimal level: only basic info
327
+ if (auditLevel === "minimal") {
328
+ await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
329
+ return;
330
+ }
331
+ // Standard/Detailed/Full levels: include headers and bodies (masked)
332
+ const maxResponseSize = this.config.audit?.maxResponseSize || 10000;
333
+ const maxMaskingSize = this.config.audit?.maxMaskingSize || 50000;
334
+ // Truncate and mask request body
335
+ let maskedRequestBody = undefined;
336
+ if (requestBody !== undefined) {
337
+ const truncated = truncatePayload(requestBody, maxMaskingSize);
338
+ if (!truncated.truncated) {
339
+ maskedRequestBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
340
+ }
341
+ else {
342
+ maskedRequestBody = truncated.data;
343
+ }
344
+ }
345
+ // Truncate and mask response body (for standard, detailed, full levels)
346
+ let maskedResponseBody = undefined;
347
+ if (responseBody !== undefined) {
348
+ const truncated = truncatePayload(responseBody, maxResponseSize);
349
+ if (!truncated.truncated) {
350
+ maskedResponseBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
351
+ }
352
+ else {
353
+ maskedResponseBody = truncated.data;
354
+ }
355
+ }
356
+ // Mask headers
357
+ const maskedRequestHeaders = requestHeaders
358
+ ? data_masker_1.DataMasker.maskSensitiveData(requestHeaders)
359
+ : undefined;
360
+ const maskedResponseHeaders = responseHeaders
361
+ ? data_masker_1.DataMasker.maskSensitiveData(responseHeaders)
362
+ : undefined;
363
+ // Add to context based on level (standard, detailed, full all include headers/bodies)
364
+ if (maskedRequestHeaders)
365
+ auditContext.requestHeaders = maskedRequestHeaders;
366
+ if (maskedResponseHeaders)
367
+ auditContext.responseHeaders = maskedResponseHeaders;
368
+ if (maskedRequestBody !== undefined)
369
+ auditContext.requestBody = maskedRequestBody;
370
+ if (maskedResponseBody !== undefined)
371
+ auditContext.responseBody = maskedResponseBody;
372
+ // Add sizes for detailed/full levels
373
+ if (auditLevel === "detailed" || auditLevel === "full") {
374
+ if (requestSize !== undefined)
375
+ auditContext.requestSize = requestSize;
376
+ if (responseSize !== undefined)
377
+ auditContext.responseSize = responseSize;
378
+ }
379
+ if (error) {
380
+ const maskedError = data_masker_1.DataMasker.maskSensitiveData({
381
+ message: error.message,
382
+ name: error.name,
383
+ stack: error.stack,
384
+ });
385
+ auditContext.error = maskedError;
386
+ }
387
+ await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
388
+ }
389
+ catch (auditError) {
390
+ // Silently fail audit logging to avoid breaking requests
391
+ console.warn("Failed to log audit event:", auditError);
392
+ }
393
+ }
394
+ /**
395
+ * Make HTTP request with all features (caching, retry, deduplication, audit)
396
+ */
397
+ async request(method, endpoint, options) {
398
+ const startTime = Date.now();
399
+ const fullUrl = `${this.config.baseUrl}${endpoint}`;
400
+ const cacheKey = generateCacheKey(endpoint, options);
401
+ const isGetRequest = method.toUpperCase() === "GET";
402
+ const cacheEnabled = (this.config.cache?.enabled !== false) &&
403
+ isGetRequest &&
404
+ (options?.cache?.enabled !== false);
405
+ // Check cache for GET requests
406
+ if (cacheEnabled) {
407
+ const cached = this.cache.get(cacheKey);
408
+ if (cached && cached.expiresAt > Date.now()) {
409
+ this.metrics.cacheHits++;
410
+ return cached.data;
411
+ }
412
+ this.metrics.cacheMisses++;
413
+ }
414
+ // Check for duplicate concurrent requests (only for GET requests)
415
+ if (isGetRequest) {
416
+ const pendingRequest = this.pendingRequests.get(cacheKey);
417
+ if (pendingRequest) {
418
+ return pendingRequest;
419
+ }
420
+ }
421
+ // Create request promise
422
+ const requestPromise = this.executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime);
423
+ // Store pending request (only for GET requests)
424
+ if (isGetRequest) {
425
+ this.pendingRequests.set(cacheKey, requestPromise);
426
+ }
427
+ try {
428
+ const result = await requestPromise;
429
+ return result;
430
+ }
431
+ finally {
432
+ // Cleanup pending request
433
+ if (isGetRequest) {
434
+ this.pendingRequests.delete(cacheKey);
435
+ }
436
+ }
437
+ }
438
+ /**
439
+ * Execute HTTP request with retry logic
440
+ */
441
+ async executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime) {
442
+ const maxRetries = options?.retries !== undefined
443
+ ? options.retries
444
+ : this.config.retry?.maxRetries || 3;
445
+ const retryEnabled = this.config.retry?.enabled !== false;
446
+ let lastError;
447
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
448
+ try {
449
+ const response = await this.makeFetchRequest(method, fullUrl, options);
450
+ const duration = Date.now() - startTime;
451
+ // Update metrics
452
+ this.metrics.totalRequests++;
453
+ this.metrics.responseTimes.push(duration);
454
+ // Parse response
455
+ const data = await this.parseResponse(response);
456
+ // Cache successful GET responses
457
+ if (cacheEnabled && response.ok) {
458
+ const ttl = options?.cache?.ttl || this.config.cache?.defaultTTL || 300;
459
+ this.cache.set(cacheKey, {
460
+ data,
461
+ expiresAt: Date.now() + ttl * 1000,
462
+ key: cacheKey,
463
+ });
464
+ // Enforce max cache size
465
+ if (this.cache.size > (this.config.cache?.maxSize || 100)) {
466
+ const firstKey = this.cache.keys().next().value;
467
+ if (firstKey)
468
+ this.cache.delete(firstKey);
469
+ }
470
+ }
471
+ // Apply response interceptor
472
+ if (this.interceptors.onResponse) {
473
+ return await this.interceptors.onResponse(response, data);
474
+ }
475
+ // Audit logging
476
+ if (!options?.skipAudit) {
477
+ const requestSize = options?.body
478
+ ? calculateSize(options.body)
479
+ : undefined;
480
+ const responseSize = calculateSize(data);
481
+ await this.logAuditEvent(method, endpoint, response.status, duration, requestSize, responseSize, undefined, this.extractHeaders(options?.headers), this.extractHeaders(response.headers), options?.body, data);
482
+ }
483
+ // Handle error responses
484
+ if (!response.ok) {
485
+ if (response.status === 401) {
486
+ this.handleAuthError();
487
+ throw new data_client_types_1.AuthenticationError("Authentication required", response);
488
+ }
489
+ throw new data_client_types_1.ApiError(`Request failed with status ${response.status}`, response.status, response);
490
+ }
491
+ return data;
492
+ }
493
+ catch (error) {
494
+ lastError = error;
495
+ const duration = Date.now() - startTime;
496
+ // Check if retryable
497
+ const isRetryable = retryEnabled &&
498
+ attempt < maxRetries &&
499
+ isRetryableError(error.statusCode, error);
500
+ if (!isRetryable) {
501
+ this.metrics.totalFailures++;
502
+ // Audit log error
503
+ if (!options?.skipAudit) {
504
+ 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);
505
+ }
506
+ // Apply error interceptor
507
+ if (this.interceptors.onError) {
508
+ throw await this.interceptors.onError(error);
509
+ }
510
+ throw error;
511
+ }
512
+ // Calculate backoff delay
513
+ const baseDelay = this.config.retry?.baseDelay || 1000;
514
+ const maxDelay = this.config.retry?.maxDelay || 10000;
515
+ const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);
516
+ // Wait before retry
517
+ await new Promise((resolve) => setTimeout(resolve, delay));
518
+ }
519
+ }
520
+ // All retries exhausted
521
+ this.metrics.totalFailures++;
522
+ if (lastError) {
523
+ if (this.interceptors.onError) {
524
+ throw await this.interceptors.onError(lastError);
525
+ }
526
+ throw lastError;
527
+ }
528
+ throw new Error("Request failed after retries");
529
+ }
530
+ /**
531
+ * Make fetch request with timeout and authentication
532
+ */
533
+ async makeFetchRequest(method, url, options) {
534
+ // Build headers
535
+ const headers = new Headers(this.config.defaultHeaders);
536
+ if (options?.headers) {
537
+ if (options.headers instanceof Headers) {
538
+ options.headers.forEach((value, key) => headers.set(key, value));
539
+ }
540
+ else {
541
+ Object.entries(options.headers).forEach(([key, value]) => {
542
+ headers.set(key, String(value));
543
+ });
544
+ }
545
+ }
546
+ // Add authentication
547
+ if (!options?.skipAuth) {
548
+ const token = this.getToken();
549
+ if (token) {
550
+ headers.set("Authorization", `Bearer ${token}`);
551
+ }
552
+ // Note: MisoClient client-token is handled server-side, not in browser
553
+ }
554
+ // Create abort controller for timeout
555
+ const timeout = options?.timeout || this.config.timeout || 30000;
556
+ const controller = new AbortController();
557
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
558
+ // Merge signals
559
+ const signal = options?.signal
560
+ ? this.mergeSignals(controller.signal, options.signal)
561
+ : controller.signal;
562
+ try {
563
+ const response = await fetch(url, {
564
+ method,
565
+ headers,
566
+ body: options?.body,
567
+ signal,
568
+ ...options,
569
+ });
570
+ clearTimeout(timeoutId);
571
+ return response;
572
+ }
573
+ catch (error) {
574
+ clearTimeout(timeoutId);
575
+ if (error instanceof Error && error.name === "AbortError") {
576
+ throw new data_client_types_1.TimeoutError(`Request timeout after ${timeout}ms`, timeout);
577
+ }
578
+ throw new data_client_types_1.NetworkError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
579
+ }
580
+ }
581
+ /**
582
+ * Merge AbortSignals
583
+ */
584
+ mergeSignals(signal1, signal2) {
585
+ const controller = new AbortController();
586
+ const abort = () => {
587
+ controller.abort();
588
+ signal1.removeEventListener("abort", abort);
589
+ signal2.removeEventListener("abort", abort);
590
+ };
591
+ if (signal1.aborted || signal2.aborted) {
592
+ controller.abort();
593
+ return controller.signal;
594
+ }
595
+ signal1.addEventListener("abort", abort);
596
+ signal2.addEventListener("abort", abort);
597
+ return controller.signal;
598
+ }
599
+ /**
600
+ * Parse response based on content type
601
+ */
602
+ async parseResponse(response) {
603
+ const contentType = response.headers.get("content-type") || "";
604
+ if (contentType.includes("application/json")) {
605
+ return (await response.json());
606
+ }
607
+ if (contentType.includes("text/")) {
608
+ return (await response.text());
609
+ }
610
+ return (await response.blob());
611
+ }
612
+ /**
613
+ * Extract headers from Headers object or Record
614
+ */
615
+ extractHeaders(headers) {
616
+ if (!headers)
617
+ return undefined;
618
+ if (headers instanceof Headers) {
619
+ const result = {};
620
+ headers.forEach((value, key) => {
621
+ result[key] = value;
622
+ });
623
+ return result;
624
+ }
625
+ if (Array.isArray(headers)) {
626
+ const result = {};
627
+ headers.forEach(([key, value]) => {
628
+ result[key] = String(value);
629
+ });
630
+ return result;
631
+ }
632
+ // Convert Record<string, string | readonly string[]> to Record<string, string>
633
+ const result = {};
634
+ Object.entries(headers).forEach(([key, value]) => {
635
+ result[key] = Array.isArray(value) ? value.join(", ") : String(value);
636
+ });
637
+ return result;
638
+ }
639
+ /**
640
+ * Handle authentication error
641
+ */
642
+ handleAuthError() {
643
+ if (isBrowser()) {
644
+ // Fire and forget - redirect doesn't need to complete before throwing error
645
+ this.redirectToLogin().catch((error) => {
646
+ console.error("Failed to redirect to login:", error);
647
+ });
648
+ }
649
+ }
650
+ /**
651
+ * Apply request interceptor
652
+ */
653
+ async applyRequestInterceptor(url, options) {
654
+ if (this.interceptors.onRequest) {
655
+ return await this.interceptors.onRequest(url, options);
656
+ }
657
+ return options;
658
+ }
659
+ // ==================== HTTP METHODS ====================
660
+ /**
661
+ * GET request
662
+ */
663
+ async get(endpoint, options) {
664
+ const finalOptions = await this.applyRequestInterceptor(endpoint, {
665
+ ...options,
666
+ method: "GET",
667
+ });
668
+ return this.request("GET", endpoint, finalOptions);
669
+ }
670
+ /**
671
+ * POST request
672
+ */
673
+ async post(endpoint, data, options) {
674
+ const finalOptions = await this.applyRequestInterceptor(endpoint, {
675
+ ...options,
676
+ method: "POST",
677
+ body: data ? JSON.stringify(data) : undefined,
678
+ headers: {
679
+ "Content-Type": "application/json",
680
+ ...options?.headers,
681
+ },
682
+ });
683
+ return this.request("POST", endpoint, finalOptions);
684
+ }
685
+ /**
686
+ * PUT request
687
+ */
688
+ async put(endpoint, data, options) {
689
+ const finalOptions = await this.applyRequestInterceptor(endpoint, {
690
+ ...options,
691
+ method: "PUT",
692
+ body: data ? JSON.stringify(data) : undefined,
693
+ headers: {
694
+ "Content-Type": "application/json",
695
+ ...options?.headers,
696
+ },
697
+ });
698
+ return this.request("PUT", endpoint, finalOptions);
699
+ }
700
+ /**
701
+ * PATCH request (uses fetch fallback since MisoClient doesn't support PATCH)
702
+ */
703
+ async patch(endpoint, data, options) {
704
+ const finalOptions = await this.applyRequestInterceptor(endpoint, {
705
+ ...options,
706
+ method: "PATCH",
707
+ body: data ? JSON.stringify(data) : undefined,
708
+ headers: {
709
+ "Content-Type": "application/json",
710
+ ...options?.headers,
711
+ },
712
+ });
713
+ return this.request("PATCH", endpoint, finalOptions);
714
+ }
715
+ /**
716
+ * DELETE request
717
+ */
718
+ async delete(endpoint, options) {
719
+ const finalOptions = await this.applyRequestInterceptor(endpoint, {
720
+ ...options,
721
+ method: "DELETE",
722
+ });
723
+ return this.request("DELETE", endpoint, finalOptions);
724
+ }
725
+ }
726
+ exports.DataClient = DataClient;
727
+ /**
728
+ * Singleton instance factory
729
+ */
730
+ let defaultDataClient = null;
731
+ /**
732
+ * Get or create default DataClient instance
733
+ */
734
+ function dataClient(config) {
735
+ if (!defaultDataClient && config) {
736
+ defaultDataClient = new DataClient(config);
737
+ }
738
+ if (!defaultDataClient) {
739
+ throw new Error("DataClient not initialized. Call dataClient(config) first.");
740
+ }
741
+ return defaultDataClient;
742
+ }
743
+ //# sourceMappingURL=data-client.js.map