vesant-sdk 1.6.2 → 1.6.3

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,604 @@
1
+ // src/core/errors.ts
2
+ var VesantError = class _VesantError extends Error {
3
+ constructor(message, code, statusCode, details) {
4
+ super(message);
5
+ this.code = code;
6
+ this.statusCode = statusCode;
7
+ this.details = details;
8
+ this.name = "VesantError";
9
+ Object.setPrototypeOf(this, _VesantError.prototype);
10
+ }
11
+ };
12
+ var NetworkError = class _NetworkError extends VesantError {
13
+ constructor(message, originalError) {
14
+ super(message, "NETWORK_ERROR");
15
+ this.originalError = originalError;
16
+ this.name = "NetworkError";
17
+ Object.setPrototypeOf(this, _NetworkError.prototype);
18
+ }
19
+ };
20
+ var ValidationError = class _ValidationError extends VesantError {
21
+ constructor(message, fields) {
22
+ super(message, "VALIDATION_ERROR", 400, { fields });
23
+ this.name = "ValidationError";
24
+ Object.setPrototypeOf(this, _ValidationError.prototype);
25
+ }
26
+ };
27
+ var ServiceUnavailableError = class _ServiceUnavailableError extends VesantError {
28
+ constructor(message = "Service unavailable") {
29
+ super(message, "SERVICE_UNAVAILABLE", 503);
30
+ this.name = "ServiceUnavailableError";
31
+ Object.setPrototypeOf(this, _ServiceUnavailableError.prototype);
32
+ }
33
+ };
34
+ var AuthenticationError = class _AuthenticationError extends VesantError {
35
+ constructor(message = "Authentication failed") {
36
+ super(message, "AUTHENTICATION_ERROR", 401);
37
+ this.name = "AuthenticationError";
38
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
39
+ }
40
+ };
41
+ var RateLimitError = class _RateLimitError extends VesantError {
42
+ constructor(retryAfter) {
43
+ super("Rate limit exceeded", "RATE_LIMIT_EXCEEDED", 429, { retryAfter });
44
+ this.retryAfter = retryAfter;
45
+ this.name = "RateLimitError";
46
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
47
+ }
48
+ };
49
+ var TimeoutError = class _TimeoutError extends VesantError {
50
+ constructor(timeout) {
51
+ super(`Request timeout after ${timeout}ms`, "TIMEOUT", 408, { timeout });
52
+ this.timeout = timeout;
53
+ this.name = "TimeoutError";
54
+ Object.setPrototypeOf(this, _TimeoutError.prototype);
55
+ }
56
+ };
57
+ var CircuitBreakerOpenError = class _CircuitBreakerOpenError extends VesantError {
58
+ constructor() {
59
+ super("Circuit breaker is open \u2014 requests are temporarily blocked", "CIRCUIT_BREAKER_OPEN", 503);
60
+ this.name = "CircuitBreakerOpenError";
61
+ Object.setPrototypeOf(this, _CircuitBreakerOpenError.prototype);
62
+ }
63
+ };
64
+
65
+ // src/core/circuit-breaker.ts
66
+ var CircuitBreaker = class {
67
+ constructor(config = {}) {
68
+ this.state = "closed";
69
+ this.failures = 0;
70
+ this.successes = 0;
71
+ this.lastFailureTime = null;
72
+ this.failureThreshold = config.failureThreshold ?? 5;
73
+ this.resetTimeout = config.resetTimeout ?? 3e4;
74
+ this.successThreshold = config.successThreshold ?? 1;
75
+ }
76
+ /**
77
+ * Check if a request can proceed through the circuit breaker.
78
+ */
79
+ canExecute() {
80
+ if (this.state === "closed") {
81
+ return true;
82
+ }
83
+ if (this.state === "open") {
84
+ const now = Date.now();
85
+ if (this.lastFailureTime && now - this.lastFailureTime >= this.resetTimeout) {
86
+ this.state = "half-open";
87
+ this.successes = 0;
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ return true;
93
+ }
94
+ /**
95
+ * Record a successful request.
96
+ */
97
+ onSuccess() {
98
+ if (this.state === "half-open") {
99
+ this.successes++;
100
+ if (this.successes >= this.successThreshold) {
101
+ this.state = "closed";
102
+ this.failures = 0;
103
+ this.successes = 0;
104
+ this.lastFailureTime = null;
105
+ }
106
+ } else if (this.state === "closed") {
107
+ this.failures = 0;
108
+ }
109
+ }
110
+ /**
111
+ * Record a failed request.
112
+ */
113
+ onFailure() {
114
+ this.failures++;
115
+ this.lastFailureTime = Date.now();
116
+ if (this.state === "half-open") {
117
+ this.state = "open";
118
+ this.successes = 0;
119
+ } else if (this.state === "closed" && this.failures >= this.failureThreshold) {
120
+ this.state = "open";
121
+ }
122
+ }
123
+ /**
124
+ * Get current circuit breaker status.
125
+ */
126
+ getStatus() {
127
+ return {
128
+ state: this.state,
129
+ failures: this.failures,
130
+ successes: this.successes,
131
+ lastFailureTime: this.lastFailureTime,
132
+ nextRetryTime: this.state === "open" && this.lastFailureTime ? this.lastFailureTime + this.resetTimeout : null
133
+ };
134
+ }
135
+ /**
136
+ * Reset the circuit breaker to its initial closed state.
137
+ */
138
+ reset() {
139
+ this.state = "closed";
140
+ this.failures = 0;
141
+ this.successes = 0;
142
+ this.lastFailureTime = null;
143
+ }
144
+ };
145
+
146
+ // src/core/rate-limiter.ts
147
+ var RateLimitTracker = class {
148
+ constructor() {
149
+ this.limit = null;
150
+ this.remaining = null;
151
+ this.reset = null;
152
+ this.retryAfter = null;
153
+ }
154
+ /**
155
+ * Extract rate limit information from response headers.
156
+ */
157
+ updateFromHeaders(headers) {
158
+ const limit = headers.get("x-ratelimit-limit");
159
+ const remaining = headers.get("x-ratelimit-remaining");
160
+ const reset = headers.get("x-ratelimit-reset");
161
+ const retryAfter = headers.get("retry-after");
162
+ if (limit !== null) this.limit = parseInt(limit, 10);
163
+ if (remaining !== null) this.remaining = parseInt(remaining, 10);
164
+ if (reset !== null) this.reset = parseInt(reset, 10);
165
+ if (retryAfter !== null) this.retryAfter = parseInt(retryAfter, 10);
166
+ }
167
+ /**
168
+ * Check if the rate limit has been exceeded based on tracked headers.
169
+ */
170
+ isLimitExceeded() {
171
+ if (this.remaining !== null && this.remaining <= 0) {
172
+ if (this.reset !== null) {
173
+ const now = Math.floor(Date.now() / 1e3);
174
+ if (now >= this.reset) {
175
+ this.remaining = null;
176
+ this.reset = null;
177
+ this.retryAfter = null;
178
+ return false;
179
+ }
180
+ }
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ /**
186
+ * Get current rate limit status.
187
+ */
188
+ getStatus() {
189
+ return {
190
+ limit: this.limit,
191
+ remaining: this.remaining,
192
+ reset: this.reset,
193
+ retryAfter: this.retryAfter
194
+ };
195
+ }
196
+ };
197
+
198
+ // src/core/logger.ts
199
+ function createConsoleLogger() {
200
+ return {
201
+ debug(message, meta) {
202
+ console.log(`[Vesant SDK] ${message}`, meta !== void 0 ? meta : "");
203
+ },
204
+ info(message, meta) {
205
+ console.info(`[Vesant SDK] ${message}`, meta !== void 0 ? meta : "");
206
+ },
207
+ warn(message, meta) {
208
+ console.warn(`[Vesant SDK] ${message}`, meta !== void 0 ? meta : "");
209
+ },
210
+ error(message, meta) {
211
+ console.error(`[Vesant SDK] ${message}`, meta !== void 0 ? meta : "");
212
+ }
213
+ };
214
+ }
215
+
216
+ // src/core/version.ts
217
+ var SDK_VERSION = "1.6.2";
218
+
219
+ // src/shared/browser-utils.ts
220
+ function generateUUID() {
221
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
222
+ return crypto.randomUUID();
223
+ }
224
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
225
+ const r = Math.random() * 16 | 0;
226
+ const v = c === "x" ? r : r & 3 | 8;
227
+ return v.toString(16);
228
+ });
229
+ }
230
+
231
+ // src/core/client.ts
232
+ var BaseClient = class {
233
+ constructor(config) {
234
+ this.circuitBreaker = null;
235
+ this.rateLimitTracker = null;
236
+ if (!config.baseURL?.trim()) {
237
+ throw new ValidationError("baseURL is required and must be a non-empty string", ["baseURL"]);
238
+ }
239
+ if (!config.tenantId?.trim()) {
240
+ throw new ValidationError("tenantId is required and must be a non-empty string", ["tenantId"]);
241
+ }
242
+ this.interceptors = config.interceptors || [];
243
+ this.logger = config.logger || createConsoleLogger();
244
+ let environment = config.environment;
245
+ const apiKey = config.apiKey || "";
246
+ if (apiKey.startsWith("pk_test_")) {
247
+ if (environment === "production") {
248
+ this.logger.warn('Sandbox API key (pk_test_*) used with environment: "production" \u2014 overriding to "sandbox"');
249
+ }
250
+ environment = "sandbox";
251
+ } else if (apiKey.startsWith("pk_live_") && environment === "sandbox") {
252
+ this.logger.warn('Production API key (pk_live_*) used with environment: "sandbox" \u2014 sandbox isolation will still be applied for backward compatibility');
253
+ }
254
+ this.config = {
255
+ ...config,
256
+ apiKey,
257
+ environment,
258
+ headers: config.headers || {},
259
+ timeout: config.timeout || 1e4,
260
+ retries: config.retries || 3,
261
+ debug: config.debug || false,
262
+ interceptors: this.interceptors,
263
+ logger: this.logger
264
+ };
265
+ if (config.circuitBreaker) {
266
+ this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
267
+ }
268
+ if (config.enableRateLimitTracking) {
269
+ this.rateLimitTracker = new RateLimitTracker();
270
+ }
271
+ }
272
+ /**
273
+ * Make an HTTP request with timeout and error handling
274
+ */
275
+ async request(endpoint, options = {}, serviceURL, requestOptions) {
276
+ const requestId = generateUUID();
277
+ if (this.circuitBreaker && !this.circuitBreaker.canExecute()) {
278
+ const error = new CircuitBreakerOpenError();
279
+ error.requestId = requestId;
280
+ throw error;
281
+ }
282
+ if (this.rateLimitTracker && this.rateLimitTracker.isLimitExceeded()) {
283
+ const status = this.rateLimitTracker.getStatus();
284
+ const error = new RateLimitError(status.retryAfter ?? void 0);
285
+ error.requestId = requestId;
286
+ throw error;
287
+ }
288
+ const url = `${serviceURL || this.config.baseURL}${endpoint}`;
289
+ const headers = {
290
+ "Content-Type": "application/json",
291
+ "X-Tenant-ID": this.config.tenantId,
292
+ "X-SDK-Version": `vesant-sdk-ts/${SDK_VERSION}`,
293
+ "X-Request-ID": requestId,
294
+ ...this.config.headers,
295
+ ...options.headers || {}
296
+ };
297
+ if (this.config.apiKey) {
298
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
299
+ }
300
+ if (this.config.environment === "sandbox") {
301
+ headers["X-Sandbox"] = "true";
302
+ }
303
+ const method = (options.method || "GET").toUpperCase();
304
+ if (["POST", "PUT", "PATCH"].includes(method)) {
305
+ headers["Idempotency-Key"] = requestOptions?.idempotencyKey || generateUUID();
306
+ }
307
+ const controller = new AbortController();
308
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
309
+ if (requestOptions?.signal) {
310
+ if (requestOptions.signal.aborted) {
311
+ controller.abort();
312
+ } else {
313
+ requestOptions.signal.addEventListener("abort", () => controller.abort(), { once: true });
314
+ }
315
+ }
316
+ try {
317
+ let finalOptions = { ...options, headers };
318
+ for (const interceptor of this.interceptors) {
319
+ if (interceptor.onRequest) {
320
+ finalOptions = await interceptor.onRequest(url, finalOptions);
321
+ }
322
+ }
323
+ if (this.config.debug) {
324
+ this.logger.debug(`${finalOptions.method || "GET"} ${endpoint}`);
325
+ }
326
+ const response = await fetch(url, {
327
+ ...finalOptions,
328
+ signal: controller.signal
329
+ });
330
+ clearTimeout(timeoutId);
331
+ if (this.rateLimitTracker) {
332
+ this.rateLimitTracker.updateFromHeaders(response.headers);
333
+ }
334
+ if (requestOptions?.responseType === "arraybuffer") {
335
+ if (!response.ok) {
336
+ let errData = {};
337
+ try {
338
+ errData = await response.json();
339
+ } catch {
340
+ }
341
+ this.handleErrorResponse(response.status, errData, requestId);
342
+ }
343
+ this.circuitBreaker?.onSuccess();
344
+ const buffer = await response.arrayBuffer();
345
+ return buffer;
346
+ }
347
+ let data;
348
+ try {
349
+ data = await response.json();
350
+ } catch {
351
+ if (!response.ok) {
352
+ this.handleErrorResponse(response.status, {
353
+ error: `HTTP ${response.status}`,
354
+ message: response.statusText
355
+ }, requestId);
356
+ }
357
+ this.circuitBreaker?.onSuccess();
358
+ return void 0;
359
+ }
360
+ if (!response.ok) {
361
+ this.handleErrorResponse(response.status, data || {}, requestId);
362
+ }
363
+ this.circuitBreaker?.onSuccess();
364
+ let result = data;
365
+ for (const interceptor of this.interceptors) {
366
+ if (interceptor.onResponse) {
367
+ result = await interceptor.onResponse(url, result);
368
+ }
369
+ }
370
+ if (this.config.debug) {
371
+ this.logger.debug(`Response: ${response.status}`);
372
+ }
373
+ return result;
374
+ } catch (error) {
375
+ clearTimeout(timeoutId);
376
+ if (error instanceof VesantError && error.statusCode && error.statusCode >= 500) {
377
+ this.circuitBreaker?.onFailure();
378
+ } else if (error instanceof NetworkError || error instanceof TimeoutError) {
379
+ this.circuitBreaker?.onFailure();
380
+ }
381
+ if (error instanceof VesantError && !error.requestId) {
382
+ error.requestId = requestId;
383
+ }
384
+ if (error instanceof Error) {
385
+ for (const interceptor of this.interceptors) {
386
+ if (interceptor.onError) {
387
+ await interceptor.onError(url, error);
388
+ }
389
+ }
390
+ }
391
+ if (error instanceof Error) {
392
+ if (error.name === "AbortError") {
393
+ if (requestOptions?.signal?.aborted) {
394
+ const abortError = new VesantError("Request aborted", "REQUEST_ABORTED");
395
+ abortError.requestId = requestId;
396
+ throw abortError;
397
+ }
398
+ this.circuitBreaker?.onFailure();
399
+ const timeoutError = new TimeoutError(this.config.timeout);
400
+ timeoutError.requestId = requestId;
401
+ throw timeoutError;
402
+ }
403
+ if (error instanceof VesantError) {
404
+ throw error;
405
+ }
406
+ }
407
+ const networkError = new NetworkError("Network request failed", error);
408
+ networkError.requestId = requestId;
409
+ this.circuitBreaker?.onFailure();
410
+ throw networkError;
411
+ }
412
+ }
413
+ /**
414
+ * Make an HTTP request with retry logic
415
+ */
416
+ async requestWithRetry(endpoint, options = {}, serviceURL, retries = this.config.retries, requestOptions) {
417
+ let lastError;
418
+ for (let attempt = 0; attempt <= retries; attempt++) {
419
+ try {
420
+ return await this.request(endpoint, options, serviceURL, requestOptions);
421
+ } catch (error) {
422
+ lastError = error instanceof Error ? error : new Error("Unknown error");
423
+ if (requestOptions?.signal?.aborted) {
424
+ throw lastError;
425
+ }
426
+ if (lastError instanceof VesantError && lastError.statusCode && lastError.statusCode >= 400 && lastError.statusCode < 500 && lastError.statusCode !== 429) {
427
+ throw lastError;
428
+ }
429
+ if (attempt === retries) {
430
+ break;
431
+ }
432
+ let delay;
433
+ if (lastError instanceof RateLimitError && lastError.retryAfter) {
434
+ delay = lastError.retryAfter * 1e3;
435
+ } else {
436
+ delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 1e3, 1e4);
437
+ }
438
+ await new Promise((resolve) => setTimeout(resolve, delay));
439
+ if (this.config.debug) {
440
+ this.logger.debug(`Retry attempt ${attempt + 1}/${retries} after ${delay.toFixed(0)}ms`);
441
+ }
442
+ }
443
+ }
444
+ throw new NetworkError(`Request failed after ${retries} retries`, lastError);
445
+ }
446
+ /**
447
+ * Handle error responses from API
448
+ */
449
+ handleErrorResponse(status, data, requestId) {
450
+ const message = data.error || data.message || `HTTP ${status}`;
451
+ const createError = () => {
452
+ switch (status) {
453
+ case 400:
454
+ return new VesantError(message, "BAD_REQUEST", 400);
455
+ case 401:
456
+ return new AuthenticationError(message);
457
+ case 403:
458
+ return new VesantError(message, "FORBIDDEN", 403);
459
+ case 404:
460
+ return new VesantError(message, "NOT_FOUND", 404);
461
+ case 409:
462
+ return new VesantError(message, "DUPLICATE_PROFILE", 409);
463
+ case 429: {
464
+ const retryAfter = data.retry_after || data.retryAfter;
465
+ return new RateLimitError(retryAfter);
466
+ }
467
+ case 500:
468
+ case 502:
469
+ case 503:
470
+ case 504:
471
+ return new ServiceUnavailableError(message);
472
+ default:
473
+ return new VesantError(message, "UNKNOWN_ERROR", status);
474
+ }
475
+ };
476
+ const error = createError();
477
+ if (requestId) {
478
+ error.requestId = requestId;
479
+ }
480
+ throw error;
481
+ }
482
+ /**
483
+ * Build query string from parameters
484
+ */
485
+ buildQueryString(params) {
486
+ const query = new URLSearchParams();
487
+ Object.entries(params).forEach(([key, value]) => {
488
+ if (value !== void 0 && value !== null) {
489
+ if (Array.isArray(value)) {
490
+ value.forEach((item) => query.append(key, String(item)));
491
+ } else {
492
+ query.append(key, String(value));
493
+ }
494
+ }
495
+ });
496
+ const queryString = query.toString();
497
+ return queryString ? `?${queryString}` : "";
498
+ }
499
+ /**
500
+ * Update client configuration
501
+ */
502
+ updateConfig(config) {
503
+ this.config = {
504
+ ...this.config,
505
+ ...config,
506
+ headers: {
507
+ ...this.config.headers,
508
+ ...config.headers || {}
509
+ }
510
+ };
511
+ if (config.logger) {
512
+ this.logger = config.logger;
513
+ }
514
+ if (config.interceptors) {
515
+ this.interceptors = config.interceptors;
516
+ }
517
+ }
518
+ /**
519
+ * Get current configuration (readonly)
520
+ */
521
+ getConfig() {
522
+ return { ...this.config };
523
+ }
524
+ /**
525
+ * Get rate limit status from tracked response headers.
526
+ */
527
+ getRateLimitStatus() {
528
+ return this.rateLimitTracker?.getStatus() ?? null;
529
+ }
530
+ /**
531
+ * Get circuit breaker status.
532
+ */
533
+ getCircuitBreakerStatus() {
534
+ return this.circuitBreaker?.getStatus() ?? null;
535
+ }
536
+ /**
537
+ * Health check endpoint
538
+ */
539
+ async healthCheck() {
540
+ return this.request("/api/v1/health");
541
+ }
542
+ };
543
+
544
+ // src/fraud/client.ts
545
+ var FraudClient = class extends BaseClient {
546
+ /**
547
+ * Submit a single fraud event for scoring.
548
+ */
549
+ async scoreEvent(request, requestOptions) {
550
+ this.validateScoreRequest(request);
551
+ return this.requestWithRetry(
552
+ "/api/v1/fraud/score",
553
+ {
554
+ method: "POST",
555
+ body: JSON.stringify(request)
556
+ },
557
+ void 0,
558
+ void 0,
559
+ requestOptions
560
+ );
561
+ }
562
+ /**
563
+ * Submit multiple fraud events for scoring.
564
+ */
565
+ async scoreEventsBulk(requests, requestOptions) {
566
+ if (!Array.isArray(requests) || requests.length === 0) {
567
+ throw new ValidationError(
568
+ "Invalid bulk score request: at least one score request is required",
569
+ ["requests"]
570
+ );
571
+ }
572
+ requests.forEach((request, index) => this.validateScoreRequest(request, index));
573
+ return this.requestWithRetry(
574
+ "/api/v1/fraud/score/bulk",
575
+ {
576
+ method: "POST",
577
+ body: JSON.stringify(requests)
578
+ },
579
+ void 0,
580
+ void 0,
581
+ requestOptions
582
+ );
583
+ }
584
+ validateScoreRequest(request, index) {
585
+ const errors = [];
586
+ const prefix = index === void 0 ? "" : `requests[${index}].`;
587
+ if (!request.customer_id?.trim()) {
588
+ errors.push(`${prefix}customer_id is required`);
589
+ }
590
+ if (!request.sift_user_id?.trim()) {
591
+ errors.push(`${prefix}sift_user_id is required`);
592
+ }
593
+ if (!request.event_type?.trim()) {
594
+ errors.push(`${prefix}event_type is required`);
595
+ }
596
+ if (errors.length > 0) {
597
+ throw new ValidationError(`Invalid fraud score request: ${errors.join(", ")}`, errors);
598
+ }
599
+ }
600
+ };
601
+
602
+ export { FraudClient };
603
+ //# sourceMappingURL=index.mjs.map
604
+ //# sourceMappingURL=index.mjs.map