@aifabrix/miso-client 2.1.2 → 2.2.0
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 +45 -0
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- package/dist/index.js.map +1 -1
- package/dist/services/auth.service.d.ts +22 -5
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +89 -11
- package/dist/services/auth.service.js.map +1 -1
- package/dist/types/config.types.d.ts +20 -1
- package/dist/types/config.types.d.ts.map +1 -1
- package/dist/types/config.types.js.map +1 -1
- package/dist/types/data-client.types.d.ts +267 -0
- package/dist/types/data-client.types.d.ts.map +1 -0
- package/dist/types/data-client.types.js +53 -0
- package/dist/types/data-client.types.js.map +1 -0
- package/dist/utils/data-client.d.ts +113 -0
- package/dist/utils/data-client.d.ts.map +1 -0
- package/dist/utils/data-client.js +703 -0
- package/dist/utils/data-client.js.map +1 -0
- package/dist/utils/internal-http-client.d.ts +6 -1
- package/dist/utils/internal-http-client.d.ts.map +1 -1
- package/dist/utils/internal-http-client.js +61 -7
- package/dist/utils/internal-http-client.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,703 @@
|
|
|
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
|
|
185
|
+
*/
|
|
186
|
+
redirectToLogin() {
|
|
187
|
+
if (!isBrowser())
|
|
188
|
+
return;
|
|
189
|
+
const loginUrl = this.config.loginUrl || "/login";
|
|
190
|
+
globalThis.window.location.href = loginUrl;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Set interceptors
|
|
194
|
+
*/
|
|
195
|
+
setInterceptors(config) {
|
|
196
|
+
this.interceptors = { ...this.interceptors, ...config };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Set audit configuration
|
|
200
|
+
*/
|
|
201
|
+
setAuditConfig(config) {
|
|
202
|
+
this.config.audit = { ...this.config.audit, ...config };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Set log level for MisoClient logger
|
|
206
|
+
* Note: This updates the MisoClient config logLevel if MisoClient is initialized
|
|
207
|
+
* @param level - Log level ('debug' | 'info' | 'warn' | 'error')
|
|
208
|
+
*/
|
|
209
|
+
setLogLevel(level) {
|
|
210
|
+
if (this.misoClient && this.config.misoConfig) {
|
|
211
|
+
// Update the config's logLevel
|
|
212
|
+
// Note: TypeScript readonly doesn't prevent runtime updates
|
|
213
|
+
this.config.misoConfig.logLevel = level;
|
|
214
|
+
// Also try to update MisoClient's internal config if accessible
|
|
215
|
+
try {
|
|
216
|
+
const misoConfig = this.misoClient.config;
|
|
217
|
+
if (misoConfig) {
|
|
218
|
+
misoConfig.logLevel = level;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Silently ignore if config is not accessible
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Clear all cached responses
|
|
228
|
+
*/
|
|
229
|
+
clearCache() {
|
|
230
|
+
this.cache.clear();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get request metrics
|
|
234
|
+
*/
|
|
235
|
+
getMetrics() {
|
|
236
|
+
const responseTimes = this.metrics.responseTimes.sort((a, b) => a - b);
|
|
237
|
+
const len = responseTimes.length;
|
|
238
|
+
return {
|
|
239
|
+
totalRequests: this.metrics.totalRequests,
|
|
240
|
+
totalFailures: this.metrics.totalFailures,
|
|
241
|
+
averageResponseTime: len > 0
|
|
242
|
+
? responseTimes.reduce((a, b) => a + b, 0) / len
|
|
243
|
+
: 0,
|
|
244
|
+
responseTimeDistribution: {
|
|
245
|
+
min: len > 0 ? responseTimes[0] : 0,
|
|
246
|
+
max: len > 0 ? responseTimes[len - 1] : 0,
|
|
247
|
+
p50: len > 0 ? responseTimes[Math.floor(len * 0.5)] : 0,
|
|
248
|
+
p95: len > 0 ? responseTimes[Math.floor(len * 0.95)] : 0,
|
|
249
|
+
p99: len > 0 ? responseTimes[Math.floor(len * 0.99)] : 0,
|
|
250
|
+
},
|
|
251
|
+
errorRate: this.metrics.totalRequests > 0
|
|
252
|
+
? this.metrics.totalFailures / this.metrics.totalRequests
|
|
253
|
+
: 0,
|
|
254
|
+
cacheHitRate: this.metrics.cacheHits + this.metrics.cacheMisses > 0
|
|
255
|
+
? this.metrics.cacheHits /
|
|
256
|
+
(this.metrics.cacheHits + this.metrics.cacheMisses)
|
|
257
|
+
: 0,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if endpoint should skip audit logging
|
|
262
|
+
*/
|
|
263
|
+
shouldSkipAudit(endpoint) {
|
|
264
|
+
if (!this.config.audit?.enabled)
|
|
265
|
+
return true;
|
|
266
|
+
const skipEndpoints = this.config.audit.skipEndpoints || [];
|
|
267
|
+
return skipEndpoints.some((skip) => endpoint.includes(skip));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Log audit event (ISO 27001 compliance)
|
|
271
|
+
*/
|
|
272
|
+
async logAuditEvent(method, url, statusCode, duration, requestSize, responseSize, error, requestHeaders, responseHeaders, requestBody, responseBody) {
|
|
273
|
+
if (this.shouldSkipAudit(url) || !this.misoClient)
|
|
274
|
+
return;
|
|
275
|
+
try {
|
|
276
|
+
const token = this.getToken();
|
|
277
|
+
const userId = token ? extractUserIdFromToken(token) : undefined;
|
|
278
|
+
const auditLevel = this.config.audit?.level || "standard";
|
|
279
|
+
// Build audit context based on level
|
|
280
|
+
const auditContext = {
|
|
281
|
+
method,
|
|
282
|
+
url,
|
|
283
|
+
statusCode,
|
|
284
|
+
duration,
|
|
285
|
+
};
|
|
286
|
+
if (userId) {
|
|
287
|
+
auditContext.userId = userId;
|
|
288
|
+
}
|
|
289
|
+
// Minimal level: only basic info
|
|
290
|
+
if (auditLevel === "minimal") {
|
|
291
|
+
await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// Standard/Detailed/Full levels: include headers and bodies (masked)
|
|
295
|
+
const maxResponseSize = this.config.audit?.maxResponseSize || 10000;
|
|
296
|
+
const maxMaskingSize = this.config.audit?.maxMaskingSize || 50000;
|
|
297
|
+
// Truncate and mask request body
|
|
298
|
+
let maskedRequestBody = undefined;
|
|
299
|
+
if (requestBody !== undefined) {
|
|
300
|
+
const truncated = truncatePayload(requestBody, maxMaskingSize);
|
|
301
|
+
if (!truncated.truncated) {
|
|
302
|
+
maskedRequestBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
maskedRequestBody = truncated.data;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Truncate and mask response body (for standard, detailed, full levels)
|
|
309
|
+
let maskedResponseBody = undefined;
|
|
310
|
+
if (responseBody !== undefined) {
|
|
311
|
+
const truncated = truncatePayload(responseBody, maxResponseSize);
|
|
312
|
+
if (!truncated.truncated) {
|
|
313
|
+
maskedResponseBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
maskedResponseBody = truncated.data;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Mask headers
|
|
320
|
+
const maskedRequestHeaders = requestHeaders
|
|
321
|
+
? data_masker_1.DataMasker.maskSensitiveData(requestHeaders)
|
|
322
|
+
: undefined;
|
|
323
|
+
const maskedResponseHeaders = responseHeaders
|
|
324
|
+
? data_masker_1.DataMasker.maskSensitiveData(responseHeaders)
|
|
325
|
+
: undefined;
|
|
326
|
+
// Add to context based on level (standard, detailed, full all include headers/bodies)
|
|
327
|
+
if (maskedRequestHeaders)
|
|
328
|
+
auditContext.requestHeaders = maskedRequestHeaders;
|
|
329
|
+
if (maskedResponseHeaders)
|
|
330
|
+
auditContext.responseHeaders = maskedResponseHeaders;
|
|
331
|
+
if (maskedRequestBody !== undefined)
|
|
332
|
+
auditContext.requestBody = maskedRequestBody;
|
|
333
|
+
if (maskedResponseBody !== undefined)
|
|
334
|
+
auditContext.responseBody = maskedResponseBody;
|
|
335
|
+
// Add sizes for detailed/full levels
|
|
336
|
+
if (auditLevel === "detailed" || auditLevel === "full") {
|
|
337
|
+
if (requestSize !== undefined)
|
|
338
|
+
auditContext.requestSize = requestSize;
|
|
339
|
+
if (responseSize !== undefined)
|
|
340
|
+
auditContext.responseSize = responseSize;
|
|
341
|
+
}
|
|
342
|
+
if (error) {
|
|
343
|
+
const maskedError = data_masker_1.DataMasker.maskSensitiveData({
|
|
344
|
+
message: error.message,
|
|
345
|
+
name: error.name,
|
|
346
|
+
stack: error.stack,
|
|
347
|
+
});
|
|
348
|
+
auditContext.error = maskedError;
|
|
349
|
+
}
|
|
350
|
+
await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
|
|
351
|
+
}
|
|
352
|
+
catch (auditError) {
|
|
353
|
+
// Silently fail audit logging to avoid breaking requests
|
|
354
|
+
console.warn("Failed to log audit event:", auditError);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Make HTTP request with all features (caching, retry, deduplication, audit)
|
|
359
|
+
*/
|
|
360
|
+
async request(method, endpoint, options) {
|
|
361
|
+
const startTime = Date.now();
|
|
362
|
+
const fullUrl = `${this.config.baseUrl}${endpoint}`;
|
|
363
|
+
const cacheKey = generateCacheKey(endpoint, options);
|
|
364
|
+
const isGetRequest = method.toUpperCase() === "GET";
|
|
365
|
+
const cacheEnabled = (this.config.cache?.enabled !== false) &&
|
|
366
|
+
isGetRequest &&
|
|
367
|
+
(options?.cache?.enabled !== false);
|
|
368
|
+
// Check cache for GET requests
|
|
369
|
+
if (cacheEnabled) {
|
|
370
|
+
const cached = this.cache.get(cacheKey);
|
|
371
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
372
|
+
this.metrics.cacheHits++;
|
|
373
|
+
return cached.data;
|
|
374
|
+
}
|
|
375
|
+
this.metrics.cacheMisses++;
|
|
376
|
+
}
|
|
377
|
+
// Check for duplicate concurrent requests (only for GET requests)
|
|
378
|
+
if (isGetRequest) {
|
|
379
|
+
const pendingRequest = this.pendingRequests.get(cacheKey);
|
|
380
|
+
if (pendingRequest) {
|
|
381
|
+
return pendingRequest;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Create request promise
|
|
385
|
+
const requestPromise = this.executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime);
|
|
386
|
+
// Store pending request (only for GET requests)
|
|
387
|
+
if (isGetRequest) {
|
|
388
|
+
this.pendingRequests.set(cacheKey, requestPromise);
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const result = await requestPromise;
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
// Cleanup pending request
|
|
396
|
+
if (isGetRequest) {
|
|
397
|
+
this.pendingRequests.delete(cacheKey);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Execute HTTP request with retry logic
|
|
403
|
+
*/
|
|
404
|
+
async executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime) {
|
|
405
|
+
const maxRetries = options?.retries !== undefined
|
|
406
|
+
? options.retries
|
|
407
|
+
: this.config.retry?.maxRetries || 3;
|
|
408
|
+
const retryEnabled = this.config.retry?.enabled !== false;
|
|
409
|
+
let lastError;
|
|
410
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
411
|
+
try {
|
|
412
|
+
const response = await this.makeFetchRequest(method, fullUrl, options);
|
|
413
|
+
const duration = Date.now() - startTime;
|
|
414
|
+
// Update metrics
|
|
415
|
+
this.metrics.totalRequests++;
|
|
416
|
+
this.metrics.responseTimes.push(duration);
|
|
417
|
+
// Parse response
|
|
418
|
+
const data = await this.parseResponse(response);
|
|
419
|
+
// Cache successful GET responses
|
|
420
|
+
if (cacheEnabled && response.ok) {
|
|
421
|
+
const ttl = options?.cache?.ttl || this.config.cache?.defaultTTL || 300;
|
|
422
|
+
this.cache.set(cacheKey, {
|
|
423
|
+
data,
|
|
424
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
425
|
+
key: cacheKey,
|
|
426
|
+
});
|
|
427
|
+
// Enforce max cache size
|
|
428
|
+
if (this.cache.size > (this.config.cache?.maxSize || 100)) {
|
|
429
|
+
const firstKey = this.cache.keys().next().value;
|
|
430
|
+
if (firstKey)
|
|
431
|
+
this.cache.delete(firstKey);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Apply response interceptor
|
|
435
|
+
if (this.interceptors.onResponse) {
|
|
436
|
+
return await this.interceptors.onResponse(response, data);
|
|
437
|
+
}
|
|
438
|
+
// Audit logging
|
|
439
|
+
if (!options?.skipAudit) {
|
|
440
|
+
const requestSize = options?.body
|
|
441
|
+
? calculateSize(options.body)
|
|
442
|
+
: undefined;
|
|
443
|
+
const responseSize = calculateSize(data);
|
|
444
|
+
await this.logAuditEvent(method, endpoint, response.status, duration, requestSize, responseSize, undefined, this.extractHeaders(options?.headers), this.extractHeaders(response.headers), options?.body, data);
|
|
445
|
+
}
|
|
446
|
+
// Handle error responses
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
if (response.status === 401) {
|
|
449
|
+
this.handleAuthError();
|
|
450
|
+
throw new data_client_types_1.AuthenticationError("Authentication required", response);
|
|
451
|
+
}
|
|
452
|
+
throw new data_client_types_1.ApiError(`Request failed with status ${response.status}`, response.status, response);
|
|
453
|
+
}
|
|
454
|
+
return data;
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
lastError = error;
|
|
458
|
+
const duration = Date.now() - startTime;
|
|
459
|
+
// Check if retryable
|
|
460
|
+
const isRetryable = retryEnabled &&
|
|
461
|
+
attempt < maxRetries &&
|
|
462
|
+
isRetryableError(error.statusCode, error);
|
|
463
|
+
if (!isRetryable) {
|
|
464
|
+
this.metrics.totalFailures++;
|
|
465
|
+
// Audit log error
|
|
466
|
+
if (!options?.skipAudit) {
|
|
467
|
+
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);
|
|
468
|
+
}
|
|
469
|
+
// Apply error interceptor
|
|
470
|
+
if (this.interceptors.onError) {
|
|
471
|
+
throw await this.interceptors.onError(error);
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
// Calculate backoff delay
|
|
476
|
+
const baseDelay = this.config.retry?.baseDelay || 1000;
|
|
477
|
+
const maxDelay = this.config.retry?.maxDelay || 10000;
|
|
478
|
+
const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);
|
|
479
|
+
// Wait before retry
|
|
480
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// All retries exhausted
|
|
484
|
+
this.metrics.totalFailures++;
|
|
485
|
+
if (lastError) {
|
|
486
|
+
if (this.interceptors.onError) {
|
|
487
|
+
throw await this.interceptors.onError(lastError);
|
|
488
|
+
}
|
|
489
|
+
throw lastError;
|
|
490
|
+
}
|
|
491
|
+
throw new Error("Request failed after retries");
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Make fetch request with timeout and authentication
|
|
495
|
+
*/
|
|
496
|
+
async makeFetchRequest(method, url, options) {
|
|
497
|
+
// Build headers
|
|
498
|
+
const headers = new Headers(this.config.defaultHeaders);
|
|
499
|
+
if (options?.headers) {
|
|
500
|
+
if (options.headers instanceof Headers) {
|
|
501
|
+
options.headers.forEach((value, key) => headers.set(key, value));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
Object.entries(options.headers).forEach(([key, value]) => {
|
|
505
|
+
headers.set(key, String(value));
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Add authentication
|
|
510
|
+
if (!options?.skipAuth) {
|
|
511
|
+
const token = this.getToken();
|
|
512
|
+
if (token) {
|
|
513
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
514
|
+
}
|
|
515
|
+
// Note: MisoClient client-token is handled server-side, not in browser
|
|
516
|
+
}
|
|
517
|
+
// Create abort controller for timeout
|
|
518
|
+
const timeout = options?.timeout || this.config.timeout || 30000;
|
|
519
|
+
const controller = new AbortController();
|
|
520
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
521
|
+
// Merge signals
|
|
522
|
+
const signal = options?.signal
|
|
523
|
+
? this.mergeSignals(controller.signal, options.signal)
|
|
524
|
+
: controller.signal;
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(url, {
|
|
527
|
+
method,
|
|
528
|
+
headers,
|
|
529
|
+
body: options?.body,
|
|
530
|
+
signal,
|
|
531
|
+
...options,
|
|
532
|
+
});
|
|
533
|
+
clearTimeout(timeoutId);
|
|
534
|
+
return response;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
clearTimeout(timeoutId);
|
|
538
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
539
|
+
throw new data_client_types_1.TimeoutError(`Request timeout after ${timeout}ms`, timeout);
|
|
540
|
+
}
|
|
541
|
+
throw new data_client_types_1.NetworkError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Merge AbortSignals
|
|
546
|
+
*/
|
|
547
|
+
mergeSignals(signal1, signal2) {
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
const abort = () => {
|
|
550
|
+
controller.abort();
|
|
551
|
+
signal1.removeEventListener("abort", abort);
|
|
552
|
+
signal2.removeEventListener("abort", abort);
|
|
553
|
+
};
|
|
554
|
+
if (signal1.aborted || signal2.aborted) {
|
|
555
|
+
controller.abort();
|
|
556
|
+
return controller.signal;
|
|
557
|
+
}
|
|
558
|
+
signal1.addEventListener("abort", abort);
|
|
559
|
+
signal2.addEventListener("abort", abort);
|
|
560
|
+
return controller.signal;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Parse response based on content type
|
|
564
|
+
*/
|
|
565
|
+
async parseResponse(response) {
|
|
566
|
+
const contentType = response.headers.get("content-type") || "";
|
|
567
|
+
if (contentType.includes("application/json")) {
|
|
568
|
+
return (await response.json());
|
|
569
|
+
}
|
|
570
|
+
if (contentType.includes("text/")) {
|
|
571
|
+
return (await response.text());
|
|
572
|
+
}
|
|
573
|
+
return (await response.blob());
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Extract headers from Headers object or Record
|
|
577
|
+
*/
|
|
578
|
+
extractHeaders(headers) {
|
|
579
|
+
if (!headers)
|
|
580
|
+
return undefined;
|
|
581
|
+
if (headers instanceof Headers) {
|
|
582
|
+
const result = {};
|
|
583
|
+
headers.forEach((value, key) => {
|
|
584
|
+
result[key] = value;
|
|
585
|
+
});
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(headers)) {
|
|
589
|
+
const result = {};
|
|
590
|
+
headers.forEach(([key, value]) => {
|
|
591
|
+
result[key] = String(value);
|
|
592
|
+
});
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
// Convert Record<string, string | readonly string[]> to Record<string, string>
|
|
596
|
+
const result = {};
|
|
597
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
598
|
+
result[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
599
|
+
});
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Handle authentication error
|
|
604
|
+
*/
|
|
605
|
+
handleAuthError() {
|
|
606
|
+
if (isBrowser()) {
|
|
607
|
+
this.redirectToLogin();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Apply request interceptor
|
|
612
|
+
*/
|
|
613
|
+
async applyRequestInterceptor(url, options) {
|
|
614
|
+
if (this.interceptors.onRequest) {
|
|
615
|
+
return await this.interceptors.onRequest(url, options);
|
|
616
|
+
}
|
|
617
|
+
return options;
|
|
618
|
+
}
|
|
619
|
+
// ==================== HTTP METHODS ====================
|
|
620
|
+
/**
|
|
621
|
+
* GET request
|
|
622
|
+
*/
|
|
623
|
+
async get(endpoint, options) {
|
|
624
|
+
const finalOptions = await this.applyRequestInterceptor(endpoint, {
|
|
625
|
+
...options,
|
|
626
|
+
method: "GET",
|
|
627
|
+
});
|
|
628
|
+
return this.request("GET", endpoint, finalOptions);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* POST request
|
|
632
|
+
*/
|
|
633
|
+
async post(endpoint, data, options) {
|
|
634
|
+
const finalOptions = await this.applyRequestInterceptor(endpoint, {
|
|
635
|
+
...options,
|
|
636
|
+
method: "POST",
|
|
637
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
638
|
+
headers: {
|
|
639
|
+
"Content-Type": "application/json",
|
|
640
|
+
...options?.headers,
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
return this.request("POST", endpoint, finalOptions);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* PUT request
|
|
647
|
+
*/
|
|
648
|
+
async put(endpoint, data, options) {
|
|
649
|
+
const finalOptions = await this.applyRequestInterceptor(endpoint, {
|
|
650
|
+
...options,
|
|
651
|
+
method: "PUT",
|
|
652
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
...options?.headers,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
return this.request("PUT", endpoint, finalOptions);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* PATCH request (uses fetch fallback since MisoClient doesn't support PATCH)
|
|
662
|
+
*/
|
|
663
|
+
async patch(endpoint, data, options) {
|
|
664
|
+
const finalOptions = await this.applyRequestInterceptor(endpoint, {
|
|
665
|
+
...options,
|
|
666
|
+
method: "PATCH",
|
|
667
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
668
|
+
headers: {
|
|
669
|
+
"Content-Type": "application/json",
|
|
670
|
+
...options?.headers,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
return this.request("PATCH", endpoint, finalOptions);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* DELETE request
|
|
677
|
+
*/
|
|
678
|
+
async delete(endpoint, options) {
|
|
679
|
+
const finalOptions = await this.applyRequestInterceptor(endpoint, {
|
|
680
|
+
...options,
|
|
681
|
+
method: "DELETE",
|
|
682
|
+
});
|
|
683
|
+
return this.request("DELETE", endpoint, finalOptions);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
exports.DataClient = DataClient;
|
|
687
|
+
/**
|
|
688
|
+
* Singleton instance factory
|
|
689
|
+
*/
|
|
690
|
+
let defaultDataClient = null;
|
|
691
|
+
/**
|
|
692
|
+
* Get or create default DataClient instance
|
|
693
|
+
*/
|
|
694
|
+
function dataClient(config) {
|
|
695
|
+
if (!defaultDataClient && config) {
|
|
696
|
+
defaultDataClient = new DataClient(config);
|
|
697
|
+
}
|
|
698
|
+
if (!defaultDataClient) {
|
|
699
|
+
throw new Error("DataClient not initialized. Call dataClient(config) first.");
|
|
700
|
+
}
|
|
701
|
+
return defaultDataClient;
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=data-client.js.map
|