@vaultsaas/core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,4250 @@
1
+ // src/errors/error-codes.ts
2
+ var ERROR_DOCS_BASE_URL = "https://docs.vaultsaas.com/errors";
3
+ var FALLBACK_CODE_DEFINITION = {
4
+ category: "unknown",
5
+ suggestion: "Review provider response details and retry only when the failure is transient.",
6
+ retriable: false
7
+ };
8
+ var VAULT_ERROR_CODE_DEFINITIONS = {
9
+ INVALID_CONFIGURATION: {
10
+ category: "configuration_error",
11
+ suggestion: "Check VaultClient configuration values and required provider settings.",
12
+ retriable: false
13
+ },
14
+ PROVIDER_NOT_CONFIGURED: {
15
+ category: "configuration_error",
16
+ suggestion: "Add the provider adapter and credentials to VaultClient.providers.",
17
+ retriable: false
18
+ },
19
+ ADAPTER_NOT_FOUND: {
20
+ category: "configuration_error",
21
+ suggestion: "Install the provider adapter package and wire it in config.",
22
+ retriable: false
23
+ },
24
+ PROVIDER_AUTH_FAILED: {
25
+ category: "configuration_error",
26
+ suggestion: "Verify provider credentials and ensure they match the current environment.",
27
+ retriable: false
28
+ },
29
+ NO_ROUTING_MATCH: {
30
+ category: "routing_error",
31
+ suggestion: "Add a matching routing rule or configure a default fallback provider.",
32
+ retriable: false
33
+ },
34
+ ROUTING_PROVIDER_EXCLUDED: {
35
+ category: "routing_error",
36
+ suggestion: "Remove the forced provider from exclusions or choose a different provider override.",
37
+ retriable: false
38
+ },
39
+ ROUTING_PROVIDER_UNAVAILABLE: {
40
+ category: "routing_error",
41
+ suggestion: "Enable the provider in config or update routing rules to a valid provider.",
42
+ retriable: false
43
+ },
44
+ INVALID_REQUEST: {
45
+ category: "invalid_request",
46
+ suggestion: "Fix invalid or missing request fields before retrying the operation.",
47
+ retriable: false
48
+ },
49
+ IDEMPOTENCY_CONFLICT: {
50
+ category: "invalid_request",
51
+ suggestion: "Reuse the same payload for an idempotency key or generate a new key.",
52
+ retriable: false
53
+ },
54
+ WEBHOOK_SIGNATURE_INVALID: {
55
+ category: "invalid_request",
56
+ suggestion: "Verify webhook secret, signature algorithm, and that the raw body is unmodified.",
57
+ retriable: false
58
+ },
59
+ CARD_DECLINED: {
60
+ category: "card_declined",
61
+ suggestion: "Ask the customer for another payment method or a retry with updated details.",
62
+ retriable: false
63
+ },
64
+ AUTHENTICATION_REQUIRED: {
65
+ category: "authentication_required",
66
+ suggestion: "Trigger customer authentication (for example, a 3DS challenge).",
67
+ retriable: false
68
+ },
69
+ FRAUD_SUSPECTED: {
70
+ category: "fraud_suspected",
71
+ suggestion: "Block automatic retries and route the payment through manual fraud review.",
72
+ retriable: false
73
+ },
74
+ RATE_LIMITED: {
75
+ category: "rate_limited",
76
+ suggestion: "Apply exponential backoff before retrying provider requests.",
77
+ retriable: true
78
+ },
79
+ NETWORK_ERROR: {
80
+ category: "network_error",
81
+ suggestion: "Retry with backoff and confirm outbound connectivity/timeouts to the provider.",
82
+ retriable: true
83
+ },
84
+ PROVIDER_TIMEOUT: {
85
+ category: "network_error",
86
+ suggestion: "Retry with backoff and increase timeout if the provider latency is expected.",
87
+ retriable: true
88
+ },
89
+ PLATFORM_UNREACHABLE: {
90
+ category: "network_error",
91
+ suggestion: "Use local routing fallback and verify platform API key and network reachability.",
92
+ retriable: true
93
+ },
94
+ PROVIDER_ERROR: {
95
+ category: "provider_error",
96
+ suggestion: "Retry if transient, or fail over to another provider when configured.",
97
+ retriable: true
98
+ },
99
+ PROVIDER_UNKNOWN: {
100
+ category: "unknown",
101
+ suggestion: "Capture provider response metadata and map this error case for deterministic handling.",
102
+ retriable: false
103
+ }
104
+ };
105
+ function getVaultErrorCodeDefinition(code) {
106
+ return VAULT_ERROR_CODE_DEFINITIONS[code] ?? FALLBACK_CODE_DEFINITION;
107
+ }
108
+ function buildVaultErrorDocsUrl(code, docsPath) {
109
+ const path = docsPath ?? code.toLowerCase();
110
+ return `${ERROR_DOCS_BASE_URL}/${path}`;
111
+ }
112
+
113
+ // src/errors/vault-error.ts
114
+ var VaultError = class extends Error {
115
+ code;
116
+ category;
117
+ suggestion;
118
+ docsUrl;
119
+ retriable;
120
+ context;
121
+ constructor(message, options) {
122
+ super(message);
123
+ const definition = getVaultErrorCodeDefinition(options.code);
124
+ this.name = "VaultError";
125
+ this.code = options.code;
126
+ this.category = options.category ?? definition.category;
127
+ this.suggestion = options.suggestion ?? definition.suggestion;
128
+ this.docsUrl = options.docsUrl ?? buildVaultErrorDocsUrl(options.code, definition.docsPath);
129
+ this.retriable = options.retriable ?? definition.retriable;
130
+ this.context = options.context ?? {};
131
+ Object.setPrototypeOf(this, new.target.prototype);
132
+ }
133
+ };
134
+ var SUBCLASS_OPTION_KEYS = [
135
+ "code",
136
+ "category",
137
+ "suggestion",
138
+ "docsUrl",
139
+ "retriable",
140
+ "context"
141
+ ];
142
+ function isSubclassOptions(value) {
143
+ if (!value || typeof value !== "object") {
144
+ return false;
145
+ }
146
+ const record = value;
147
+ return SUBCLASS_OPTION_KEYS.some((key) => key in record);
148
+ }
149
+ function normalizeSubclassOptions(value) {
150
+ if (isSubclassOptions(value)) {
151
+ return value;
152
+ }
153
+ return value ? { context: value } : {};
154
+ }
155
+ var VaultConfigError = class extends VaultError {
156
+ constructor(message, options) {
157
+ const normalized = normalizeSubclassOptions(options);
158
+ super(message, {
159
+ code: normalized.code ?? "INVALID_CONFIGURATION",
160
+ category: normalized.category,
161
+ suggestion: normalized.suggestion,
162
+ docsUrl: normalized.docsUrl,
163
+ retriable: normalized.retriable,
164
+ context: normalized.context
165
+ });
166
+ this.name = "VaultConfigError";
167
+ }
168
+ };
169
+ var VaultRoutingError = class extends VaultError {
170
+ constructor(message, options) {
171
+ const normalized = normalizeSubclassOptions(options);
172
+ super(message, {
173
+ code: normalized.code ?? "NO_ROUTING_MATCH",
174
+ category: normalized.category,
175
+ suggestion: normalized.suggestion,
176
+ docsUrl: normalized.docsUrl,
177
+ retriable: normalized.retriable,
178
+ context: normalized.context
179
+ });
180
+ this.name = "VaultRoutingError";
181
+ }
182
+ };
183
+ var VaultProviderError = class extends VaultError {
184
+ constructor(message, options) {
185
+ const normalized = normalizeSubclassOptions(options);
186
+ super(message, {
187
+ code: normalized.code ?? "PROVIDER_ERROR",
188
+ category: normalized.category,
189
+ suggestion: normalized.suggestion,
190
+ docsUrl: normalized.docsUrl,
191
+ retriable: normalized.retriable,
192
+ context: normalized.context
193
+ });
194
+ this.name = "VaultProviderError";
195
+ }
196
+ };
197
+ var VaultNetworkError = class extends VaultError {
198
+ constructor(message, options) {
199
+ const normalized = normalizeSubclassOptions(options);
200
+ super(message, {
201
+ code: normalized.code ?? "NETWORK_ERROR",
202
+ category: normalized.category,
203
+ suggestion: normalized.suggestion,
204
+ docsUrl: normalized.docsUrl,
205
+ retriable: normalized.retriable ?? true,
206
+ context: normalized.context
207
+ });
208
+ this.name = "VaultNetworkError";
209
+ }
210
+ };
211
+ var WebhookVerificationError = class extends VaultError {
212
+ constructor(message, options) {
213
+ const normalized = normalizeSubclassOptions(options);
214
+ super(message, {
215
+ code: normalized.code ?? "WEBHOOK_SIGNATURE_INVALID",
216
+ category: normalized.category,
217
+ suggestion: normalized.suggestion,
218
+ docsUrl: normalized.docsUrl,
219
+ retriable: normalized.retriable,
220
+ context: normalized.context
221
+ });
222
+ this.name = "WebhookVerificationError";
223
+ }
224
+ };
225
+ var VaultIdempotencyConflictError = class extends VaultError {
226
+ constructor(message, options) {
227
+ const normalized = normalizeSubclassOptions(options);
228
+ super(message, {
229
+ code: normalized.code ?? "IDEMPOTENCY_CONFLICT",
230
+ category: normalized.category,
231
+ suggestion: normalized.suggestion,
232
+ docsUrl: normalized.docsUrl,
233
+ retriable: normalized.retriable,
234
+ context: normalized.context
235
+ });
236
+ this.name = "VaultIdempotencyConflictError";
237
+ }
238
+ };
239
+
240
+ // src/errors/provider-error-mapper.ts
241
+ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
242
+ "ECONNABORTED",
243
+ "ECONNREFUSED",
244
+ "ECONNRESET",
245
+ "ENETUNREACH",
246
+ "ENOTFOUND",
247
+ "ETIMEDOUT",
248
+ "EAI_AGAIN",
249
+ "UND_ERR_CONNECT_TIMEOUT"
250
+ ]);
251
+ var CARD_DECLINED_PATTERNS = [
252
+ /card[\s_-]?declined/i,
253
+ /insufficient[\s_-]?funds/i,
254
+ /do[\s_-]?not[\s_-]?honou?r/i,
255
+ /generic[\s_-]?decline/i
256
+ ];
257
+ var AUTHENTICATION_REQUIRED_PATTERNS = [
258
+ /\b3ds\b/i,
259
+ /authentication[\s_-]?required/i,
260
+ /requires[\s_-]?action/i,
261
+ /challenge[\s_-]?required/i
262
+ ];
263
+ var FRAUD_PATTERNS = [
264
+ /fraud/i,
265
+ /risk[\s_-]?check/i,
266
+ /suspected/i,
267
+ /blocked[\s_-]?for[\s_-]?risk/i
268
+ ];
269
+ var INVALID_REQUEST_PATTERNS = [
270
+ /invalid[\s_-]?request/i,
271
+ /missing required/i,
272
+ /malformed/i,
273
+ /validation/i
274
+ ];
275
+ var RATE_LIMIT_PATTERNS = [/rate[\s_-]?limit/i, /too many requests/i];
276
+ var AUTH_FAILED_PATTERNS = [
277
+ /invalid[\s_-]?api[\s_-]?key/i,
278
+ /unauthorized/i,
279
+ /authentication failed/i,
280
+ /forbidden/i
281
+ ];
282
+ function asRecord(value) {
283
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
284
+ return null;
285
+ }
286
+ return value;
287
+ }
288
+ function readString(source, key) {
289
+ if (!source) {
290
+ return void 0;
291
+ }
292
+ const value = source[key];
293
+ return typeof value === "string" && value.trim() ? value : void 0;
294
+ }
295
+ function readNumber(source, key) {
296
+ if (!source) {
297
+ return void 0;
298
+ }
299
+ const value = source[key];
300
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
301
+ }
302
+ function matchAny(text, patterns) {
303
+ return patterns.some((pattern) => pattern.test(text));
304
+ }
305
+ function isProviderErrorHint(value) {
306
+ const record = asRecord(value);
307
+ if (!record) {
308
+ return false;
309
+ }
310
+ return typeof record.providerCode === "string" || typeof record.providerMessage === "string" || typeof record.requestId === "string" || typeof record.httpStatus === "number" || typeof record.declineCode === "string" || typeof record.type === "string" || typeof record.isNetworkError === "boolean" || typeof record.isTimeout === "boolean" || "raw" in record;
311
+ }
312
+ function extractHint(source) {
313
+ if (isProviderErrorHint(source)) {
314
+ return source;
315
+ }
316
+ const record = asRecord(source);
317
+ if (!record) {
318
+ return void 0;
319
+ }
320
+ const hint = record.hint;
321
+ if (isProviderErrorHint(hint)) {
322
+ return hint;
323
+ }
324
+ const providerError = record.providerError;
325
+ if (isProviderErrorHint(providerError)) {
326
+ return providerError;
327
+ }
328
+ return void 0;
329
+ }
330
+ function extractProviderError(error) {
331
+ const record = asRecord(error);
332
+ const hint = extractHint(error);
333
+ const response = asRecord(record?.response);
334
+ const responseData = asRecord(response?.data);
335
+ const responseError = asRecord(responseData?.error);
336
+ const providerCode = hint?.providerCode ?? readString(record, "providerCode") ?? readString(responseError, "code") ?? readString(responseData, "code");
337
+ const providerMessage = hint?.providerMessage ?? readString(record, "providerMessage") ?? readString(responseError, "message") ?? readString(responseData, "message");
338
+ const requestId = hint?.requestId ?? readString(record, "requestId") ?? readString(response, "requestId");
339
+ const httpStatus = hint?.httpStatus ?? readNumber(record, "status") ?? readNumber(record, "statusCode") ?? readNumber(response, "status");
340
+ const declineCode = hint?.declineCode ?? readString(record, "declineCode") ?? readString(responseError, "declineCode") ?? readString(responseData, "declineCode");
341
+ const type = hint?.type ?? readString(record, "type") ?? readString(responseError, "type") ?? readString(responseData, "type");
342
+ const message = providerMessage ?? (error instanceof Error ? error.message : void 0) ?? readString(record, "message") ?? "Provider operation failed.";
343
+ const errorCode = readString(record, "code") ?? readString(responseError, "code") ?? readString(responseData, "code");
344
+ const errorCodeUpper = errorCode?.toUpperCase();
345
+ const textBlob = [message, providerMessage, providerCode, declineCode, type].filter((value) => Boolean(value)).join(" ");
346
+ const isTimeout = hint?.isTimeout ?? (errorCodeUpper === "ETIMEDOUT" || /timeout|timed out/i.test(textBlob));
347
+ const isNetworkError = hint?.isNetworkError ?? (isTimeout || (errorCodeUpper ? NETWORK_ERROR_CODES.has(errorCodeUpper) : false) || /network|socket|dns|connection reset|connection refused/i.test(textBlob));
348
+ return {
349
+ message,
350
+ providerCode,
351
+ providerMessage,
352
+ requestId,
353
+ httpStatus,
354
+ declineCode,
355
+ type,
356
+ errorCode,
357
+ isNetworkError,
358
+ isTimeout,
359
+ raw: hint?.raw ?? responseData ?? error
360
+ };
361
+ }
362
+ function classifyProviderError(details) {
363
+ const textBlob = [
364
+ details.message,
365
+ details.providerMessage,
366
+ details.providerCode,
367
+ details.declineCode,
368
+ details.type
369
+ ].filter((value) => Boolean(value)).join(" ");
370
+ if (details.httpStatus === 429 || matchAny(textBlob, RATE_LIMIT_PATTERNS)) {
371
+ return { code: "RATE_LIMITED" };
372
+ }
373
+ if (details.httpStatus === 401 || details.httpStatus === 403 || matchAny(textBlob, AUTH_FAILED_PATTERNS)) {
374
+ return { code: "PROVIDER_AUTH_FAILED" };
375
+ }
376
+ if (matchAny(textBlob, AUTHENTICATION_REQUIRED_PATTERNS)) {
377
+ return { code: "AUTHENTICATION_REQUIRED" };
378
+ }
379
+ if (matchAny(textBlob, FRAUD_PATTERNS)) {
380
+ return { code: "FRAUD_SUSPECTED" };
381
+ }
382
+ if (matchAny(textBlob, CARD_DECLINED_PATTERNS)) {
383
+ return { code: "CARD_DECLINED" };
384
+ }
385
+ if (details.httpStatus === 400 || details.httpStatus === 404 || details.httpStatus === 409 || details.httpStatus === 422 || matchAny(textBlob, INVALID_REQUEST_PATTERNS)) {
386
+ return { code: "INVALID_REQUEST" };
387
+ }
388
+ if (details.httpStatus !== void 0 && details.httpStatus >= 500) {
389
+ return { code: "PROVIDER_ERROR" };
390
+ }
391
+ return { code: "PROVIDER_UNKNOWN" };
392
+ }
393
+ function mapProviderError(error, mappingContext) {
394
+ if (error instanceof VaultError) {
395
+ return error;
396
+ }
397
+ const details = extractProviderError(error);
398
+ const context = {
399
+ provider: mappingContext.provider,
400
+ operation: mappingContext.operation,
401
+ providerCode: details.providerCode,
402
+ providerMessage: details.providerMessage ?? details.message,
403
+ requestId: details.requestId,
404
+ httpStatus: details.httpStatus,
405
+ declineCode: details.declineCode,
406
+ errorCode: details.errorCode,
407
+ raw: details.raw
408
+ };
409
+ if (details.isNetworkError) {
410
+ return new VaultNetworkError(details.message, {
411
+ code: details.isTimeout ? "PROVIDER_TIMEOUT" : "NETWORK_ERROR",
412
+ context
413
+ });
414
+ }
415
+ const classification = classifyProviderError(details);
416
+ return new VaultProviderError(details.message, {
417
+ code: classification.code,
418
+ context
419
+ });
420
+ }
421
+
422
+ // src/webhooks/event-types.ts
423
+ var DEFAULT_VAULT_EVENT_TYPES = [
424
+ "payment.completed",
425
+ "payment.failed",
426
+ "payment.pending",
427
+ "payment.requires_action",
428
+ "payment.refunded",
429
+ "payment.partially_refunded",
430
+ "payment.disputed",
431
+ "payment.dispute_resolved",
432
+ "payout.completed",
433
+ "payout.failed"
434
+ ];
435
+
436
+ // src/webhooks/handler.ts
437
+ var KNOWN_EVENT_TYPES = {
438
+ "payment.completed": true,
439
+ "payment.failed": true,
440
+ "payment.pending": true,
441
+ "payment.requires_action": true,
442
+ "payment.refunded": true,
443
+ "payment.partially_refunded": true,
444
+ "payment.disputed": true,
445
+ "payment.dispute_resolved": true,
446
+ "payout.completed": true,
447
+ "payout.failed": true
448
+ };
449
+ function normalizeEventType(value) {
450
+ if (value && value in KNOWN_EVENT_TYPES) {
451
+ return value;
452
+ }
453
+ return "payment.failed";
454
+ }
455
+ function normalizeWebhookEvent(provider, payload, rawPayload = payload) {
456
+ const timestamp = payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
457
+ const providerEventId = payload.providerEventId ?? payload.id ?? `pevt_${Date.now()}`;
458
+ return {
459
+ id: payload.id ?? `vevt_${provider}_${Date.now()}`,
460
+ type: normalizeEventType(payload.type),
461
+ provider,
462
+ transactionId: payload.transactionId,
463
+ providerEventId,
464
+ data: payload.data ?? {},
465
+ rawPayload,
466
+ timestamp
467
+ };
468
+ }
469
+
470
+ // src/adapters/shared/http.ts
471
+ function asRecord2(value) {
472
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
473
+ return null;
474
+ }
475
+ return value;
476
+ }
477
+ function readString2(source, key) {
478
+ if (!source) {
479
+ return void 0;
480
+ }
481
+ const value = source[key];
482
+ return typeof value === "string" && value.trim() ? value : void 0;
483
+ }
484
+ function readNumber2(source, key) {
485
+ if (!source) {
486
+ return void 0;
487
+ }
488
+ const value = source[key];
489
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
490
+ }
491
+ function stringifyBody(body) {
492
+ if (body === void 0 || body === null) {
493
+ return void 0;
494
+ }
495
+ if (typeof body === "string") {
496
+ return body;
497
+ }
498
+ return JSON.stringify(body);
499
+ }
500
+ function parseJsonSafe(text) {
501
+ try {
502
+ return JSON.parse(text);
503
+ } catch {
504
+ return null;
505
+ }
506
+ }
507
+ function encodeFormBody(body) {
508
+ const params = new URLSearchParams();
509
+ function appendValue(prefix, value) {
510
+ if (value === void 0) {
511
+ return;
512
+ }
513
+ if (value === null) {
514
+ params.append(prefix, "");
515
+ return;
516
+ }
517
+ if (Array.isArray(value)) {
518
+ value.forEach((item, index) => {
519
+ appendValue(`${prefix}[${index}]`, item);
520
+ });
521
+ return;
522
+ }
523
+ if (typeof value === "object") {
524
+ for (const [key, nestedValue] of Object.entries(
525
+ value
526
+ )) {
527
+ appendValue(`${prefix}[${key}]`, nestedValue);
528
+ }
529
+ return;
530
+ }
531
+ const primitive = value;
532
+ params.append(prefix, String(primitive));
533
+ }
534
+ for (const [key, value] of Object.entries(body)) {
535
+ appendValue(key, value);
536
+ }
537
+ return params;
538
+ }
539
+ function readHeader(headers, name) {
540
+ const needle = name.toLowerCase();
541
+ for (const [key, value] of Object.entries(headers)) {
542
+ if (key.toLowerCase() === needle) {
543
+ return value;
544
+ }
545
+ }
546
+ return void 0;
547
+ }
548
+ async function requestJson(options) {
549
+ const controller = new AbortController();
550
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
551
+ const url = `${options.baseUrl}${options.path}`;
552
+ const serializedBody = stringifyBody(options.body);
553
+ const headers = {
554
+ ...options.body !== void 0 ? { "content-type": "application/json" } : {},
555
+ ...options.headers
556
+ };
557
+ try {
558
+ const response = await options.fetchFn(url, {
559
+ method: options.method ?? "GET",
560
+ headers,
561
+ body: serializedBody,
562
+ signal: controller.signal
563
+ });
564
+ const text = await response.text();
565
+ const payload = text ? parseJsonSafe(text) : null;
566
+ if (!response.ok) {
567
+ const payloadRecord = asRecord2(payload);
568
+ const errorRecord = asRecord2(payloadRecord?.error) ?? payloadRecord;
569
+ const hint = {
570
+ httpStatus: response.status,
571
+ providerCode: readString2(errorRecord, "code") ?? readString2(payloadRecord, "code"),
572
+ providerMessage: readString2(errorRecord, "message") ?? readString2(payloadRecord, "message") ?? response.statusText,
573
+ declineCode: readString2(errorRecord, "decline_code") ?? readString2(errorRecord, "declineCode"),
574
+ type: readString2(errorRecord, "type"),
575
+ requestId: response.headers.get("request-id") ?? response.headers.get("x-request-id") ?? void 0,
576
+ raw: payload
577
+ };
578
+ throw {
579
+ message: hint.providerMessage ?? `Provider request failed with status ${response.status}.`,
580
+ status: response.status,
581
+ hint
582
+ };
583
+ }
584
+ if (!text) {
585
+ return {};
586
+ }
587
+ return payload;
588
+ } catch (error) {
589
+ const record = asRecord2(error);
590
+ const isAbortError = error instanceof Error && error.name === "AbortError";
591
+ const message = (error instanceof Error ? error.message : void 0) ?? readString2(record, "message") ?? "Provider request failed.";
592
+ if (isAbortError) {
593
+ throw {
594
+ message: "Request timed out.",
595
+ code: "ETIMEDOUT",
596
+ hint: {
597
+ httpStatus: readNumber2(record, "status"),
598
+ providerMessage: message,
599
+ isNetworkError: true,
600
+ isTimeout: true,
601
+ raw: error
602
+ }
603
+ };
604
+ }
605
+ if (record && "hint" in record) {
606
+ throw error;
607
+ }
608
+ throw {
609
+ message,
610
+ hint: {
611
+ providerMessage: message,
612
+ isNetworkError: true,
613
+ raw: error
614
+ }
615
+ };
616
+ } finally {
617
+ clearTimeout(timeout);
618
+ }
619
+ }
620
+
621
+ // src/adapters/shared/signature.ts
622
+ import { createHmac, timingSafeEqual } from "crypto";
623
+ function toRawString(payload) {
624
+ return typeof payload === "string" ? payload : payload.toString("utf-8");
625
+ }
626
+ function createHmacDigest(algorithm, secret, content) {
627
+ return createHmac(algorithm, secret).update(content).digest("hex");
628
+ }
629
+ function secureCompareHex(leftHex, rightHex) {
630
+ const left = Buffer.from(leftHex, "hex");
631
+ const right = Buffer.from(rightHex, "hex");
632
+ if (left.length !== right.length || left.length === 0) {
633
+ return false;
634
+ }
635
+ return timingSafeEqual(left, right);
636
+ }
637
+
638
+ // src/adapters/dlocal-adapter.ts
639
+ var DEFAULT_DLOCAL_BASE_URL = "https://api.dlocal.com";
640
+ var DEFAULT_TIMEOUT_MS = 15e3;
641
+ var DLOCAL_API_VERSION = "2.1";
642
+ function asRecord3(value) {
643
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
644
+ return null;
645
+ }
646
+ return value;
647
+ }
648
+ function readString3(source, key) {
649
+ if (!source) {
650
+ return void 0;
651
+ }
652
+ const value = source[key];
653
+ return typeof value === "string" && value.trim() ? value : void 0;
654
+ }
655
+ function readNumber3(source, key) {
656
+ if (!source) {
657
+ return void 0;
658
+ }
659
+ const value = source[key];
660
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
661
+ }
662
+ function mapDLocalStatus(status) {
663
+ switch (status?.toUpperCase()) {
664
+ case "AUTHORIZED":
665
+ return "authorized";
666
+ case "PAID":
667
+ case "APPROVED":
668
+ case "CAPTURED":
669
+ return "completed";
670
+ case "PENDING":
671
+ case "IN_PROCESS":
672
+ return "pending";
673
+ case "REJECTED":
674
+ case "DECLINED":
675
+ return "declined";
676
+ case "CANCELED":
677
+ case "CANCELLED":
678
+ return "cancelled";
679
+ case "REQUIRES_ACTION":
680
+ return "requires_action";
681
+ default:
682
+ return "failed";
683
+ }
684
+ }
685
+ function mapRefundStatus(status) {
686
+ switch (status?.toUpperCase()) {
687
+ case "COMPLETED":
688
+ case "APPROVED":
689
+ return "completed";
690
+ case "PENDING":
691
+ return "pending";
692
+ default:
693
+ return "failed";
694
+ }
695
+ }
696
+ function mapDLocalEventType(type) {
697
+ switch (type?.toLowerCase()) {
698
+ case "payment.approved":
699
+ case "payment.captured":
700
+ return "payment.completed";
701
+ case "payment.pending":
702
+ return "payment.pending";
703
+ case "payment.failed":
704
+ case "payment.rejected":
705
+ return "payment.failed";
706
+ case "payment.refunded":
707
+ return "payment.refunded";
708
+ case "payment.partially_refunded":
709
+ return "payment.partially_refunded";
710
+ case "chargeback.created":
711
+ return "payment.disputed";
712
+ case "chargeback.closed":
713
+ return "payment.dispute_resolved";
714
+ default:
715
+ return "payment.failed";
716
+ }
717
+ }
718
+ function mapPaymentMethod(paymentMethod) {
719
+ if (paymentMethod.type === "card" && "token" in paymentMethod) {
720
+ return {
721
+ payment_method_id: "CARD",
722
+ card: {
723
+ token: paymentMethod.token
724
+ }
725
+ };
726
+ }
727
+ if (paymentMethod.type === "card") {
728
+ return {
729
+ payment_method_id: "CARD",
730
+ card: {
731
+ number: paymentMethod.number,
732
+ expiration_month: paymentMethod.expMonth,
733
+ expiration_year: paymentMethod.expYear,
734
+ cvv: paymentMethod.cvc
735
+ }
736
+ };
737
+ }
738
+ if (paymentMethod.type === "pix") {
739
+ return {
740
+ payment_method_id: "PIX"
741
+ };
742
+ }
743
+ if (paymentMethod.type === "boleto") {
744
+ return {
745
+ payment_method_id: "BOLETO",
746
+ payer: {
747
+ document: paymentMethod.customerDocument
748
+ }
749
+ };
750
+ }
751
+ if (paymentMethod.type === "bank_transfer") {
752
+ return {
753
+ payment_method_id: "BANK_TRANSFER",
754
+ bank_transfer: {
755
+ bank_code: paymentMethod.bankCode,
756
+ account_number: paymentMethod.accountNumber
757
+ }
758
+ };
759
+ }
760
+ return {
761
+ payment_method_id: paymentMethod.type.toUpperCase()
762
+ };
763
+ }
764
+ function timestampOrNow(input) {
765
+ if (!input) {
766
+ return (/* @__PURE__ */ new Date()).toISOString();
767
+ }
768
+ const date = new Date(input);
769
+ return Number.isNaN(date.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : date.toISOString();
770
+ }
771
+ var DLocalAdapter = class _DLocalAdapter {
772
+ name = "dlocal";
773
+ static supportedMethods = [
774
+ "card",
775
+ "pix",
776
+ "boleto",
777
+ "bank_transfer"
778
+ ];
779
+ static supportedCurrencies = [
780
+ "BRL",
781
+ "MXN",
782
+ "ARS",
783
+ "CLP",
784
+ "COP",
785
+ "PEN",
786
+ "UYU",
787
+ "BOB",
788
+ "PYG",
789
+ "CRC",
790
+ "GTQ",
791
+ "PAB",
792
+ "DOP",
793
+ "USD"
794
+ ];
795
+ static supportedCountries = [
796
+ "BR",
797
+ "MX",
798
+ "AR",
799
+ "CL",
800
+ "CO",
801
+ "PE",
802
+ "UY",
803
+ "BO",
804
+ "PY",
805
+ "CR",
806
+ "GT",
807
+ "PA",
808
+ "DO",
809
+ "EC",
810
+ "SV",
811
+ "NI",
812
+ "HN"
813
+ ];
814
+ metadata = {
815
+ supportedMethods: _DLocalAdapter.supportedMethods,
816
+ supportedCurrencies: _DLocalAdapter.supportedCurrencies,
817
+ supportedCountries: _DLocalAdapter.supportedCountries
818
+ };
819
+ config;
820
+ constructor(rawConfig) {
821
+ const xLogin = typeof rawConfig.xLogin === "string" ? rawConfig.xLogin.trim() : "";
822
+ const xTransKey = typeof rawConfig.xTransKey === "string" ? rawConfig.xTransKey.trim() : "";
823
+ const secretKey = typeof rawConfig.secretKey === "string" ? rawConfig.secretKey.trim() : "";
824
+ if (!xLogin || !xTransKey || !secretKey) {
825
+ throw new VaultConfigError(
826
+ "dLocal adapter requires config.xLogin, config.xTransKey, and config.secretKey.",
827
+ {
828
+ code: "INVALID_CONFIGURATION",
829
+ context: {
830
+ provider: "dlocal"
831
+ }
832
+ }
833
+ );
834
+ }
835
+ const baseUrl = typeof rawConfig.baseUrl === "string" && rawConfig.baseUrl.trim() ? rawConfig.baseUrl.trim() : DEFAULT_DLOCAL_BASE_URL;
836
+ const timeoutMs = typeof rawConfig.timeoutMs === "number" && Number.isFinite(rawConfig.timeoutMs) && rawConfig.timeoutMs > 0 ? Math.floor(rawConfig.timeoutMs) : DEFAULT_TIMEOUT_MS;
837
+ const customFetch = rawConfig.fetchFn;
838
+ const fetchFn = typeof customFetch === "function" ? customFetch : fetch;
839
+ this.config = {
840
+ xLogin,
841
+ xTransKey,
842
+ secretKey,
843
+ baseUrl,
844
+ timeoutMs,
845
+ fetchFn,
846
+ webhookSecret: typeof rawConfig.webhookSecret === "string" ? rawConfig.webhookSecret : void 0
847
+ };
848
+ }
849
+ async charge(request) {
850
+ return this.createPayment(request, false);
851
+ }
852
+ async authorize(request) {
853
+ return this.createPayment(request, true);
854
+ }
855
+ async capture(request) {
856
+ const payment = await this.request({
857
+ operation: "capture",
858
+ path: `/v1/payments/${request.transactionId}/capture`,
859
+ method: "POST",
860
+ body: request.amount !== void 0 ? {
861
+ amount: request.amount
862
+ } : void 0
863
+ });
864
+ return this.normalizePaymentResult(
865
+ payment,
866
+ void 0,
867
+ request.transactionId
868
+ );
869
+ }
870
+ async refund(request) {
871
+ const refund = await this.request({
872
+ operation: "refund",
873
+ path: `/v1/payments/${request.transactionId}/refund`,
874
+ method: "POST",
875
+ body: {
876
+ amount: request.amount,
877
+ reason: request.reason
878
+ }
879
+ });
880
+ return {
881
+ id: refund.refund_id ?? refund.id ?? `refund_${Date.now()}`,
882
+ transactionId: refund.payment_id ?? request.transactionId,
883
+ status: mapRefundStatus(refund.status),
884
+ amount: refund.amount ?? request.amount ?? 0,
885
+ currency: (refund.currency ?? "USD").toUpperCase(),
886
+ provider: this.name,
887
+ providerId: refund.id ?? refund.refund_id ?? request.transactionId,
888
+ reason: refund.reason ?? request.reason,
889
+ createdAt: timestampOrNow(refund.created_date)
890
+ };
891
+ }
892
+ async void(request) {
893
+ const payment = await this.request({
894
+ operation: "void",
895
+ path: `/v1/payments/${request.transactionId}/cancel`,
896
+ method: "POST",
897
+ body: {}
898
+ });
899
+ return {
900
+ id: `void_${payment.payment_id ?? payment.id ?? request.transactionId}`,
901
+ transactionId: request.transactionId,
902
+ status: mapDLocalStatus(payment.status) === "cancelled" ? "completed" : "failed",
903
+ provider: this.name,
904
+ createdAt: timestampOrNow(payment.created_date)
905
+ };
906
+ }
907
+ async getStatus(transactionId) {
908
+ const payment = await this.request({
909
+ operation: "getStatus",
910
+ path: `/v1/payments/${transactionId}`,
911
+ method: "GET"
912
+ });
913
+ const status = mapDLocalStatus(payment.status);
914
+ const timestamp = timestampOrNow(payment.created_date);
915
+ return {
916
+ id: payment.payment_id ?? payment.id ?? transactionId,
917
+ status,
918
+ provider: this.name,
919
+ providerId: payment.id ?? payment.payment_id ?? transactionId,
920
+ amount: payment.amount ?? 0,
921
+ currency: (payment.currency ?? "USD").toUpperCase(),
922
+ history: [
923
+ {
924
+ status,
925
+ timestamp,
926
+ reason: `dlocal status: ${payment.status ?? "unknown"}`
927
+ }
928
+ ],
929
+ updatedAt: timestamp
930
+ };
931
+ }
932
+ async listPaymentMethods(country, currency) {
933
+ const normalizedCurrency = currency.toUpperCase();
934
+ return [
935
+ {
936
+ type: "card",
937
+ provider: this.name,
938
+ name: "dLocal Card",
939
+ countries: [country],
940
+ currencies: [normalizedCurrency]
941
+ },
942
+ {
943
+ type: "pix",
944
+ provider: this.name,
945
+ name: "dLocal PIX",
946
+ countries: ["BR"],
947
+ currencies: ["BRL"]
948
+ },
949
+ {
950
+ type: "boleto",
951
+ provider: this.name,
952
+ name: "dLocal Boleto",
953
+ countries: ["BR"],
954
+ currencies: ["BRL"]
955
+ }
956
+ ];
957
+ }
958
+ async handleWebhook(payload, headers) {
959
+ const rawPayload = toRawString(payload);
960
+ this.verifyWebhook(rawPayload, headers);
961
+ let parsed;
962
+ try {
963
+ parsed = JSON.parse(rawPayload);
964
+ } catch {
965
+ throw new WebhookVerificationError(
966
+ "dLocal webhook payload is not valid JSON.",
967
+ {
968
+ context: {
969
+ provider: this.name
970
+ }
971
+ }
972
+ );
973
+ }
974
+ const providerEventId = parsed.id ?? `evt_${Date.now()}`;
975
+ return normalizeWebhookEvent(
976
+ this.name,
977
+ {
978
+ id: providerEventId,
979
+ providerEventId,
980
+ type: mapDLocalEventType(parsed.type ?? parsed.event),
981
+ transactionId: parsed.payment_id ?? parsed.transaction_id ?? readString3(asRecord3(parsed.data), "payment_id"),
982
+ data: parsed.data ?? {},
983
+ timestamp: timestampOrNow(parsed.timestamp ?? parsed.created_date)
984
+ },
985
+ parsed
986
+ );
987
+ }
988
+ verifyWebhook(rawPayload, headers) {
989
+ const secret = this.config.webhookSecret ?? this.config.secretKey;
990
+ const receivedSignature = readHeader(headers, "x-dlocal-signature") ?? readHeader(headers, "x-signature");
991
+ if (!receivedSignature) {
992
+ throw new WebhookVerificationError("Missing dLocal signature header.", {
993
+ context: {
994
+ provider: this.name
995
+ }
996
+ });
997
+ }
998
+ const computedSignature = createHmacDigest("sha256", secret, rawPayload);
999
+ if (!secureCompareHex(receivedSignature, computedSignature)) {
1000
+ throw new WebhookVerificationError(
1001
+ "dLocal webhook signature verification failed.",
1002
+ {
1003
+ context: {
1004
+ provider: this.name
1005
+ }
1006
+ }
1007
+ );
1008
+ }
1009
+ }
1010
+ async createPayment(request, authorizeOnly) {
1011
+ const body = {
1012
+ amount: request.amount,
1013
+ currency: request.currency.toUpperCase(),
1014
+ capture: !authorizeOnly,
1015
+ description: request.description,
1016
+ metadata: request.metadata,
1017
+ country: request.customer?.address?.country,
1018
+ payer: {
1019
+ name: request.customer?.name,
1020
+ email: request.customer?.email,
1021
+ document: request.customer?.document
1022
+ },
1023
+ ...mapPaymentMethod(request.paymentMethod)
1024
+ };
1025
+ const payment = await this.request({
1026
+ operation: authorizeOnly ? "authorize" : "charge",
1027
+ path: "/v1/payments",
1028
+ method: "POST",
1029
+ body
1030
+ });
1031
+ return this.normalizePaymentResult(payment, request);
1032
+ }
1033
+ normalizePaymentResult(payment, request, fallbackId) {
1034
+ const transactionId = payment.payment_id ?? payment.id ?? fallbackId;
1035
+ const status = mapDLocalStatus(payment.status);
1036
+ return {
1037
+ id: transactionId ?? `payment_${Date.now()}`,
1038
+ status,
1039
+ provider: this.name,
1040
+ providerId: payment.id ?? transactionId ?? `provider_${Date.now()}`,
1041
+ amount: payment.amount ?? request?.amount ?? 0,
1042
+ currency: (payment.currency ?? request?.currency ?? "USD").toUpperCase(),
1043
+ paymentMethod: {
1044
+ type: payment.payment_method_id?.toLowerCase() ?? request?.paymentMethod.type ?? "card",
1045
+ last4: payment.card?.last4
1046
+ },
1047
+ customer: request?.customer?.email ? {
1048
+ email: request.customer.email
1049
+ } : void 0,
1050
+ metadata: request?.metadata ?? {},
1051
+ routing: {
1052
+ source: "local",
1053
+ reason: "dlocal adapter request"
1054
+ },
1055
+ createdAt: timestampOrNow(payment.created_date),
1056
+ providerMetadata: {
1057
+ dlocalStatus: payment.status,
1058
+ orderId: payment.order_id
1059
+ }
1060
+ };
1061
+ }
1062
+ buildHeaders(serializedBody, timestamp) {
1063
+ const authPayload = `${this.config.xLogin}${timestamp}${serializedBody}`;
1064
+ const signature = createHmacDigest(
1065
+ "sha256",
1066
+ this.config.secretKey,
1067
+ authPayload
1068
+ );
1069
+ return {
1070
+ "x-login": this.config.xLogin,
1071
+ "x-trans-key": this.config.xTransKey,
1072
+ "x-version": DLOCAL_API_VERSION,
1073
+ "x-date": timestamp,
1074
+ authorization: `V2-HMAC-SHA256, Signature: ${signature}`,
1075
+ "content-type": "application/json"
1076
+ };
1077
+ }
1078
+ async request(params) {
1079
+ const serializedBody = params.body ? JSON.stringify(params.body) : "";
1080
+ return requestJson({
1081
+ provider: this.name,
1082
+ fetchFn: this.config.fetchFn,
1083
+ baseUrl: this.config.baseUrl,
1084
+ path: params.path,
1085
+ method: params.method,
1086
+ timeoutMs: this.config.timeoutMs,
1087
+ headers: this.buildHeaders(serializedBody, (/* @__PURE__ */ new Date()).toISOString()),
1088
+ body: params.body
1089
+ }).catch((error) => {
1090
+ const record = asRecord3(error);
1091
+ const hint = asRecord3(record?.hint);
1092
+ const raw = asRecord3(hint?.raw);
1093
+ throw {
1094
+ ...record,
1095
+ hint: {
1096
+ ...hint,
1097
+ providerCode: readString3(hint, "providerCode") ?? readString3(raw, "code") ?? readString3(raw, "error_code"),
1098
+ providerMessage: readString3(hint, "providerMessage") ?? readString3(raw, "message") ?? readString3(record, "message") ?? "dLocal request failed.",
1099
+ httpStatus: readNumber3(hint, "httpStatus") ?? readNumber3(record, "status"),
1100
+ raw: error
1101
+ },
1102
+ operation: params.operation
1103
+ };
1104
+ });
1105
+ }
1106
+ };
1107
+
1108
+ // src/adapters/paystack-adapter.ts
1109
+ var DEFAULT_PAYSTACK_BASE_URL = "https://api.paystack.co";
1110
+ var DEFAULT_TIMEOUT_MS2 = 15e3;
1111
+ function asRecord4(value) {
1112
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1113
+ return null;
1114
+ }
1115
+ return value;
1116
+ }
1117
+ function readString4(source, key) {
1118
+ if (!source) {
1119
+ return void 0;
1120
+ }
1121
+ const value = source[key];
1122
+ return typeof value === "string" && value.trim() ? value : void 0;
1123
+ }
1124
+ function readNumber4(source, key) {
1125
+ if (!source) {
1126
+ return void 0;
1127
+ }
1128
+ const value = source[key];
1129
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1130
+ }
1131
+ function mapPaystackPaymentStatus(status) {
1132
+ switch (status?.toLowerCase()) {
1133
+ case "success":
1134
+ return "completed";
1135
+ case "pending":
1136
+ case "ongoing":
1137
+ case "queued":
1138
+ return "pending";
1139
+ case "abandoned":
1140
+ return "cancelled";
1141
+ case "failed":
1142
+ case "reversed":
1143
+ return "failed";
1144
+ default:
1145
+ return "failed";
1146
+ }
1147
+ }
1148
+ function mapPaystackRefundStatus(status) {
1149
+ switch (status?.toLowerCase()) {
1150
+ case "processed":
1151
+ case "success":
1152
+ return "completed";
1153
+ case "pending":
1154
+ return "pending";
1155
+ default:
1156
+ return "failed";
1157
+ }
1158
+ }
1159
+ function mapPaystackEventType(event) {
1160
+ switch (event) {
1161
+ case "charge.success":
1162
+ return "payment.completed";
1163
+ case "charge.failed":
1164
+ return "payment.failed";
1165
+ case "charge.pending":
1166
+ return "payment.pending";
1167
+ case "refund.processed":
1168
+ case "refund.success":
1169
+ return "payment.refunded";
1170
+ case "refund.pending":
1171
+ return "payment.partially_refunded";
1172
+ case "dispute.create":
1173
+ case "charge.dispute.create":
1174
+ return "payment.disputed";
1175
+ case "dispute.resolve":
1176
+ case "charge.dispute.resolve":
1177
+ return "payment.dispute_resolved";
1178
+ case "transfer.success":
1179
+ return "payout.completed";
1180
+ case "transfer.failed":
1181
+ return "payout.failed";
1182
+ default:
1183
+ return "payment.failed";
1184
+ }
1185
+ }
1186
+ function mapPaymentMethod2(paymentMethod) {
1187
+ if (paymentMethod.type === "card" && "token" in paymentMethod) {
1188
+ return {
1189
+ authorization_code: paymentMethod.token
1190
+ };
1191
+ }
1192
+ if (paymentMethod.type === "card") {
1193
+ return {
1194
+ card: {
1195
+ number: paymentMethod.number,
1196
+ cvv: paymentMethod.cvc,
1197
+ expiry_month: String(paymentMethod.expMonth).padStart(2, "0"),
1198
+ expiry_year: String(paymentMethod.expYear)
1199
+ }
1200
+ };
1201
+ }
1202
+ if (paymentMethod.type === "bank_transfer") {
1203
+ return {
1204
+ bank: {
1205
+ code: paymentMethod.bankCode,
1206
+ account_number: paymentMethod.accountNumber
1207
+ }
1208
+ };
1209
+ }
1210
+ if (paymentMethod.type === "wallet") {
1211
+ return {
1212
+ mobile_money: {
1213
+ provider: paymentMethod.walletType,
1214
+ token: paymentMethod.token
1215
+ }
1216
+ };
1217
+ }
1218
+ return {
1219
+ channel: paymentMethod.type
1220
+ };
1221
+ }
1222
+ function timestampOrNow2(input) {
1223
+ if (!input) {
1224
+ return (/* @__PURE__ */ new Date()).toISOString();
1225
+ }
1226
+ const date = new Date(input);
1227
+ return Number.isNaN(date.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : date.toISOString();
1228
+ }
1229
+ var PaystackAdapter = class _PaystackAdapter {
1230
+ name = "paystack";
1231
+ static supportedMethods = [
1232
+ "card",
1233
+ "bank_transfer",
1234
+ "wallet"
1235
+ ];
1236
+ static supportedCurrencies = [
1237
+ "NGN",
1238
+ "GHS",
1239
+ "ZAR",
1240
+ "KES",
1241
+ "USD"
1242
+ ];
1243
+ static supportedCountries = ["NG", "GH", "ZA", "KE"];
1244
+ metadata = {
1245
+ supportedMethods: _PaystackAdapter.supportedMethods,
1246
+ supportedCurrencies: _PaystackAdapter.supportedCurrencies,
1247
+ supportedCountries: _PaystackAdapter.supportedCountries
1248
+ };
1249
+ config;
1250
+ constructor(rawConfig) {
1251
+ const secretKey = typeof rawConfig.secretKey === "string" ? rawConfig.secretKey.trim() : "";
1252
+ if (!secretKey) {
1253
+ throw new VaultConfigError(
1254
+ "Paystack adapter requires config.secretKey.",
1255
+ {
1256
+ code: "INVALID_CONFIGURATION",
1257
+ context: {
1258
+ provider: "paystack"
1259
+ }
1260
+ }
1261
+ );
1262
+ }
1263
+ const baseUrl = typeof rawConfig.baseUrl === "string" && rawConfig.baseUrl.trim() ? rawConfig.baseUrl.trim() : DEFAULT_PAYSTACK_BASE_URL;
1264
+ const timeoutMs = typeof rawConfig.timeoutMs === "number" && Number.isFinite(rawConfig.timeoutMs) && rawConfig.timeoutMs > 0 ? Math.floor(rawConfig.timeoutMs) : DEFAULT_TIMEOUT_MS2;
1265
+ const customFetch = rawConfig.fetchFn;
1266
+ const fetchFn = typeof customFetch === "function" ? customFetch : fetch;
1267
+ this.config = {
1268
+ secretKey,
1269
+ baseUrl,
1270
+ timeoutMs,
1271
+ fetchFn,
1272
+ webhookSecret: typeof rawConfig.webhookSecret === "string" ? rawConfig.webhookSecret : void 0
1273
+ };
1274
+ }
1275
+ async charge(request) {
1276
+ const payload = this.buildChargePayload(request, false);
1277
+ const transaction = await this.request({
1278
+ operation: "charge",
1279
+ path: "/charge",
1280
+ method: "POST",
1281
+ body: payload
1282
+ });
1283
+ return this.normalizePaymentResult(transaction, request);
1284
+ }
1285
+ async authorize(request) {
1286
+ const payload = this.buildChargePayload(request, true);
1287
+ const transaction = await this.request({
1288
+ operation: "authorize",
1289
+ path: "/charge",
1290
+ method: "POST",
1291
+ body: payload
1292
+ });
1293
+ return this.normalizePaymentResult(transaction, request);
1294
+ }
1295
+ async capture(request) {
1296
+ const current = await this.request({
1297
+ operation: "capture.verify",
1298
+ path: `/transaction/verify/${request.transactionId}`,
1299
+ method: "GET"
1300
+ });
1301
+ const authorizationCode = current.authorization?.authorization_code;
1302
+ const email = current.customer?.email;
1303
+ if (!authorizationCode || !email) {
1304
+ throw new VaultProviderError(
1305
+ "Paystack capture requires an authorization code and customer email.",
1306
+ {
1307
+ code: "INVALID_REQUEST",
1308
+ context: {
1309
+ provider: this.name,
1310
+ operation: "capture"
1311
+ }
1312
+ }
1313
+ );
1314
+ }
1315
+ const charged = await this.request({
1316
+ operation: "capture",
1317
+ path: "/transaction/charge_authorization",
1318
+ method: "POST",
1319
+ body: {
1320
+ authorization_code: authorizationCode,
1321
+ email,
1322
+ amount: request.amount ?? current.amount,
1323
+ currency: current.currency
1324
+ }
1325
+ });
1326
+ return this.normalizePaymentResult(charged);
1327
+ }
1328
+ async refund(request) {
1329
+ const refund = await this.request({
1330
+ operation: "refund",
1331
+ path: "/refund",
1332
+ method: "POST",
1333
+ body: {
1334
+ transaction: request.transactionId,
1335
+ amount: request.amount
1336
+ }
1337
+ });
1338
+ return {
1339
+ id: String(refund.id ?? `refund_${Date.now()}`),
1340
+ transactionId: String(refund.transaction ?? request.transactionId),
1341
+ status: mapPaystackRefundStatus(refund.status),
1342
+ amount: refund.amount ?? request.amount ?? 0,
1343
+ currency: (refund.currency ?? "NGN").toUpperCase(),
1344
+ provider: this.name,
1345
+ providerId: String(refund.id ?? request.transactionId),
1346
+ reason: refund.reason ?? request.reason,
1347
+ createdAt: timestampOrNow2(refund.created_at)
1348
+ };
1349
+ }
1350
+ async void(request) {
1351
+ const refund = await this.refund({
1352
+ transactionId: request.transactionId,
1353
+ reason: "void"
1354
+ });
1355
+ return {
1356
+ id: `void_${refund.id}`,
1357
+ transactionId: request.transactionId,
1358
+ status: refund.status === "completed" ? "completed" : "failed",
1359
+ provider: this.name,
1360
+ createdAt: refund.createdAt
1361
+ };
1362
+ }
1363
+ async getStatus(transactionId) {
1364
+ const transaction = await this.request({
1365
+ operation: "getStatus",
1366
+ path: `/transaction/verify/${transactionId}`,
1367
+ method: "GET"
1368
+ });
1369
+ const status = mapPaystackPaymentStatus(transaction.status);
1370
+ const timestamp = timestampOrNow2(
1371
+ transaction.paid_at ?? transaction.created_at
1372
+ );
1373
+ return {
1374
+ id: transaction.reference ?? transactionId,
1375
+ status,
1376
+ provider: this.name,
1377
+ providerId: String(
1378
+ transaction.id ?? transaction.reference ?? transactionId
1379
+ ),
1380
+ amount: transaction.amount ?? 0,
1381
+ currency: (transaction.currency ?? "NGN").toUpperCase(),
1382
+ history: [
1383
+ {
1384
+ status,
1385
+ timestamp,
1386
+ reason: transaction.gateway_response
1387
+ }
1388
+ ],
1389
+ updatedAt: timestamp
1390
+ };
1391
+ }
1392
+ async listPaymentMethods(country, currency) {
1393
+ return [
1394
+ {
1395
+ type: "card",
1396
+ provider: this.name,
1397
+ name: "Paystack Card",
1398
+ countries: [country],
1399
+ currencies: [currency.toUpperCase()]
1400
+ },
1401
+ {
1402
+ type: "bank_transfer",
1403
+ provider: this.name,
1404
+ name: "Paystack Bank Transfer",
1405
+ countries: ["NG", "GH", "ZA", "KE"],
1406
+ currencies: ["NGN", "GHS", "ZAR", "KES"]
1407
+ }
1408
+ ];
1409
+ }
1410
+ async handleWebhook(payload, headers) {
1411
+ const rawPayload = toRawString(payload);
1412
+ this.verifyWebhook(rawPayload, headers);
1413
+ let parsed;
1414
+ try {
1415
+ parsed = JSON.parse(rawPayload);
1416
+ } catch {
1417
+ throw new WebhookVerificationError(
1418
+ "Paystack webhook payload is not valid JSON.",
1419
+ {
1420
+ context: {
1421
+ provider: this.name
1422
+ }
1423
+ }
1424
+ );
1425
+ }
1426
+ const data = asRecord4(parsed.data);
1427
+ const providerEventId = readString4(data, "id") ?? readString4(data, "reference") ?? `evt_${Date.now()}`;
1428
+ return normalizeWebhookEvent(
1429
+ this.name,
1430
+ {
1431
+ id: providerEventId,
1432
+ providerEventId,
1433
+ type: mapPaystackEventType(parsed.event),
1434
+ transactionId: readString4(data, "reference") ?? (typeof readNumber4(data, "id") === "number" ? String(readNumber4(data, "id")) : void 0),
1435
+ data: data ?? {},
1436
+ timestamp: timestampOrNow2(readString4(data, "created_at"))
1437
+ },
1438
+ parsed
1439
+ );
1440
+ }
1441
+ verifyWebhook(rawPayload, headers) {
1442
+ const signature = readHeader(headers, "x-paystack-signature");
1443
+ if (!signature) {
1444
+ throw new WebhookVerificationError("Missing Paystack signature header.", {
1445
+ context: {
1446
+ provider: this.name
1447
+ }
1448
+ });
1449
+ }
1450
+ const secret = this.config.webhookSecret ?? this.config.secretKey;
1451
+ const computed = createHmacDigest("sha512", secret, rawPayload);
1452
+ if (!secureCompareHex(signature, computed)) {
1453
+ throw new WebhookVerificationError(
1454
+ "Paystack webhook signature verification failed.",
1455
+ {
1456
+ context: {
1457
+ provider: this.name
1458
+ }
1459
+ }
1460
+ );
1461
+ }
1462
+ }
1463
+ buildChargePayload(request, authorizeOnly) {
1464
+ const email = request.customer?.email;
1465
+ if (!email) {
1466
+ throw new VaultProviderError(
1467
+ "Paystack charge requires customer.email in the request.",
1468
+ {
1469
+ code: "INVALID_REQUEST",
1470
+ context: {
1471
+ provider: this.name,
1472
+ operation: authorizeOnly ? "authorize" : "charge"
1473
+ }
1474
+ }
1475
+ );
1476
+ }
1477
+ return {
1478
+ email,
1479
+ amount: request.amount,
1480
+ currency: request.currency.toUpperCase(),
1481
+ metadata: {
1482
+ ...request.metadata ?? {},
1483
+ vaultsaas_intent: authorizeOnly ? "authorize" : "charge"
1484
+ },
1485
+ ...mapPaymentMethod2(request.paymentMethod)
1486
+ };
1487
+ }
1488
+ normalizePaymentResult(transaction, request) {
1489
+ const id = transaction.reference ?? String(transaction.id ?? `txn_${Date.now()}`);
1490
+ return {
1491
+ id,
1492
+ status: mapPaystackPaymentStatus(transaction.status),
1493
+ provider: this.name,
1494
+ providerId: String(transaction.id ?? id),
1495
+ amount: transaction.amount ?? request?.amount ?? 0,
1496
+ currency: (transaction.currency ?? request?.currency ?? "NGN").toUpperCase(),
1497
+ paymentMethod: {
1498
+ type: request?.paymentMethod.type ?? "card",
1499
+ last4: transaction.authorization?.last4,
1500
+ brand: transaction.authorization?.brand,
1501
+ expiryMonth: transaction.authorization?.exp_month ? Number(transaction.authorization.exp_month) : void 0,
1502
+ expiryYear: transaction.authorization?.exp_year ? Number(transaction.authorization.exp_year) : void 0
1503
+ },
1504
+ customer: transaction.customer?.email || request?.customer?.email ? {
1505
+ email: transaction.customer?.email ?? request?.customer?.email
1506
+ } : void 0,
1507
+ metadata: transaction.metadata ?? request?.metadata ?? {},
1508
+ routing: {
1509
+ source: "local",
1510
+ reason: "paystack adapter request"
1511
+ },
1512
+ createdAt: timestampOrNow2(transaction.paid_at ?? transaction.created_at),
1513
+ providerMetadata: {
1514
+ paystackStatus: transaction.status,
1515
+ gatewayResponse: transaction.gateway_response
1516
+ }
1517
+ };
1518
+ }
1519
+ async request(params) {
1520
+ const envelope = await requestJson({
1521
+ provider: this.name,
1522
+ fetchFn: this.config.fetchFn,
1523
+ baseUrl: this.config.baseUrl,
1524
+ path: params.path,
1525
+ method: params.method,
1526
+ timeoutMs: this.config.timeoutMs,
1527
+ headers: {
1528
+ Authorization: `Bearer ${this.config.secretKey}`
1529
+ },
1530
+ body: params.body
1531
+ }).catch((error) => {
1532
+ const record = asRecord4(error);
1533
+ const hint = asRecord4(record?.hint);
1534
+ const raw = asRecord4(hint?.raw);
1535
+ throw {
1536
+ ...record,
1537
+ hint: {
1538
+ ...hint,
1539
+ providerCode: readString4(hint, "providerCode") ?? readString4(raw, "code"),
1540
+ providerMessage: readString4(hint, "providerMessage") ?? readString4(raw, "message") ?? readString4(record, "message") ?? "Paystack request failed.",
1541
+ httpStatus: readNumber4(hint, "httpStatus") ?? readNumber4(record, "status"),
1542
+ raw: error
1543
+ },
1544
+ operation: params.operation
1545
+ };
1546
+ });
1547
+ if (!envelope.status) {
1548
+ throw {
1549
+ message: envelope.message || "Paystack rejected the request.",
1550
+ hint: {
1551
+ providerMessage: envelope.message,
1552
+ providerCode: "paystack_error",
1553
+ raw: envelope
1554
+ },
1555
+ operation: params.operation
1556
+ };
1557
+ }
1558
+ return envelope.data;
1559
+ }
1560
+ };
1561
+
1562
+ // src/adapters/stripe-adapter.ts
1563
+ var DEFAULT_STRIPE_BASE_URL = "https://api.stripe.com";
1564
+ var DEFAULT_TIMEOUT_MS3 = 15e3;
1565
+ function asRecord5(value) {
1566
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1567
+ return null;
1568
+ }
1569
+ return value;
1570
+ }
1571
+ function readString5(source, key) {
1572
+ if (!source) {
1573
+ return void 0;
1574
+ }
1575
+ const value = source[key];
1576
+ return typeof value === "string" && value.trim() ? value : void 0;
1577
+ }
1578
+ function toIsoTimestamp(unixSeconds) {
1579
+ if (!unixSeconds || !Number.isFinite(unixSeconds)) {
1580
+ return (/* @__PURE__ */ new Date()).toISOString();
1581
+ }
1582
+ return new Date(unixSeconds * 1e3).toISOString();
1583
+ }
1584
+ function mapStripeStatus(status) {
1585
+ switch (status) {
1586
+ case "succeeded":
1587
+ return "completed";
1588
+ case "requires_capture":
1589
+ return "authorized";
1590
+ case "requires_action":
1591
+ return "requires_action";
1592
+ case "processing":
1593
+ return "pending";
1594
+ case "canceled":
1595
+ return "cancelled";
1596
+ case "requires_payment_method":
1597
+ return "declined";
1598
+ case "requires_confirmation":
1599
+ return "pending";
1600
+ default:
1601
+ return "failed";
1602
+ }
1603
+ }
1604
+ function mapRefundStatus2(status) {
1605
+ switch (status) {
1606
+ case "succeeded":
1607
+ return "completed";
1608
+ case "pending":
1609
+ return "pending";
1610
+ default:
1611
+ return "failed";
1612
+ }
1613
+ }
1614
+ function buildStripePaymentMethodData(paymentMethod) {
1615
+ if (paymentMethod.type === "card" && "token" in paymentMethod) {
1616
+ return {
1617
+ payment_method: paymentMethod.token,
1618
+ payment_method_types: ["card"]
1619
+ };
1620
+ }
1621
+ if (paymentMethod.type === "card") {
1622
+ return {
1623
+ payment_method_data: {
1624
+ type: "card",
1625
+ card: {
1626
+ number: paymentMethod.number,
1627
+ exp_month: paymentMethod.expMonth,
1628
+ exp_year: paymentMethod.expYear,
1629
+ cvc: paymentMethod.cvc
1630
+ }
1631
+ },
1632
+ payment_method_types: ["card"]
1633
+ };
1634
+ }
1635
+ if (paymentMethod.type === "wallet") {
1636
+ return {
1637
+ payment_method_types: [paymentMethod.walletType],
1638
+ payment_method_data: {
1639
+ type: paymentMethod.walletType,
1640
+ wallet: {
1641
+ token: paymentMethod.token
1642
+ }
1643
+ }
1644
+ };
1645
+ }
1646
+ if (paymentMethod.type === "bank_transfer") {
1647
+ return {
1648
+ payment_method_types: ["customer_balance"],
1649
+ payment_method_data: {
1650
+ type: "customer_balance",
1651
+ customer_balance: {
1652
+ funding_type: "bank_transfer"
1653
+ }
1654
+ }
1655
+ };
1656
+ }
1657
+ return {
1658
+ payment_method_types: [paymentMethod.type]
1659
+ };
1660
+ }
1661
+ function extractPaymentMethodSnapshot(request, intent) {
1662
+ if (request?.paymentMethod.type === "card" && "number" in request.paymentMethod) {
1663
+ return {
1664
+ type: "card",
1665
+ last4: request.paymentMethod.number.slice(-4),
1666
+ expiryMonth: request.paymentMethod.expMonth,
1667
+ expiryYear: request.paymentMethod.expYear
1668
+ };
1669
+ }
1670
+ if (request?.paymentMethod.type === "card" && "token" in request.paymentMethod) {
1671
+ return {
1672
+ type: "card"
1673
+ };
1674
+ }
1675
+ if (request) {
1676
+ return {
1677
+ type: request.paymentMethod.type
1678
+ };
1679
+ }
1680
+ return {
1681
+ type: intent.payment_method_types?.[0] ?? "card"
1682
+ };
1683
+ }
1684
+ function mapStripeEventType(type) {
1685
+ switch (type) {
1686
+ case "payment_intent.succeeded":
1687
+ return "payment.completed";
1688
+ case "payment_intent.payment_failed":
1689
+ return "payment.failed";
1690
+ case "payment_intent.processing":
1691
+ return "payment.pending";
1692
+ case "payment_intent.requires_action":
1693
+ return "payment.requires_action";
1694
+ case "charge.refunded":
1695
+ return "payment.refunded";
1696
+ case "charge.dispute.created":
1697
+ return "payment.disputed";
1698
+ case "charge.dispute.closed":
1699
+ return "payment.dispute_resolved";
1700
+ case "payout.paid":
1701
+ return "payout.completed";
1702
+ case "payout.failed":
1703
+ return "payout.failed";
1704
+ default:
1705
+ return "payment.failed";
1706
+ }
1707
+ }
1708
+ function extractStripeTransactionId(webhook) {
1709
+ const object = asRecord5(webhook.data?.object);
1710
+ return readString5(object, "payment_intent") ?? readString5(object, "id") ?? readString5(object, "charge");
1711
+ }
1712
+ var StripeAdapter = class _StripeAdapter {
1713
+ name = "stripe";
1714
+ static supportedMethods = [
1715
+ "card",
1716
+ "bank_transfer",
1717
+ "wallet"
1718
+ ];
1719
+ static supportedCurrencies = [
1720
+ "USD",
1721
+ "EUR",
1722
+ "GBP",
1723
+ "CAD",
1724
+ "AUD",
1725
+ "JPY",
1726
+ "CHF",
1727
+ "SEK",
1728
+ "NOK",
1729
+ "DKK",
1730
+ "NZD",
1731
+ "SGD",
1732
+ "HKD",
1733
+ "MXN",
1734
+ "BRL",
1735
+ "PLN",
1736
+ "CZK",
1737
+ "HUF",
1738
+ "RON",
1739
+ "BGN",
1740
+ "INR",
1741
+ "MYR",
1742
+ "THB"
1743
+ ];
1744
+ static supportedCountries = [
1745
+ "US",
1746
+ "GB",
1747
+ "DE",
1748
+ "FR",
1749
+ "CA",
1750
+ "AU",
1751
+ "JP",
1752
+ "IT",
1753
+ "ES",
1754
+ "NL",
1755
+ "BE",
1756
+ "AT",
1757
+ "CH",
1758
+ "SE",
1759
+ "NO",
1760
+ "DK",
1761
+ "FI",
1762
+ "IE",
1763
+ "PT",
1764
+ "LU",
1765
+ "NZ",
1766
+ "SG",
1767
+ "HK",
1768
+ "MY",
1769
+ "MX",
1770
+ "BR",
1771
+ "PL",
1772
+ "CZ",
1773
+ "HU",
1774
+ "RO",
1775
+ "BG",
1776
+ "HR",
1777
+ "CY",
1778
+ "EE",
1779
+ "GR",
1780
+ "LV",
1781
+ "LT",
1782
+ "MT",
1783
+ "SK",
1784
+ "SI",
1785
+ "IN",
1786
+ "TH"
1787
+ ];
1788
+ metadata = {
1789
+ supportedMethods: _StripeAdapter.supportedMethods,
1790
+ supportedCurrencies: _StripeAdapter.supportedCurrencies,
1791
+ supportedCountries: _StripeAdapter.supportedCountries
1792
+ };
1793
+ config;
1794
+ constructor(rawConfig) {
1795
+ const apiKey = typeof rawConfig.apiKey === "string" ? rawConfig.apiKey.trim() : "";
1796
+ if (!apiKey) {
1797
+ throw new VaultConfigError("Stripe adapter requires config.apiKey.", {
1798
+ code: "INVALID_CONFIGURATION",
1799
+ context: {
1800
+ provider: "stripe"
1801
+ }
1802
+ });
1803
+ }
1804
+ const baseUrl = typeof rawConfig.baseUrl === "string" && rawConfig.baseUrl.trim() ? rawConfig.baseUrl.trim() : DEFAULT_STRIPE_BASE_URL;
1805
+ const timeoutMs = typeof rawConfig.timeoutMs === "number" && Number.isFinite(rawConfig.timeoutMs) && rawConfig.timeoutMs > 0 ? Math.floor(rawConfig.timeoutMs) : DEFAULT_TIMEOUT_MS3;
1806
+ const customFetch = rawConfig.fetchFn;
1807
+ const fetchFn = typeof customFetch === "function" ? customFetch : fetch;
1808
+ this.config = {
1809
+ apiKey,
1810
+ baseUrl,
1811
+ timeoutMs,
1812
+ fetchFn,
1813
+ webhookSecret: typeof rawConfig.webhookSecret === "string" ? rawConfig.webhookSecret : void 0
1814
+ };
1815
+ }
1816
+ async charge(request) {
1817
+ return this.createPaymentIntent(request, "automatic");
1818
+ }
1819
+ async authorize(request) {
1820
+ return this.createPaymentIntent(request, "manual");
1821
+ }
1822
+ async capture(request) {
1823
+ const body = {};
1824
+ if (request.amount !== void 0) {
1825
+ body.amount_to_capture = request.amount;
1826
+ }
1827
+ const intent = await this.postForm(
1828
+ `/v1/payment_intents/${request.transactionId}/capture`,
1829
+ body,
1830
+ "capture"
1831
+ );
1832
+ return this.normalizePaymentResult(intent);
1833
+ }
1834
+ async refund(request) {
1835
+ const body = {
1836
+ payment_intent: request.transactionId
1837
+ };
1838
+ if (request.amount !== void 0) {
1839
+ body.amount = request.amount;
1840
+ }
1841
+ if (request.reason) {
1842
+ body.reason = request.reason;
1843
+ }
1844
+ const refund = await this.postForm(
1845
+ "/v1/refunds",
1846
+ body,
1847
+ "refund"
1848
+ );
1849
+ return {
1850
+ id: refund.id,
1851
+ transactionId: refund.payment_intent ?? request.transactionId,
1852
+ status: mapRefundStatus2(refund.status),
1853
+ amount: refund.amount,
1854
+ currency: refund.currency.toUpperCase(),
1855
+ provider: this.name,
1856
+ providerId: refund.charge ?? refund.id,
1857
+ reason: refund.reason,
1858
+ createdAt: toIsoTimestamp(refund.created)
1859
+ };
1860
+ }
1861
+ async void(request) {
1862
+ const intent = await this.postForm(
1863
+ `/v1/payment_intents/${request.transactionId}/cancel`,
1864
+ {},
1865
+ "void"
1866
+ );
1867
+ return {
1868
+ id: `void_${intent.id}`,
1869
+ transactionId: request.transactionId,
1870
+ status: intent.status === "canceled" ? "completed" : "failed",
1871
+ provider: this.name,
1872
+ createdAt: toIsoTimestamp(intent.created)
1873
+ };
1874
+ }
1875
+ async getStatus(transactionId) {
1876
+ const intent = await this.get(
1877
+ `/v1/payment_intents/${transactionId}`,
1878
+ "getStatus"
1879
+ );
1880
+ const status = mapStripeStatus(intent.status);
1881
+ const timestamp = toIsoTimestamp(intent.created);
1882
+ return {
1883
+ id: intent.id,
1884
+ status,
1885
+ provider: this.name,
1886
+ providerId: intent.latest_charge ?? intent.id,
1887
+ amount: intent.amount,
1888
+ currency: intent.currency.toUpperCase(),
1889
+ history: [
1890
+ {
1891
+ status,
1892
+ timestamp,
1893
+ reason: `stripe status: ${intent.status}`
1894
+ }
1895
+ ],
1896
+ updatedAt: timestamp
1897
+ };
1898
+ }
1899
+ async listPaymentMethods(country, currency) {
1900
+ return [
1901
+ {
1902
+ type: "card",
1903
+ provider: this.name,
1904
+ name: "Stripe Card",
1905
+ countries: [country],
1906
+ currencies: [currency.toUpperCase()]
1907
+ },
1908
+ {
1909
+ type: "wallet",
1910
+ provider: this.name,
1911
+ name: "Stripe Wallets",
1912
+ countries: [country],
1913
+ currencies: [currency.toUpperCase()]
1914
+ }
1915
+ ];
1916
+ }
1917
+ async handleWebhook(payload, headers) {
1918
+ const rawPayload = toRawString(payload);
1919
+ this.verifyWebhook(rawPayload, headers);
1920
+ let parsed;
1921
+ try {
1922
+ parsed = JSON.parse(rawPayload);
1923
+ } catch {
1924
+ throw new WebhookVerificationError(
1925
+ "Stripe webhook payload is not valid JSON.",
1926
+ {
1927
+ context: {
1928
+ provider: this.name
1929
+ }
1930
+ }
1931
+ );
1932
+ }
1933
+ const transactionId = extractStripeTransactionId(parsed);
1934
+ const providerEventId = parsed.id ?? `evt_${Date.now()}`;
1935
+ return normalizeWebhookEvent(
1936
+ this.name,
1937
+ {
1938
+ id: providerEventId,
1939
+ providerEventId,
1940
+ type: mapStripeEventType(parsed.type),
1941
+ transactionId,
1942
+ data: asRecord5(parsed.data?.object) ?? {},
1943
+ timestamp: toIsoTimestamp(parsed.created)
1944
+ },
1945
+ parsed
1946
+ );
1947
+ }
1948
+ verifyWebhook(rawPayload, headers) {
1949
+ if (!this.config.webhookSecret) {
1950
+ throw new WebhookVerificationError(
1951
+ "Stripe webhook secret is not configured.",
1952
+ {
1953
+ context: {
1954
+ provider: this.name
1955
+ }
1956
+ }
1957
+ );
1958
+ }
1959
+ const signature = readHeader(headers, "stripe-signature");
1960
+ if (!signature) {
1961
+ throw new WebhookVerificationError("Missing Stripe signature header.", {
1962
+ context: {
1963
+ provider: this.name
1964
+ }
1965
+ });
1966
+ }
1967
+ const components = signature.split(",").map((part) => part.trim());
1968
+ let timestamp;
1969
+ const signatures = [];
1970
+ for (const component of components) {
1971
+ const [key, value] = component.split("=");
1972
+ if (!key || !value) {
1973
+ continue;
1974
+ }
1975
+ if (key === "t") {
1976
+ timestamp = value;
1977
+ } else if (key === "v1") {
1978
+ signatures.push(value);
1979
+ }
1980
+ }
1981
+ if (!timestamp || signatures.length === 0) {
1982
+ throw new WebhookVerificationError(
1983
+ "Stripe signature header is malformed.",
1984
+ {
1985
+ context: {
1986
+ provider: this.name
1987
+ }
1988
+ }
1989
+ );
1990
+ }
1991
+ const timestampNum = Number(timestamp);
1992
+ if (!Number.isFinite(timestampNum)) {
1993
+ throw new WebhookVerificationError(
1994
+ "Stripe webhook timestamp is not a valid number.",
1995
+ {
1996
+ context: {
1997
+ provider: this.name
1998
+ }
1999
+ }
2000
+ );
2001
+ }
2002
+ const ageMs = Date.now() - timestampNum * 1e3;
2003
+ const toleranceMs = 5 * 60 * 1e3;
2004
+ if (ageMs > toleranceMs) {
2005
+ throw new WebhookVerificationError(
2006
+ "Stripe webhook timestamp is too old (exceeds 5-minute tolerance). Possible replay attack.",
2007
+ {
2008
+ context: {
2009
+ provider: this.name,
2010
+ timestampAge: `${Math.round(ageMs / 1e3)}s`
2011
+ }
2012
+ }
2013
+ );
2014
+ }
2015
+ const computed = createHmacDigest(
2016
+ "sha256",
2017
+ this.config.webhookSecret,
2018
+ `${timestamp}.${rawPayload}`
2019
+ );
2020
+ const verified = signatures.some(
2021
+ (item) => secureCompareHex(item, computed)
2022
+ );
2023
+ if (!verified) {
2024
+ throw new WebhookVerificationError(
2025
+ "Stripe webhook signature verification failed.",
2026
+ {
2027
+ context: {
2028
+ provider: this.name
2029
+ }
2030
+ }
2031
+ );
2032
+ }
2033
+ }
2034
+ async createPaymentIntent(request, captureMethod) {
2035
+ const body = {
2036
+ amount: request.amount,
2037
+ currency: request.currency.toLowerCase(),
2038
+ confirm: true,
2039
+ capture_method: captureMethod,
2040
+ metadata: request.metadata ?? {},
2041
+ ...buildStripePaymentMethodData(request.paymentMethod)
2042
+ };
2043
+ if (request.description) {
2044
+ body.description = request.description;
2045
+ }
2046
+ if (request.customer?.email) {
2047
+ body.receipt_email = request.customer.email;
2048
+ }
2049
+ if (request.customer?.name) {
2050
+ body["shipping[name]"] = request.customer.name;
2051
+ }
2052
+ const intent = await this.postForm(
2053
+ "/v1/payment_intents",
2054
+ body,
2055
+ captureMethod === "manual" ? "authorize" : "charge"
2056
+ );
2057
+ return this.normalizePaymentResult(intent, request);
2058
+ }
2059
+ normalizePaymentResult(intent, request) {
2060
+ return {
2061
+ id: intent.id,
2062
+ status: mapStripeStatus(intent.status),
2063
+ provider: this.name,
2064
+ providerId: intent.latest_charge ?? intent.id,
2065
+ amount: intent.amount,
2066
+ currency: intent.currency.toUpperCase(),
2067
+ paymentMethod: extractPaymentMethodSnapshot(request, intent),
2068
+ customer: request?.customer?.email ? {
2069
+ email: request.customer.email
2070
+ } : void 0,
2071
+ metadata: {
2072
+ ...request?.metadata ?? {},
2073
+ ...intent.metadata ?? {}
2074
+ },
2075
+ routing: {
2076
+ source: "local",
2077
+ reason: "stripe adapter request"
2078
+ },
2079
+ createdAt: toIsoTimestamp(intent.created),
2080
+ providerMetadata: {
2081
+ stripeStatus: intent.status,
2082
+ paymentMethod: intent.payment_method
2083
+ }
2084
+ };
2085
+ }
2086
+ async get(path, operation) {
2087
+ return requestJson({
2088
+ provider: this.name,
2089
+ fetchFn: this.config.fetchFn,
2090
+ baseUrl: this.config.baseUrl,
2091
+ path,
2092
+ method: "GET",
2093
+ timeoutMs: this.config.timeoutMs,
2094
+ headers: {
2095
+ Authorization: `Bearer ${this.config.apiKey}`
2096
+ }
2097
+ }).catch((error) => {
2098
+ throw {
2099
+ ...asRecord5(error),
2100
+ hint: {
2101
+ ...asRecord5(asRecord5(error)?.hint) ?? {},
2102
+ providerMessage: readString5(asRecord5(error), "message") ?? "Stripe request failed.",
2103
+ raw: error
2104
+ },
2105
+ operation
2106
+ };
2107
+ });
2108
+ }
2109
+ async postForm(path, body, operation) {
2110
+ const formBody = encodeFormBody(body);
2111
+ const payload = formBody.toString();
2112
+ return requestJson({
2113
+ provider: this.name,
2114
+ fetchFn: this.config.fetchFn,
2115
+ baseUrl: this.config.baseUrl,
2116
+ path,
2117
+ method: "POST",
2118
+ timeoutMs: this.config.timeoutMs,
2119
+ headers: {
2120
+ Authorization: `Bearer ${this.config.apiKey}`,
2121
+ "content-type": "application/x-www-form-urlencoded"
2122
+ },
2123
+ body: payload
2124
+ }).catch((error) => {
2125
+ const record = asRecord5(error);
2126
+ const hint = asRecord5(record?.hint);
2127
+ throw {
2128
+ ...record,
2129
+ hint: {
2130
+ ...hint,
2131
+ providerCode: readString5(hint, "providerCode") ?? readString5(record, "providerCode"),
2132
+ providerMessage: readString5(hint, "providerMessage") ?? readString5(record, "message") ?? "Stripe request failed.",
2133
+ declineCode: readString5(hint, "declineCode") ?? readString5(asRecord5(hint?.raw), "decline_code"),
2134
+ raw: error
2135
+ },
2136
+ operation
2137
+ };
2138
+ });
2139
+ }
2140
+ };
2141
+
2142
+ // src/idempotency/memory-store.ts
2143
+ var MemoryIdempotencyStore = class {
2144
+ records = /* @__PURE__ */ new Map();
2145
+ get(key) {
2146
+ const record = this.records.get(key);
2147
+ if (!record) {
2148
+ return null;
2149
+ }
2150
+ if (record.expiresAt <= Date.now()) {
2151
+ this.records.delete(key);
2152
+ return null;
2153
+ }
2154
+ return record;
2155
+ }
2156
+ set(record) {
2157
+ this.records.set(record.key, record);
2158
+ }
2159
+ delete(key) {
2160
+ this.records.delete(key);
2161
+ }
2162
+ clearExpired(now = Date.now()) {
2163
+ for (const [key, record] of this.records.entries()) {
2164
+ if (record.expiresAt <= now) {
2165
+ this.records.delete(key);
2166
+ }
2167
+ }
2168
+ }
2169
+ };
2170
+
2171
+ // src/idempotency/hash.ts
2172
+ import { createHash } from "crypto";
2173
+ function stableSerialize(value) {
2174
+ if (value === null || value === void 0) {
2175
+ return "null";
2176
+ }
2177
+ if (typeof value !== "object") {
2178
+ return JSON.stringify(value);
2179
+ }
2180
+ if (Array.isArray(value)) {
2181
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
2182
+ }
2183
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableSerialize(item)}`);
2184
+ return `{${entries.join(",")}}`;
2185
+ }
2186
+ function hashIdempotencyPayload(payload) {
2187
+ const serialized = stableSerialize(payload);
2188
+ return createHash("sha256").update(serialized).digest("hex");
2189
+ }
2190
+
2191
+ // src/idempotency/store.ts
2192
+ var DEFAULT_IDEMPOTENCY_TTL_MS = 864e5;
2193
+
2194
+ // src/platform/buffer.ts
2195
+ var BatchBuffer = class {
2196
+ constructor(maxSize) {
2197
+ this.maxSize = maxSize;
2198
+ }
2199
+ items = [];
2200
+ push(item) {
2201
+ this.items.push(item);
2202
+ if (this.items.length >= this.maxSize) {
2203
+ return this.flush();
2204
+ }
2205
+ return null;
2206
+ }
2207
+ flush() {
2208
+ const snapshot = [...this.items];
2209
+ this.items.length = 0;
2210
+ return snapshot;
2211
+ }
2212
+ size() {
2213
+ return this.items.length;
2214
+ }
2215
+ };
2216
+
2217
+ // src/platform/connector.ts
2218
+ var PlatformConnector = class _PlatformConnector {
2219
+ static DEFAULT_BASE_URL = "https://api.vaultsaas.com";
2220
+ static DEFAULT_TIMEOUT_MS = 75;
2221
+ static DEFAULT_BATCH_SIZE = 50;
2222
+ static DEFAULT_FLUSH_INTERVAL_MS = 2e3;
2223
+ static DEFAULT_MAX_RETRIES = 2;
2224
+ static DEFAULT_INITIAL_BACKOFF_MS = 100;
2225
+ config;
2226
+ transactionBuffer;
2227
+ webhookBuffer;
2228
+ fetchFn;
2229
+ logger;
2230
+ flushTimer;
2231
+ transactionSendQueue = Promise.resolve();
2232
+ webhookSendQueue = Promise.resolve();
2233
+ constructor(config) {
2234
+ this.config = {
2235
+ ...config,
2236
+ baseUrl: config.baseUrl ?? _PlatformConnector.DEFAULT_BASE_URL,
2237
+ timeoutMs: config.timeoutMs ?? _PlatformConnector.DEFAULT_TIMEOUT_MS,
2238
+ batchSize: config.batchSize ?? _PlatformConnector.DEFAULT_BATCH_SIZE,
2239
+ flushIntervalMs: config.flushIntervalMs ?? _PlatformConnector.DEFAULT_FLUSH_INTERVAL_MS,
2240
+ maxRetries: config.maxRetries ?? _PlatformConnector.DEFAULT_MAX_RETRIES,
2241
+ initialBackoffMs: config.initialBackoffMs ?? _PlatformConnector.DEFAULT_INITIAL_BACKOFF_MS
2242
+ };
2243
+ this.fetchFn = config.fetchFn ?? fetch;
2244
+ this.logger = config.logger;
2245
+ this.transactionBuffer = new BatchBuffer(this.config.batchSize);
2246
+ this.webhookBuffer = new BatchBuffer(this.config.batchSize);
2247
+ this.flushTimer = setInterval(() => {
2248
+ void this.flush().catch((error) => {
2249
+ this.warn("Platform connector periodic flush failed.", {
2250
+ error: error instanceof Error ? error.message : String(error)
2251
+ });
2252
+ });
2253
+ }, this.config.flushIntervalMs);
2254
+ if (typeof this.flushTimer.unref === "function") {
2255
+ this.flushTimer.unref();
2256
+ }
2257
+ }
2258
+ close() {
2259
+ clearInterval(this.flushTimer);
2260
+ }
2261
+ async decideRouting(request) {
2262
+ try {
2263
+ const response = await this.postJson({
2264
+ path: "/v1/routing/decide",
2265
+ body: request,
2266
+ timeoutMs: this.config.timeoutMs,
2267
+ maxRetries: 0
2268
+ });
2269
+ const decision = this.normalizeRoutingDecision(response);
2270
+ if (!decision) {
2271
+ return null;
2272
+ }
2273
+ return decision.provider ? decision : null;
2274
+ } catch (error) {
2275
+ throw new VaultNetworkError("Platform routing decision failed.", {
2276
+ code: "PLATFORM_UNREACHABLE",
2277
+ context: {
2278
+ endpoint: "/v1/routing/decide",
2279
+ operation: "decideRouting",
2280
+ cause: error instanceof Error ? error.message : String(error)
2281
+ }
2282
+ });
2283
+ }
2284
+ }
2285
+ queueTransactionReport(transaction) {
2286
+ const batch = this.transactionBuffer.push(transaction);
2287
+ if (!batch) {
2288
+ return;
2289
+ }
2290
+ this.enqueueTransactionBatch(batch);
2291
+ }
2292
+ queueWebhookEvent(event) {
2293
+ const batch = this.webhookBuffer.push(event);
2294
+ if (!batch) {
2295
+ return;
2296
+ }
2297
+ this.enqueueWebhookBatch(batch);
2298
+ }
2299
+ async flush() {
2300
+ const pendingTransactions = this.transactionBuffer.flush();
2301
+ if (pendingTransactions.length > 0) {
2302
+ this.enqueueTransactionBatch(pendingTransactions);
2303
+ }
2304
+ const pendingWebhookEvents = this.webhookBuffer.flush();
2305
+ if (pendingWebhookEvents.length > 0) {
2306
+ this.enqueueWebhookBatch(pendingWebhookEvents);
2307
+ }
2308
+ await Promise.all([this.transactionSendQueue, this.webhookSendQueue]);
2309
+ }
2310
+ enqueueTransactionBatch(batch) {
2311
+ this.transactionSendQueue = this.transactionSendQueue.then(async () => {
2312
+ try {
2313
+ await this.postJson({
2314
+ path: "/v1/transactions/report",
2315
+ body: { transactions: batch }
2316
+ });
2317
+ } catch (error) {
2318
+ this.warn("Failed to report transactions batch to platform.", {
2319
+ endpoint: "/v1/transactions/report",
2320
+ batchSize: batch.length,
2321
+ error: error instanceof Error ? error.message : String(error)
2322
+ });
2323
+ }
2324
+ });
2325
+ }
2326
+ enqueueWebhookBatch(batch) {
2327
+ this.webhookSendQueue = this.webhookSendQueue.then(async () => {
2328
+ try {
2329
+ await this.postJson({
2330
+ path: "/v1/events/webhook",
2331
+ body: { events: batch }
2332
+ });
2333
+ } catch (error) {
2334
+ this.warn("Failed to forward webhook batch to platform.", {
2335
+ endpoint: "/v1/events/webhook",
2336
+ batchSize: batch.length,
2337
+ error: error instanceof Error ? error.message : String(error)
2338
+ });
2339
+ }
2340
+ });
2341
+ }
2342
+ async postJson(options) {
2343
+ const maxRetries = options.maxRetries ?? this.config.maxRetries;
2344
+ const timeoutMs = options.timeoutMs ?? this.config.timeoutMs;
2345
+ let attempt = 0;
2346
+ while (attempt <= maxRetries) {
2347
+ attempt += 1;
2348
+ try {
2349
+ const response = await this.fetchWithTimeout(options.path, {
2350
+ method: "POST",
2351
+ body: JSON.stringify(options.body),
2352
+ timeoutMs
2353
+ });
2354
+ if (!response.ok) {
2355
+ if (attempt <= maxRetries && (response.status >= 500 || response.status === 429)) {
2356
+ await this.delay(this.backoffForAttempt(attempt));
2357
+ continue;
2358
+ }
2359
+ throw new Error(`platform status ${response.status}`);
2360
+ }
2361
+ const contentType = response.headers.get("content-type");
2362
+ if (contentType?.includes("application/json")) {
2363
+ return await response.json();
2364
+ }
2365
+ return {};
2366
+ } catch (error) {
2367
+ if (attempt > maxRetries) {
2368
+ throw error;
2369
+ }
2370
+ this.debug("Retrying platform request after failure.", {
2371
+ endpoint: options.path,
2372
+ attempt,
2373
+ maxRetries,
2374
+ error: error instanceof Error ? error.message : String(error)
2375
+ });
2376
+ await this.delay(this.backoffForAttempt(attempt));
2377
+ }
2378
+ }
2379
+ throw new Error("Platform request exhausted retries.");
2380
+ }
2381
+ async fetchWithTimeout(path, options) {
2382
+ const controller = new AbortController();
2383
+ const timeout = setTimeout(() => {
2384
+ controller.abort();
2385
+ }, options.timeoutMs);
2386
+ try {
2387
+ const response = await this.fetchFn(this.urlFor(path), {
2388
+ method: options.method,
2389
+ headers: {
2390
+ authorization: `Bearer ${this.config.apiKey}`,
2391
+ "content-type": "application/json"
2392
+ },
2393
+ body: options.body,
2394
+ signal: controller.signal
2395
+ });
2396
+ return response;
2397
+ } finally {
2398
+ clearTimeout(timeout);
2399
+ }
2400
+ }
2401
+ normalizeRoutingDecision(input) {
2402
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
2403
+ return null;
2404
+ }
2405
+ const data = input;
2406
+ return {
2407
+ provider: typeof data.provider === "string" ? data.provider : null,
2408
+ source: typeof data.source === "string" ? data.source : void 0,
2409
+ reason: typeof data.reason === "string" ? data.reason : void 0,
2410
+ decisionId: typeof data.decisionId === "string" ? data.decisionId : void 0,
2411
+ ttlMs: typeof data.ttlMs === "number" ? data.ttlMs : void 0,
2412
+ cascade: Array.isArray(data.cascade) ? data.cascade.filter(
2413
+ (value) => typeof value === "string"
2414
+ ) : void 0
2415
+ };
2416
+ }
2417
+ backoffForAttempt(attempt) {
2418
+ return this.config.initialBackoffMs * 2 ** (attempt - 1);
2419
+ }
2420
+ urlFor(path) {
2421
+ const base = this.config.baseUrl.replace(/\/+$/, "");
2422
+ return `${base}${path}`;
2423
+ }
2424
+ async delay(ms) {
2425
+ await new Promise((resolve) => {
2426
+ setTimeout(resolve, ms);
2427
+ });
2428
+ }
2429
+ debug(message, context) {
2430
+ this.logger?.debug(message, context);
2431
+ }
2432
+ warn(message, context) {
2433
+ this.logger?.warn(message, context);
2434
+ }
2435
+ };
2436
+
2437
+ // src/router/rule-evaluator.ts
2438
+ function matchesValue(ruleValue, input) {
2439
+ if (input === void 0) {
2440
+ return false;
2441
+ }
2442
+ return Array.isArray(ruleValue) ? ruleValue.includes(input) : ruleValue === input;
2443
+ }
2444
+ function ruleMatchesContext(rule, context) {
2445
+ const { match } = rule;
2446
+ if (match.default) {
2447
+ return true;
2448
+ }
2449
+ if (match.country && !matchesValue(match.country, context.country)) {
2450
+ return false;
2451
+ }
2452
+ if (match.currency && !matchesValue(match.currency, context.currency)) {
2453
+ return false;
2454
+ }
2455
+ if (match.paymentMethod && !matchesValue(match.paymentMethod, context.paymentMethod)) {
2456
+ return false;
2457
+ }
2458
+ if (match.amountMin !== void 0 && (context.amount ?? Number.NEGATIVE_INFINITY) < match.amountMin) {
2459
+ return false;
2460
+ }
2461
+ if (match.amountMax !== void 0 && (context.amount ?? Number.POSITIVE_INFINITY) > match.amountMax) {
2462
+ return false;
2463
+ }
2464
+ if (match.metadata) {
2465
+ for (const [key, expected] of Object.entries(match.metadata)) {
2466
+ if (context.metadata?.[key] !== expected) {
2467
+ return false;
2468
+ }
2469
+ }
2470
+ }
2471
+ return true;
2472
+ }
2473
+
2474
+ // src/router/router.ts
2475
+ var Router = class {
2476
+ rules;
2477
+ random;
2478
+ adapterMetadata;
2479
+ logger;
2480
+ constructor(rules, options = {}) {
2481
+ this.rules = [...rules];
2482
+ this.random = options.random ?? Math.random;
2483
+ this.adapterMetadata = options.adapterMetadata ?? {};
2484
+ this.logger = options.logger;
2485
+ if (!this.rules.some((rule) => rule.match.default)) {
2486
+ throw new VaultRoutingError(
2487
+ "Routing rules must include a default fallback rule."
2488
+ );
2489
+ }
2490
+ }
2491
+ decide(context) {
2492
+ if (context.providerOverride) {
2493
+ if (context.exclude?.includes(context.providerOverride)) {
2494
+ return null;
2495
+ }
2496
+ if (!this.providerSupportsContext(context.providerOverride, context)) {
2497
+ return null;
2498
+ }
2499
+ return {
2500
+ provider: context.providerOverride,
2501
+ reason: `provider override selected ${context.providerOverride}`,
2502
+ rule: {
2503
+ provider: context.providerOverride,
2504
+ match: {
2505
+ default: true
2506
+ }
2507
+ }
2508
+ };
2509
+ }
2510
+ for (let index = 0; index < this.rules.length; index += 1) {
2511
+ const rule = this.rules[index];
2512
+ if (!rule) {
2513
+ continue;
2514
+ }
2515
+ if (context.exclude?.includes(rule.provider)) {
2516
+ continue;
2517
+ }
2518
+ if (!ruleMatchesContext(rule, context)) {
2519
+ continue;
2520
+ }
2521
+ if (!this.providerSupportsContext(rule.provider, context)) {
2522
+ continue;
2523
+ }
2524
+ if (rule.weight !== void 0) {
2525
+ const weightedCandidates = this.getWeightedCandidates(index, context);
2526
+ if (weightedCandidates.length > 0) {
2527
+ const selected = this.selectWeightedRule(weightedCandidates);
2528
+ return {
2529
+ provider: selected.rule.provider,
2530
+ reason: this.buildWeightedReason(
2531
+ selected,
2532
+ weightedCandidates.length
2533
+ ),
2534
+ rule: selected.rule
2535
+ };
2536
+ }
2537
+ }
2538
+ return {
2539
+ provider: rule.provider,
2540
+ reason: this.buildRuleReason(rule, index),
2541
+ rule
2542
+ };
2543
+ }
2544
+ return null;
2545
+ }
2546
+ providerSupportsContext(provider, context) {
2547
+ const meta = this.adapterMetadata[provider];
2548
+ if (!meta) {
2549
+ return true;
2550
+ }
2551
+ if (context.paymentMethod && meta.supportedMethods.length > 0 && !meta.supportedMethods.includes(context.paymentMethod)) {
2552
+ this.logger?.warn(
2553
+ `Provider "${provider}" does not support payment method "${context.paymentMethod}". Skipping.`,
2554
+ {
2555
+ provider,
2556
+ paymentMethod: context.paymentMethod,
2557
+ supportedMethods: [...meta.supportedMethods]
2558
+ }
2559
+ );
2560
+ return false;
2561
+ }
2562
+ if (context.currency && meta.supportedCurrencies.length > 0 && !meta.supportedCurrencies.includes(context.currency)) {
2563
+ this.logger?.warn(
2564
+ `Provider "${provider}" does not support currency "${context.currency}". Skipping.`,
2565
+ {
2566
+ provider,
2567
+ currency: context.currency,
2568
+ supportedCurrencies: [...meta.supportedCurrencies]
2569
+ }
2570
+ );
2571
+ return false;
2572
+ }
2573
+ if (context.country && meta.supportedCountries.length > 0 && !meta.supportedCountries.includes(context.country)) {
2574
+ this.logger?.warn(
2575
+ `Provider "${provider}" does not support country "${context.country}". Skipping.`,
2576
+ {
2577
+ provider,
2578
+ country: context.country,
2579
+ supportedCountries: [...meta.supportedCountries]
2580
+ }
2581
+ );
2582
+ return false;
2583
+ }
2584
+ return true;
2585
+ }
2586
+ getWeightedCandidates(startIndex, context) {
2587
+ const candidates = [];
2588
+ for (let index = startIndex; index < this.rules.length; index += 1) {
2589
+ const rule = this.rules[index];
2590
+ if (!rule) {
2591
+ continue;
2592
+ }
2593
+ if (!ruleMatchesContext(rule, context)) {
2594
+ break;
2595
+ }
2596
+ if (rule.weight === void 0) {
2597
+ break;
2598
+ }
2599
+ if (context.exclude?.includes(rule.provider)) {
2600
+ continue;
2601
+ }
2602
+ if (!this.providerSupportsContext(rule.provider, context)) {
2603
+ continue;
2604
+ }
2605
+ if (rule.weight > 0) {
2606
+ candidates.push({
2607
+ index,
2608
+ rule
2609
+ });
2610
+ }
2611
+ }
2612
+ return candidates;
2613
+ }
2614
+ selectWeightedRule(candidates) {
2615
+ const totalWeight = candidates.reduce(
2616
+ (acc, candidate) => acc + (candidate.rule.weight ?? 0),
2617
+ 0
2618
+ );
2619
+ const randomValue = this.random() * totalWeight;
2620
+ let cumulativeWeight = 0;
2621
+ for (const candidate of candidates) {
2622
+ cumulativeWeight += candidate.rule.weight ?? 0;
2623
+ if (randomValue < cumulativeWeight) {
2624
+ return candidate;
2625
+ }
2626
+ }
2627
+ const fallback = candidates[candidates.length - 1];
2628
+ if (!fallback) {
2629
+ throw new VaultRoutingError(
2630
+ "No weighted routing candidates were available."
2631
+ );
2632
+ }
2633
+ return fallback;
2634
+ }
2635
+ buildRuleReason(rule, index) {
2636
+ if (rule.match.default) {
2637
+ return `default fallback rule matched at index ${index}`;
2638
+ }
2639
+ const criteria = this.getMatchCriteria(rule);
2640
+ if (criteria.length > 0) {
2641
+ return `rule matched at index ${index} using ${criteria.join(", ")}`;
2642
+ }
2643
+ return `rule matched at index ${index}`;
2644
+ }
2645
+ buildWeightedReason(selected, candidateCount) {
2646
+ return `weighted selection chose provider ${selected.rule.provider} from ${candidateCount} candidates starting at index ${selected.index}`;
2647
+ }
2648
+ getMatchCriteria(rule) {
2649
+ const criteria = [];
2650
+ if (rule.match.currency !== void 0) {
2651
+ criteria.push("currency");
2652
+ }
2653
+ if (rule.match.country !== void 0) {
2654
+ criteria.push("country");
2655
+ }
2656
+ if (rule.match.paymentMethod !== void 0) {
2657
+ criteria.push("paymentMethod");
2658
+ }
2659
+ if (rule.match.amountMin !== void 0 || rule.match.amountMax !== void 0) {
2660
+ criteria.push("amount");
2661
+ }
2662
+ if (rule.match.metadata !== void 0) {
2663
+ criteria.push("metadata");
2664
+ }
2665
+ return criteria;
2666
+ }
2667
+ };
2668
+
2669
+ // src/client/config-validation.ts
2670
+ var LOG_LEVELS = /* @__PURE__ */ new Set(["silent", "error", "warn", "info", "debug"]);
2671
+ function isPlainObject(value) {
2672
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2673
+ }
2674
+ function assertPositiveFiniteInteger(value, field, context) {
2675
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
2676
+ throw new VaultConfigError(`${field} must be a positive integer.`, context);
2677
+ }
2678
+ }
2679
+ function validateProviderConfig(name, provider) {
2680
+ if (!provider || typeof provider !== "object") {
2681
+ throw new VaultConfigError("Provider configuration must be an object.", {
2682
+ provider: name
2683
+ });
2684
+ }
2685
+ if (typeof provider.adapter !== "function") {
2686
+ throw new VaultConfigError("Provider adapter constructor is missing.", {
2687
+ provider: name
2688
+ });
2689
+ }
2690
+ const adapter = provider.adapter;
2691
+ if (!Array.isArray(adapter.supportedMethods)) {
2692
+ throw new VaultConfigError(
2693
+ "Provider adapter must declare static supportedMethods.",
2694
+ {
2695
+ provider: name
2696
+ }
2697
+ );
2698
+ }
2699
+ if (!Array.isArray(adapter.supportedCurrencies)) {
2700
+ throw new VaultConfigError(
2701
+ "Provider adapter must declare static supportedCurrencies.",
2702
+ {
2703
+ provider: name
2704
+ }
2705
+ );
2706
+ }
2707
+ if (!Array.isArray(adapter.supportedCountries)) {
2708
+ throw new VaultConfigError(
2709
+ "Provider adapter must declare static supportedCountries.",
2710
+ {
2711
+ provider: name
2712
+ }
2713
+ );
2714
+ }
2715
+ if (!isPlainObject(provider.config)) {
2716
+ throw new VaultConfigError("Provider config must be a plain object.", {
2717
+ provider: name
2718
+ });
2719
+ }
2720
+ if (provider.priority !== void 0 && (!Number.isFinite(provider.priority) || !Number.isInteger(provider.priority))) {
2721
+ throw new VaultConfigError("Provider priority must be an integer.", {
2722
+ provider: name
2723
+ });
2724
+ }
2725
+ }
2726
+ function validateLogger(logger) {
2727
+ const methods = [
2728
+ "error",
2729
+ "warn",
2730
+ "info",
2731
+ "debug"
2732
+ ];
2733
+ for (const method of methods) {
2734
+ if (typeof logger[method] !== "function") {
2735
+ throw new VaultConfigError("Logger implementation is missing a method.", {
2736
+ method
2737
+ });
2738
+ }
2739
+ }
2740
+ }
2741
+ function validateRoutingRules(rules, providers) {
2742
+ if (!Array.isArray(rules) || rules.length === 0) {
2743
+ throw new VaultConfigError(
2744
+ "Routing rules must include at least one rule when routing is configured."
2745
+ );
2746
+ }
2747
+ let hasDefaultRule = false;
2748
+ for (const [index, rule] of rules.entries()) {
2749
+ if (!rule || typeof rule !== "object") {
2750
+ throw new VaultConfigError("Routing rule must be an object.", {
2751
+ index
2752
+ });
2753
+ }
2754
+ if (!rule.provider || typeof rule.provider !== "string") {
2755
+ throw new VaultConfigError(
2756
+ "Routing rule provider must be a non-empty string.",
2757
+ {
2758
+ index
2759
+ }
2760
+ );
2761
+ }
2762
+ const provider = providers[rule.provider];
2763
+ if (!provider || provider.enabled === false) {
2764
+ throw new VaultConfigError(
2765
+ "Routing rule provider must reference an enabled configured provider.",
2766
+ {
2767
+ index,
2768
+ provider: rule.provider
2769
+ }
2770
+ );
2771
+ }
2772
+ if (!rule.match || typeof rule.match !== "object") {
2773
+ throw new VaultConfigError(
2774
+ "Routing rule match configuration is required.",
2775
+ {
2776
+ index,
2777
+ provider: rule.provider
2778
+ }
2779
+ );
2780
+ }
2781
+ if (rule.match.default) {
2782
+ hasDefaultRule = true;
2783
+ }
2784
+ if (rule.match.amountMin !== void 0 && (!Number.isFinite(rule.match.amountMin) || rule.match.amountMin < 0)) {
2785
+ throw new VaultConfigError(
2786
+ "Routing rule amountMin must be a non-negative number.",
2787
+ {
2788
+ index,
2789
+ provider: rule.provider
2790
+ }
2791
+ );
2792
+ }
2793
+ if (rule.match.amountMax !== void 0 && (!Number.isFinite(rule.match.amountMax) || rule.match.amountMax < 0)) {
2794
+ throw new VaultConfigError(
2795
+ "Routing rule amountMax must be a non-negative number.",
2796
+ {
2797
+ index,
2798
+ provider: rule.provider
2799
+ }
2800
+ );
2801
+ }
2802
+ if (rule.match.amountMin !== void 0 && rule.match.amountMax !== void 0 && rule.match.amountMin > rule.match.amountMax) {
2803
+ throw new VaultConfigError(
2804
+ "Routing rule amountMin cannot exceed amountMax.",
2805
+ {
2806
+ index,
2807
+ provider: rule.provider
2808
+ }
2809
+ );
2810
+ }
2811
+ if (rule.weight !== void 0 && (!Number.isFinite(rule.weight) || rule.weight <= 0)) {
2812
+ throw new VaultConfigError(
2813
+ "Routing rule weight must be a positive number.",
2814
+ {
2815
+ index,
2816
+ provider: rule.provider
2817
+ }
2818
+ );
2819
+ }
2820
+ }
2821
+ if (!hasDefaultRule) {
2822
+ throw new VaultConfigError(
2823
+ "Routing configuration must include a default fallback rule."
2824
+ );
2825
+ }
2826
+ }
2827
+ function validateVaultConfig(config) {
2828
+ if (!config || typeof config !== "object") {
2829
+ throw new VaultConfigError("VaultClient configuration must be an object.");
2830
+ }
2831
+ if (!isPlainObject(config.providers)) {
2832
+ throw new VaultConfigError("At least one provider must be configured.");
2833
+ }
2834
+ const providerEntries = Object.entries(config.providers);
2835
+ if (providerEntries.length === 0) {
2836
+ throw new VaultConfigError("At least one provider must be configured.");
2837
+ }
2838
+ for (const [name, provider] of providerEntries) {
2839
+ validateProviderConfig(name, provider);
2840
+ }
2841
+ const enabledProviders = providerEntries.filter(
2842
+ ([, provider]) => provider.enabled !== false
2843
+ );
2844
+ if (enabledProviders.length === 0) {
2845
+ throw new VaultConfigError("No enabled providers are available.");
2846
+ }
2847
+ if (config.routing) {
2848
+ validateRoutingRules(config.routing.rules, config.providers);
2849
+ }
2850
+ if (config.timeout !== void 0) {
2851
+ assertPositiveFiniteInteger(config.timeout, "timeout");
2852
+ }
2853
+ if (config.idempotency?.ttlMs !== void 0) {
2854
+ assertPositiveFiniteInteger(config.idempotency.ttlMs, "idempotency.ttlMs");
2855
+ }
2856
+ if (config.idempotency?.store) {
2857
+ const store = config.idempotency.store;
2858
+ const requiredMethods = ["get", "set", "delete", "clearExpired"];
2859
+ for (const method of requiredMethods) {
2860
+ if (typeof store[method] !== "function") {
2861
+ throw new VaultConfigError(
2862
+ "Idempotency store is missing required methods.",
2863
+ {
2864
+ method
2865
+ }
2866
+ );
2867
+ }
2868
+ }
2869
+ }
2870
+ if (config.platformApiKey !== void 0 && config.platformApiKey.trim() === "") {
2871
+ throw new VaultConfigError("platformApiKey cannot be empty when provided.");
2872
+ }
2873
+ if (config.platform) {
2874
+ if (config.platform.baseUrl !== void 0 && config.platform.baseUrl.trim() === "") {
2875
+ throw new VaultConfigError(
2876
+ "platform.baseUrl cannot be empty when provided."
2877
+ );
2878
+ }
2879
+ if (config.platform.timeoutMs !== void 0) {
2880
+ assertPositiveFiniteInteger(
2881
+ config.platform.timeoutMs,
2882
+ "platform.timeoutMs"
2883
+ );
2884
+ }
2885
+ if (config.platform.batchSize !== void 0) {
2886
+ assertPositiveFiniteInteger(
2887
+ config.platform.batchSize,
2888
+ "platform.batchSize"
2889
+ );
2890
+ }
2891
+ if (config.platform.flushIntervalMs !== void 0) {
2892
+ assertPositiveFiniteInteger(
2893
+ config.platform.flushIntervalMs,
2894
+ "platform.flushIntervalMs"
2895
+ );
2896
+ }
2897
+ if (config.platform.maxRetries !== void 0) {
2898
+ assertPositiveFiniteInteger(
2899
+ config.platform.maxRetries,
2900
+ "platform.maxRetries"
2901
+ );
2902
+ }
2903
+ if (config.platform.initialBackoffMs !== void 0) {
2904
+ assertPositiveFiniteInteger(
2905
+ config.platform.initialBackoffMs,
2906
+ "platform.initialBackoffMs"
2907
+ );
2908
+ }
2909
+ }
2910
+ if (config.logging?.level && !LOG_LEVELS.has(config.logging.level)) {
2911
+ throw new VaultConfigError("Invalid logging level configured.", {
2912
+ level: config.logging.level
2913
+ });
2914
+ }
2915
+ if (config.logging?.logger) {
2916
+ validateLogger(config.logging.logger);
2917
+ }
2918
+ }
2919
+
2920
+ // src/client/vault-client.ts
2921
+ function normalizeAdapterMetadata(metadata) {
2922
+ return {
2923
+ supportedMethods: metadata.supportedMethods.map(
2924
+ (method) => method.toLowerCase()
2925
+ ),
2926
+ supportedCurrencies: metadata.supportedCurrencies.map(
2927
+ (currency) => currency.toUpperCase()
2928
+ ),
2929
+ supportedCountries: metadata.supportedCountries.map(
2930
+ (country) => country.toUpperCase()
2931
+ )
2932
+ };
2933
+ }
2934
+ var VaultClient = class {
2935
+ config;
2936
+ adapters = /* @__PURE__ */ new Map();
2937
+ providerOrder;
2938
+ router;
2939
+ platformConnector;
2940
+ idempotencyStore;
2941
+ idempotencyTtlMs;
2942
+ transactionProviderIndex = /* @__PURE__ */ new Map();
2943
+ constructor(config) {
2944
+ validateVaultConfig(config);
2945
+ this.config = config;
2946
+ const entries = Object.entries(config.providers);
2947
+ const adapterMetadata = {};
2948
+ this.providerOrder = entries.filter(([, provider]) => provider.enabled !== false).sort(([, a], [, b]) => (a.priority ?? 0) - (b.priority ?? 0)).map(([name, provider]) => {
2949
+ if (!provider.adapter) {
2950
+ throw new VaultConfigError(
2951
+ "Provider adapter constructor is missing.",
2952
+ {
2953
+ code: "PROVIDER_NOT_CONFIGURED",
2954
+ context: {
2955
+ provider: name
2956
+ }
2957
+ }
2958
+ );
2959
+ }
2960
+ const adapter = new provider.adapter(provider.config);
2961
+ this.adapters.set(name, adapter);
2962
+ adapterMetadata[name] = normalizeAdapterMetadata({
2963
+ supportedMethods: provider.adapter.supportedMethods ?? adapter.metadata.supportedMethods,
2964
+ supportedCurrencies: provider.adapter.supportedCurrencies ?? adapter.metadata.supportedCurrencies,
2965
+ supportedCountries: provider.adapter.supportedCountries ?? adapter.metadata.supportedCountries
2966
+ });
2967
+ return name;
2968
+ });
2969
+ if (this.providerOrder.length === 0) {
2970
+ throw new VaultConfigError("No enabled providers are available.");
2971
+ }
2972
+ this.router = config.routing?.rules?.length ? new Router(config.routing.rules, {
2973
+ adapterMetadata,
2974
+ logger: config.logging?.logger
2975
+ }) : null;
2976
+ this.platformConnector = config.platformApiKey ? new PlatformConnector({
2977
+ apiKey: config.platformApiKey,
2978
+ baseUrl: config.platform?.baseUrl,
2979
+ timeoutMs: config.platform?.timeoutMs,
2980
+ batchSize: config.platform?.batchSize,
2981
+ flushIntervalMs: config.platform?.flushIntervalMs,
2982
+ maxRetries: config.platform?.maxRetries,
2983
+ initialBackoffMs: config.platform?.initialBackoffMs,
2984
+ logger: config.logging?.logger
2985
+ }) : null;
2986
+ this.idempotencyStore = config.idempotency?.store ?? new MemoryIdempotencyStore();
2987
+ this.idempotencyTtlMs = config.idempotency?.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS;
2988
+ }
2989
+ async charge(request) {
2990
+ return this.executeIdempotentOperation("charge", request, async () => {
2991
+ const route = await this.resolveProviderForCharge(request);
2992
+ const adapter = this.getAdapter(route.provider);
2993
+ const startedAt = Date.now();
2994
+ const result = await this.wrapProviderCall(
2995
+ route.provider,
2996
+ "charge",
2997
+ () => adapter.charge(request)
2998
+ );
2999
+ const latencyMs = Date.now() - startedAt;
3000
+ const normalized = this.withRouting(result, route, request);
3001
+ this.transactionProviderIndex.set(normalized.id, route.provider);
3002
+ this.queueTransactionReport({
3003
+ id: normalized.id,
3004
+ provider: normalized.provider,
3005
+ providerId: normalized.providerId,
3006
+ status: normalized.status,
3007
+ amount: normalized.amount,
3008
+ currency: normalized.currency,
3009
+ country: request.customer?.address?.country,
3010
+ paymentMethod: normalized.paymentMethod.type,
3011
+ cardBin: this.extractCardBin(request),
3012
+ cardBrand: normalized.paymentMethod.brand,
3013
+ latencyMs,
3014
+ routingSource: normalized.routing.source,
3015
+ routingDecisionId: route.decisionId,
3016
+ idempotencyKey: request.idempotencyKey,
3017
+ timestamp: normalized.createdAt
3018
+ });
3019
+ return normalized;
3020
+ });
3021
+ }
3022
+ async authorize(request) {
3023
+ return this.executeIdempotentOperation("authorize", request, async () => {
3024
+ const route = await this.resolveProviderForCharge(request);
3025
+ const adapter = this.getAdapter(route.provider);
3026
+ const startedAt = Date.now();
3027
+ const result = await this.wrapProviderCall(
3028
+ route.provider,
3029
+ "authorize",
3030
+ () => adapter.authorize(request)
3031
+ );
3032
+ const latencyMs = Date.now() - startedAt;
3033
+ const normalized = this.withRouting(result, route, request);
3034
+ this.transactionProviderIndex.set(normalized.id, route.provider);
3035
+ this.queueTransactionReport({
3036
+ id: normalized.id,
3037
+ provider: normalized.provider,
3038
+ providerId: normalized.providerId,
3039
+ status: normalized.status,
3040
+ amount: normalized.amount,
3041
+ currency: normalized.currency,
3042
+ country: request.customer?.address?.country,
3043
+ paymentMethod: normalized.paymentMethod.type,
3044
+ cardBin: this.extractCardBin(request),
3045
+ cardBrand: normalized.paymentMethod.brand,
3046
+ latencyMs,
3047
+ routingSource: normalized.routing.source,
3048
+ routingDecisionId: route.decisionId,
3049
+ idempotencyKey: request.idempotencyKey,
3050
+ timestamp: normalized.createdAt
3051
+ });
3052
+ return normalized;
3053
+ });
3054
+ }
3055
+ async capture(request) {
3056
+ return this.executeIdempotentOperation("capture", request, async () => {
3057
+ const provider = this.resolveProviderForTransaction(
3058
+ request.transactionId
3059
+ );
3060
+ const adapter = this.getAdapter(provider);
3061
+ const startedAt = Date.now();
3062
+ const result = await this.wrapProviderCall(
3063
+ provider,
3064
+ "capture",
3065
+ () => adapter.capture(request)
3066
+ );
3067
+ const latencyMs = Date.now() - startedAt;
3068
+ const normalized = this.withRouting(result, {
3069
+ provider,
3070
+ source: "local",
3071
+ reason: "transaction provider lookup"
3072
+ });
3073
+ this.transactionProviderIndex.set(normalized.id, provider);
3074
+ this.queueTransactionReport({
3075
+ id: normalized.id,
3076
+ provider: normalized.provider,
3077
+ providerId: normalized.providerId,
3078
+ status: normalized.status,
3079
+ amount: normalized.amount,
3080
+ currency: normalized.currency,
3081
+ paymentMethod: normalized.paymentMethod.type,
3082
+ cardBrand: normalized.paymentMethod.brand,
3083
+ latencyMs,
3084
+ routingSource: normalized.routing.source,
3085
+ idempotencyKey: request.idempotencyKey,
3086
+ timestamp: normalized.createdAt
3087
+ });
3088
+ return normalized;
3089
+ });
3090
+ }
3091
+ async refund(request) {
3092
+ return this.executeIdempotentOperation("refund", request, async () => {
3093
+ const provider = this.resolveProviderForTransaction(
3094
+ request.transactionId
3095
+ );
3096
+ const adapter = this.getAdapter(provider);
3097
+ const startedAt = Date.now();
3098
+ const result = await this.wrapProviderCall(
3099
+ provider,
3100
+ "refund",
3101
+ () => adapter.refund(request)
3102
+ );
3103
+ const latencyMs = Date.now() - startedAt;
3104
+ this.queueTransactionReport({
3105
+ id: result.id,
3106
+ provider: result.provider,
3107
+ providerId: result.providerId,
3108
+ status: result.status,
3109
+ amount: result.amount,
3110
+ currency: result.currency,
3111
+ latencyMs,
3112
+ idempotencyKey: request.idempotencyKey,
3113
+ timestamp: result.createdAt
3114
+ });
3115
+ return result;
3116
+ });
3117
+ }
3118
+ async void(request) {
3119
+ return this.executeIdempotentOperation("void", request, async () => {
3120
+ const provider = this.resolveProviderForTransaction(
3121
+ request.transactionId
3122
+ );
3123
+ const adapter = this.getAdapter(provider);
3124
+ const result = await this.wrapProviderCall(
3125
+ provider,
3126
+ "void",
3127
+ () => adapter.void(request)
3128
+ );
3129
+ this.queueTransactionReport({
3130
+ id: result.id,
3131
+ provider: result.provider,
3132
+ status: result.status,
3133
+ amount: 0,
3134
+ currency: "N/A",
3135
+ idempotencyKey: request.idempotencyKey,
3136
+ timestamp: result.createdAt
3137
+ });
3138
+ return result;
3139
+ });
3140
+ }
3141
+ async getStatus(transactionId) {
3142
+ const provider = this.resolveProviderForTransaction(transactionId);
3143
+ const adapter = this.getAdapter(provider);
3144
+ const startedAt = Date.now();
3145
+ const status = await this.wrapProviderCall(
3146
+ provider,
3147
+ "getStatus",
3148
+ () => adapter.getStatus(transactionId)
3149
+ );
3150
+ const latencyMs = Date.now() - startedAt;
3151
+ this.transactionProviderIndex.set(status.id, provider);
3152
+ this.queueTransactionReport({
3153
+ id: status.id,
3154
+ provider: status.provider,
3155
+ providerId: status.providerId,
3156
+ status: status.status,
3157
+ amount: status.amount,
3158
+ currency: status.currency,
3159
+ latencyMs,
3160
+ timestamp: status.updatedAt
3161
+ });
3162
+ return status;
3163
+ }
3164
+ async listPaymentMethods(country, currency) {
3165
+ const methods = await Promise.all(
3166
+ this.providerOrder.map(async (provider) => {
3167
+ const adapter = this.getAdapter(provider);
3168
+ const providerMethods = await this.wrapProviderCall(
3169
+ provider,
3170
+ "listPaymentMethods",
3171
+ () => adapter.listPaymentMethods(country, currency)
3172
+ );
3173
+ return providerMethods.map((method) => ({
3174
+ ...method,
3175
+ provider: method.provider || provider
3176
+ }));
3177
+ })
3178
+ );
3179
+ return methods.flat();
3180
+ }
3181
+ async handleWebhook(provider, payload, headers) {
3182
+ const adapter = this.getAdapter(provider);
3183
+ if (adapter.handleWebhook) {
3184
+ const handler = adapter.handleWebhook;
3185
+ const event2 = await this.wrapProviderCall(
3186
+ provider,
3187
+ "handleWebhook",
3188
+ () => Promise.resolve(handler.call(adapter, payload, headers))
3189
+ );
3190
+ if (event2.transactionId) {
3191
+ this.transactionProviderIndex.set(event2.transactionId, provider);
3192
+ }
3193
+ this.queueWebhookEvent(event2);
3194
+ return event2;
3195
+ }
3196
+ const parsedPayload = this.parseWebhookPayload(payload);
3197
+ const event = normalizeWebhookEvent(provider, parsedPayload, payload);
3198
+ if (event.transactionId) {
3199
+ this.transactionProviderIndex.set(event.transactionId, provider);
3200
+ }
3201
+ this.queueWebhookEvent(event);
3202
+ return event;
3203
+ }
3204
+ async resolveProviderForCharge(request) {
3205
+ if (request.routing?.provider) {
3206
+ if (request.routing.exclude?.includes(request.routing.provider)) {
3207
+ throw new VaultRoutingError(
3208
+ "Forced provider is listed in routing exclusions.",
3209
+ {
3210
+ code: "ROUTING_PROVIDER_EXCLUDED",
3211
+ context: {
3212
+ provider: request.routing.provider
3213
+ }
3214
+ }
3215
+ );
3216
+ }
3217
+ this.getAdapter(request.routing.provider);
3218
+ return {
3219
+ provider: request.routing.provider,
3220
+ source: "local",
3221
+ reason: "forced provider"
3222
+ };
3223
+ }
3224
+ const platformDecision = await this.resolveProviderFromPlatform(request);
3225
+ if (platformDecision) {
3226
+ return platformDecision;
3227
+ }
3228
+ const context = {
3229
+ currency: request.currency.toUpperCase(),
3230
+ country: request.customer?.address?.country?.toUpperCase(),
3231
+ paymentMethod: request.paymentMethod.type,
3232
+ amount: request.amount,
3233
+ metadata: request.metadata,
3234
+ exclude: request.routing?.exclude
3235
+ };
3236
+ const decision = this.router?.decide(context);
3237
+ if (decision) {
3238
+ this.getAdapter(decision.provider);
3239
+ return {
3240
+ provider: decision.provider,
3241
+ source: "local",
3242
+ reason: decision.reason
3243
+ };
3244
+ }
3245
+ const fallback = this.providerOrder.find(
3246
+ (provider) => !request.routing?.exclude?.includes(provider)
3247
+ );
3248
+ if (!fallback) {
3249
+ throw new VaultRoutingError(
3250
+ "No eligible provider found after exclusions."
3251
+ );
3252
+ }
3253
+ return {
3254
+ provider: fallback,
3255
+ source: "local",
3256
+ reason: "fallback provider"
3257
+ };
3258
+ }
3259
+ async resolveProviderFromPlatform(request) {
3260
+ if (!this.platformConnector) {
3261
+ return null;
3262
+ }
3263
+ try {
3264
+ const decision = await this.platformConnector.decideRouting({
3265
+ currency: request.currency,
3266
+ country: request.customer?.address?.country,
3267
+ amount: request.amount,
3268
+ paymentMethod: request.paymentMethod.type,
3269
+ cardBin: this.extractCardBin(request),
3270
+ metadata: request.metadata
3271
+ });
3272
+ if (!decision?.provider) {
3273
+ return null;
3274
+ }
3275
+ if (request.routing?.exclude?.includes(decision.provider)) {
3276
+ return null;
3277
+ }
3278
+ this.getAdapter(decision.provider);
3279
+ return {
3280
+ provider: decision.provider,
3281
+ source: "platform",
3282
+ reason: decision.reason ?? "platform routing decision",
3283
+ decisionId: decision.decisionId
3284
+ };
3285
+ } catch (error) {
3286
+ this.config.logging?.logger?.warn(
3287
+ "Platform routing unavailable. Falling back to local routing.",
3288
+ {
3289
+ operation: "resolveProviderForCharge",
3290
+ cause: error instanceof Error ? error.message : String(error)
3291
+ }
3292
+ );
3293
+ return null;
3294
+ }
3295
+ }
3296
+ resolveProviderForTransaction(transactionId) {
3297
+ const mappedProvider = this.transactionProviderIndex.get(transactionId);
3298
+ if (mappedProvider) {
3299
+ return mappedProvider;
3300
+ }
3301
+ const fallbackProvider = this.providerOrder[0];
3302
+ if (!fallbackProvider) {
3303
+ throw new VaultRoutingError(
3304
+ "No configured providers are available for transaction lookup."
3305
+ );
3306
+ }
3307
+ return fallbackProvider;
3308
+ }
3309
+ getAdapter(provider) {
3310
+ const adapter = this.adapters.get(provider);
3311
+ if (!adapter) {
3312
+ throw new VaultRoutingError("Provider is not configured or enabled.", {
3313
+ code: "ROUTING_PROVIDER_UNAVAILABLE",
3314
+ context: {
3315
+ provider
3316
+ }
3317
+ });
3318
+ }
3319
+ return adapter;
3320
+ }
3321
+ withRouting(result, route, request) {
3322
+ return {
3323
+ ...result,
3324
+ provider: result.provider || route.provider,
3325
+ metadata: {
3326
+ ...request?.metadata ?? {},
3327
+ ...result.metadata
3328
+ },
3329
+ routing: {
3330
+ source: route.source,
3331
+ reason: route.reason
3332
+ },
3333
+ providerMetadata: result.providerMetadata ?? {}
3334
+ };
3335
+ }
3336
+ parseWebhookPayload(payload) {
3337
+ const raw = typeof payload === "string" ? payload : payload.toString("utf-8");
3338
+ try {
3339
+ const parsed = JSON.parse(raw);
3340
+ return parsed;
3341
+ } catch {
3342
+ return {
3343
+ data: {
3344
+ payload: raw
3345
+ }
3346
+ };
3347
+ }
3348
+ }
3349
+ extractCardBin(request) {
3350
+ if (request.paymentMethod.type === "card" && "number" in request.paymentMethod) {
3351
+ const digits = request.paymentMethod.number.replace(/\D/g, "");
3352
+ if (digits.length >= 6) {
3353
+ return digits.slice(0, 6);
3354
+ }
3355
+ }
3356
+ return void 0;
3357
+ }
3358
+ queueTransactionReport(report) {
3359
+ if (!this.platformConnector) {
3360
+ return;
3361
+ }
3362
+ this.platformConnector.queueTransactionReport(report);
3363
+ }
3364
+ queueWebhookEvent(event) {
3365
+ if (!this.platformConnector) {
3366
+ return;
3367
+ }
3368
+ this.platformConnector.queueWebhookEvent({
3369
+ id: event.id,
3370
+ type: event.type,
3371
+ provider: event.provider,
3372
+ transactionId: event.transactionId,
3373
+ providerEventId: event.providerEventId,
3374
+ data: event.data,
3375
+ timestamp: event.timestamp
3376
+ });
3377
+ }
3378
+ async executeIdempotentOperation(operation, request, execute) {
3379
+ const key = request.idempotencyKey;
3380
+ if (!key) {
3381
+ return execute();
3382
+ }
3383
+ await this.idempotencyStore.clearExpired();
3384
+ const payloadHash = hashIdempotencyPayload({
3385
+ operation,
3386
+ request
3387
+ });
3388
+ const existingRecord = await this.idempotencyStore.get(key);
3389
+ if (existingRecord) {
3390
+ if (existingRecord.payloadHash !== payloadHash) {
3391
+ throw new VaultIdempotencyConflictError(
3392
+ "Idempotency key was reused with a different payload.",
3393
+ {
3394
+ operation,
3395
+ key
3396
+ }
3397
+ );
3398
+ }
3399
+ return existingRecord.result;
3400
+ }
3401
+ const result = await execute();
3402
+ await this.idempotencyStore.set({
3403
+ key,
3404
+ payloadHash,
3405
+ result,
3406
+ expiresAt: Date.now() + this.idempotencyTtlMs
3407
+ });
3408
+ return result;
3409
+ }
3410
+ async wrapProviderCall(provider, operation, execute) {
3411
+ try {
3412
+ return await execute();
3413
+ } catch (error) {
3414
+ if (error instanceof VaultError) {
3415
+ throw error;
3416
+ }
3417
+ throw mapProviderError(error, {
3418
+ provider,
3419
+ operation
3420
+ });
3421
+ }
3422
+ }
3423
+ };
3424
+
3425
+ // src/testing/adapter-compliance.ts
3426
+ function isRecord(value) {
3427
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3428
+ }
3429
+ function isNonEmptyString(value) {
3430
+ return typeof value === "string" && value.trim().length > 0;
3431
+ }
3432
+ function isNumber(value) {
3433
+ return typeof value === "number" && Number.isFinite(value);
3434
+ }
3435
+ var PAYMENT_STATUSES = /* @__PURE__ */ new Set([
3436
+ "completed",
3437
+ "pending",
3438
+ "requires_action",
3439
+ "declined",
3440
+ "failed",
3441
+ "cancelled",
3442
+ "authorized"
3443
+ ]);
3444
+ var REFUND_STATUSES = /* @__PURE__ */ new Set(["completed", "pending", "failed"]);
3445
+ var VOID_STATUSES = /* @__PURE__ */ new Set(["completed", "failed"]);
3446
+ var ROUTING_SOURCES = /* @__PURE__ */ new Set(["local", "platform"]);
3447
+ var AdapterComplianceError = class extends Error {
3448
+ operation;
3449
+ field;
3450
+ constructor(operation, message, field) {
3451
+ super(`[${operation}] ${message}`);
3452
+ this.name = "AdapterComplianceError";
3453
+ this.operation = operation;
3454
+ this.field = field;
3455
+ }
3456
+ };
3457
+ function assertCondition(condition, operation, message, field) {
3458
+ if (!condition) {
3459
+ throw new AdapterComplianceError(operation, message, field);
3460
+ }
3461
+ }
3462
+ function validateStringArray(operation, field, value) {
3463
+ assertCondition(
3464
+ Array.isArray(value),
3465
+ operation,
3466
+ `${field} must be an array.`,
3467
+ field
3468
+ );
3469
+ assertCondition(
3470
+ value.every(isNonEmptyString),
3471
+ operation,
3472
+ `${field} must contain non-empty strings.`,
3473
+ field
3474
+ );
3475
+ }
3476
+ function validatePaymentResult(value, operation, expectedProvider) {
3477
+ assertCondition(isRecord(value), operation, "Result must be an object.");
3478
+ assertCondition(
3479
+ isNonEmptyString(value.id),
3480
+ operation,
3481
+ "id must be a non-empty string.",
3482
+ "id"
3483
+ );
3484
+ assertCondition(
3485
+ isNonEmptyString(value.provider),
3486
+ operation,
3487
+ "provider must be a non-empty string.",
3488
+ "provider"
3489
+ );
3490
+ if (expectedProvider) {
3491
+ assertCondition(
3492
+ value.provider === expectedProvider,
3493
+ operation,
3494
+ `provider must equal "${expectedProvider}".`,
3495
+ "provider"
3496
+ );
3497
+ }
3498
+ assertCondition(
3499
+ isNonEmptyString(value.providerId),
3500
+ operation,
3501
+ "providerId must be a non-empty string.",
3502
+ "providerId"
3503
+ );
3504
+ assertCondition(
3505
+ isNumber(value.amount),
3506
+ operation,
3507
+ "amount must be a number.",
3508
+ "amount"
3509
+ );
3510
+ assertCondition(
3511
+ isNonEmptyString(value.currency),
3512
+ operation,
3513
+ "currency must be a non-empty string.",
3514
+ "currency"
3515
+ );
3516
+ assertCondition(
3517
+ PAYMENT_STATUSES.has(value.status),
3518
+ operation,
3519
+ "status must be a canonical payment status.",
3520
+ "status"
3521
+ );
3522
+ assertCondition(
3523
+ isRecord(value.paymentMethod),
3524
+ operation,
3525
+ "paymentMethod must be an object.",
3526
+ "paymentMethod"
3527
+ );
3528
+ assertCondition(
3529
+ isNonEmptyString(value.paymentMethod.type),
3530
+ operation,
3531
+ "paymentMethod.type must be a non-empty string.",
3532
+ "paymentMethod.type"
3533
+ );
3534
+ assertCondition(
3535
+ isRecord(value.routing),
3536
+ operation,
3537
+ "routing must be an object.",
3538
+ "routing"
3539
+ );
3540
+ assertCondition(
3541
+ ROUTING_SOURCES.has(value.routing.source),
3542
+ operation,
3543
+ 'routing.source must be "local" or "platform".',
3544
+ "routing.source"
3545
+ );
3546
+ assertCondition(
3547
+ isNonEmptyString(value.routing.reason),
3548
+ operation,
3549
+ "routing.reason must be a non-empty string.",
3550
+ "routing.reason"
3551
+ );
3552
+ assertCondition(
3553
+ isNonEmptyString(value.createdAt),
3554
+ operation,
3555
+ "createdAt must be a non-empty string.",
3556
+ "createdAt"
3557
+ );
3558
+ assertCondition(
3559
+ isRecord(value.metadata),
3560
+ operation,
3561
+ "metadata must be an object.",
3562
+ "metadata"
3563
+ );
3564
+ assertCondition(
3565
+ isRecord(value.providerMetadata),
3566
+ operation,
3567
+ "providerMetadata must be an object.",
3568
+ "providerMetadata"
3569
+ );
3570
+ }
3571
+ function validateRefundResult(value, operation = "refund", expectedProvider) {
3572
+ assertCondition(isRecord(value), operation, "Result must be an object.");
3573
+ assertCondition(
3574
+ isNonEmptyString(value.id),
3575
+ operation,
3576
+ "id must be a non-empty string.",
3577
+ "id"
3578
+ );
3579
+ assertCondition(
3580
+ isNonEmptyString(value.transactionId),
3581
+ operation,
3582
+ "transactionId must be a non-empty string.",
3583
+ "transactionId"
3584
+ );
3585
+ assertCondition(
3586
+ REFUND_STATUSES.has(value.status),
3587
+ operation,
3588
+ 'status must be "completed", "pending", or "failed".',
3589
+ "status"
3590
+ );
3591
+ assertCondition(
3592
+ isNumber(value.amount),
3593
+ operation,
3594
+ "amount must be a number.",
3595
+ "amount"
3596
+ );
3597
+ assertCondition(
3598
+ isNonEmptyString(value.currency),
3599
+ operation,
3600
+ "currency must be a non-empty string.",
3601
+ "currency"
3602
+ );
3603
+ assertCondition(
3604
+ isNonEmptyString(value.provider),
3605
+ operation,
3606
+ "provider must be a non-empty string.",
3607
+ "provider"
3608
+ );
3609
+ if (expectedProvider) {
3610
+ assertCondition(
3611
+ value.provider === expectedProvider,
3612
+ operation,
3613
+ `provider must equal "${expectedProvider}".`,
3614
+ "provider"
3615
+ );
3616
+ }
3617
+ assertCondition(
3618
+ isNonEmptyString(value.providerId),
3619
+ operation,
3620
+ "providerId must be a non-empty string.",
3621
+ "providerId"
3622
+ );
3623
+ assertCondition(
3624
+ isNonEmptyString(value.createdAt),
3625
+ operation,
3626
+ "createdAt must be a non-empty string.",
3627
+ "createdAt"
3628
+ );
3629
+ }
3630
+ function validateVoidResult(value, operation = "void", expectedProvider) {
3631
+ assertCondition(isRecord(value), operation, "Result must be an object.");
3632
+ assertCondition(
3633
+ isNonEmptyString(value.id),
3634
+ operation,
3635
+ "id must be a non-empty string.",
3636
+ "id"
3637
+ );
3638
+ assertCondition(
3639
+ isNonEmptyString(value.transactionId),
3640
+ operation,
3641
+ "transactionId must be a non-empty string.",
3642
+ "transactionId"
3643
+ );
3644
+ assertCondition(
3645
+ VOID_STATUSES.has(value.status),
3646
+ operation,
3647
+ 'status must be "completed" or "failed".',
3648
+ "status"
3649
+ );
3650
+ assertCondition(
3651
+ isNonEmptyString(value.provider),
3652
+ operation,
3653
+ "provider must be a non-empty string.",
3654
+ "provider"
3655
+ );
3656
+ if (expectedProvider) {
3657
+ assertCondition(
3658
+ value.provider === expectedProvider,
3659
+ operation,
3660
+ `provider must equal "${expectedProvider}".`,
3661
+ "provider"
3662
+ );
3663
+ }
3664
+ assertCondition(
3665
+ isNonEmptyString(value.createdAt),
3666
+ operation,
3667
+ "createdAt must be a non-empty string.",
3668
+ "createdAt"
3669
+ );
3670
+ }
3671
+ function validateTransactionStatus(value, operation = "getStatus", expectedProvider) {
3672
+ assertCondition(isRecord(value), operation, "Result must be an object.");
3673
+ assertCondition(
3674
+ isNonEmptyString(value.id),
3675
+ operation,
3676
+ "id must be a non-empty string.",
3677
+ "id"
3678
+ );
3679
+ assertCondition(
3680
+ isNonEmptyString(value.provider),
3681
+ operation,
3682
+ "provider must be a non-empty string.",
3683
+ "provider"
3684
+ );
3685
+ if (expectedProvider) {
3686
+ assertCondition(
3687
+ value.provider === expectedProvider,
3688
+ operation,
3689
+ `provider must equal "${expectedProvider}".`,
3690
+ "provider"
3691
+ );
3692
+ }
3693
+ assertCondition(
3694
+ isNonEmptyString(value.providerId),
3695
+ operation,
3696
+ "providerId must be a non-empty string.",
3697
+ "providerId"
3698
+ );
3699
+ assertCondition(
3700
+ isNumber(value.amount),
3701
+ operation,
3702
+ "amount must be a number.",
3703
+ "amount"
3704
+ );
3705
+ assertCondition(
3706
+ isNonEmptyString(value.currency),
3707
+ operation,
3708
+ "currency must be a non-empty string.",
3709
+ "currency"
3710
+ );
3711
+ assertCondition(
3712
+ PAYMENT_STATUSES.has(value.status),
3713
+ operation,
3714
+ "status must be a canonical payment status.",
3715
+ "status"
3716
+ );
3717
+ assertCondition(
3718
+ Array.isArray(value.history),
3719
+ operation,
3720
+ "history must be an array.",
3721
+ "history"
3722
+ );
3723
+ for (const [index, item] of value.history.entries()) {
3724
+ const fieldPrefix = `history[${index}]`;
3725
+ assertCondition(
3726
+ isRecord(item),
3727
+ operation,
3728
+ `${fieldPrefix} must be an object.`,
3729
+ fieldPrefix
3730
+ );
3731
+ assertCondition(
3732
+ PAYMENT_STATUSES.has(item.status),
3733
+ operation,
3734
+ `${fieldPrefix}.status must be a canonical payment status.`,
3735
+ `${fieldPrefix}.status`
3736
+ );
3737
+ assertCondition(
3738
+ isNonEmptyString(item.timestamp),
3739
+ operation,
3740
+ `${fieldPrefix}.timestamp must be a non-empty string.`,
3741
+ `${fieldPrefix}.timestamp`
3742
+ );
3743
+ }
3744
+ assertCondition(
3745
+ isNonEmptyString(value.updatedAt),
3746
+ operation,
3747
+ "updatedAt must be a non-empty string.",
3748
+ "updatedAt"
3749
+ );
3750
+ }
3751
+ function validatePaymentMethods(methods, operation = "listPaymentMethods", expectedProvider) {
3752
+ assertCondition(
3753
+ Array.isArray(methods),
3754
+ operation,
3755
+ "Result must be an array.",
3756
+ "paymentMethods"
3757
+ );
3758
+ for (const [index, method] of methods.entries()) {
3759
+ const fieldPrefix = `paymentMethods[${index}]`;
3760
+ assertCondition(
3761
+ isRecord(method),
3762
+ operation,
3763
+ `${fieldPrefix} must be an object.`,
3764
+ fieldPrefix
3765
+ );
3766
+ assertCondition(
3767
+ isNonEmptyString(method.type),
3768
+ operation,
3769
+ `${fieldPrefix}.type must be a non-empty string.`,
3770
+ `${fieldPrefix}.type`
3771
+ );
3772
+ assertCondition(
3773
+ isNonEmptyString(method.provider),
3774
+ operation,
3775
+ `${fieldPrefix}.provider must be a non-empty string.`,
3776
+ `${fieldPrefix}.provider`
3777
+ );
3778
+ if (expectedProvider) {
3779
+ assertCondition(
3780
+ method.provider === expectedProvider,
3781
+ operation,
3782
+ `${fieldPrefix}.provider must equal "${expectedProvider}".`,
3783
+ `${fieldPrefix}.provider`
3784
+ );
3785
+ }
3786
+ assertCondition(
3787
+ isNonEmptyString(method.name),
3788
+ operation,
3789
+ `${fieldPrefix}.name must be a non-empty string.`,
3790
+ `${fieldPrefix}.name`
3791
+ );
3792
+ validateStringArray(
3793
+ operation,
3794
+ `${fieldPrefix}.currencies`,
3795
+ method.currencies
3796
+ );
3797
+ validateStringArray(
3798
+ operation,
3799
+ `${fieldPrefix}.countries`,
3800
+ method.countries
3801
+ );
3802
+ if (typeof method.minAmount !== "undefined") {
3803
+ assertCondition(
3804
+ isNumber(method.minAmount),
3805
+ operation,
3806
+ `${fieldPrefix}.minAmount must be a number when provided.`,
3807
+ `${fieldPrefix}.minAmount`
3808
+ );
3809
+ }
3810
+ if (typeof method.maxAmount !== "undefined") {
3811
+ assertCondition(
3812
+ isNumber(method.maxAmount),
3813
+ operation,
3814
+ `${fieldPrefix}.maxAmount must be a number when provided.`,
3815
+ `${fieldPrefix}.maxAmount`
3816
+ );
3817
+ }
3818
+ }
3819
+ }
3820
+ function validateWebhookEvent(event, operation = "handleWebhook", expectedProvider) {
3821
+ assertCondition(isRecord(event), operation, "Result must be an object.");
3822
+ assertCondition(
3823
+ isNonEmptyString(event.id),
3824
+ operation,
3825
+ "id must be a non-empty string.",
3826
+ "id"
3827
+ );
3828
+ assertCondition(
3829
+ isNonEmptyString(event.provider),
3830
+ operation,
3831
+ "provider must be a non-empty string.",
3832
+ "provider"
3833
+ );
3834
+ if (expectedProvider) {
3835
+ assertCondition(
3836
+ event.provider === expectedProvider,
3837
+ operation,
3838
+ `provider must equal "${expectedProvider}".`,
3839
+ "provider"
3840
+ );
3841
+ }
3842
+ assertCondition(
3843
+ isNonEmptyString(event.type),
3844
+ operation,
3845
+ "type must be a non-empty string.",
3846
+ "type"
3847
+ );
3848
+ assertCondition(
3849
+ isNonEmptyString(event.providerEventId),
3850
+ operation,
3851
+ "providerEventId must be a non-empty string.",
3852
+ "providerEventId"
3853
+ );
3854
+ assertCondition(
3855
+ isNonEmptyString(event.timestamp),
3856
+ operation,
3857
+ "timestamp must be a non-empty string.",
3858
+ "timestamp"
3859
+ );
3860
+ }
3861
+ function createAdapterComplianceHarness(adapter, options = {}) {
3862
+ const expectedProvider = options.expectedProvider ?? adapter.name;
3863
+ return {
3864
+ async charge(request) {
3865
+ const result = await adapter.charge(request);
3866
+ validatePaymentResult(result, "charge", expectedProvider);
3867
+ return result;
3868
+ },
3869
+ async authorize(request) {
3870
+ const result = await adapter.authorize(request);
3871
+ validatePaymentResult(result, "authorize", expectedProvider);
3872
+ return result;
3873
+ },
3874
+ async capture(request) {
3875
+ const result = await adapter.capture(request);
3876
+ validatePaymentResult(result, "capture", expectedProvider);
3877
+ return result;
3878
+ },
3879
+ async refund(request) {
3880
+ const result = await adapter.refund(request);
3881
+ validateRefundResult(result, "refund", expectedProvider);
3882
+ return result;
3883
+ },
3884
+ async void(request) {
3885
+ const result = await adapter.void(request);
3886
+ validateVoidResult(result, "void", expectedProvider);
3887
+ return result;
3888
+ },
3889
+ async getStatus(transactionId) {
3890
+ const result = await adapter.getStatus(transactionId);
3891
+ validateTransactionStatus(result, "getStatus", expectedProvider);
3892
+ return result;
3893
+ },
3894
+ async listPaymentMethods(country, currency) {
3895
+ const result = await adapter.listPaymentMethods(country, currency);
3896
+ validatePaymentMethods(result, "listPaymentMethods", expectedProvider);
3897
+ return result;
3898
+ },
3899
+ async handleWebhook(payload, headers) {
3900
+ if (!adapter.handleWebhook) {
3901
+ throw new AdapterComplianceError(
3902
+ "handleWebhook",
3903
+ "Adapter does not implement handleWebhook."
3904
+ );
3905
+ }
3906
+ const result = await adapter.handleWebhook(payload, headers);
3907
+ validateWebhookEvent(result, "handleWebhook", expectedProvider);
3908
+ return result;
3909
+ }
3910
+ };
3911
+ }
3912
+
3913
+ // src/testing/mock-adapter.ts
3914
+ function unsupported(method) {
3915
+ throw new Error(`MockAdapter handler not configured: ${method}`);
3916
+ }
3917
+ var DEFAULT_MOCK_METADATA = {
3918
+ supportedMethods: ["card", "bank_transfer", "wallet"],
3919
+ supportedCurrencies: ["USD", "EUR", "GBP"],
3920
+ supportedCountries: ["US", "GB", "DE"]
3921
+ };
3922
+ function hasMockAdapterOptions(value) {
3923
+ return "handlers" in value || "scenarios" in value || "name" in value || "metadata" in value;
3924
+ }
3925
+ function toScenarioQueue(scenarios) {
3926
+ if (!scenarios || scenarios.length === 0) {
3927
+ return [];
3928
+ }
3929
+ return scenarios.map((scenario) => {
3930
+ if (scenario instanceof Error) {
3931
+ return async () => Promise.reject(scenario);
3932
+ }
3933
+ if (typeof scenario === "function") {
3934
+ return async (input) => scenario(input);
3935
+ }
3936
+ return async () => scenario;
3937
+ });
3938
+ }
3939
+ var MockAdapter = class {
3940
+ static supportedMethods = DEFAULT_MOCK_METADATA.supportedMethods;
3941
+ static supportedCurrencies = DEFAULT_MOCK_METADATA.supportedCurrencies;
3942
+ static supportedCountries = DEFAULT_MOCK_METADATA.supportedCountries;
3943
+ name;
3944
+ metadata;
3945
+ handlers;
3946
+ chargeScenarios;
3947
+ authorizeScenarios;
3948
+ captureScenarios;
3949
+ refundScenarios;
3950
+ voidScenarios;
3951
+ statusScenarios;
3952
+ paymentMethodsScenarios;
3953
+ webhookScenarios;
3954
+ constructor(options = {}) {
3955
+ const normalized = hasMockAdapterOptions(options) ? options : { handlers: options };
3956
+ this.name = normalized.name ?? "mock";
3957
+ this.metadata = normalized.metadata ?? DEFAULT_MOCK_METADATA;
3958
+ this.handlers = normalized.handlers ?? {};
3959
+ this.chargeScenarios = toScenarioQueue(normalized.scenarios?.charge);
3960
+ this.authorizeScenarios = toScenarioQueue(normalized.scenarios?.authorize);
3961
+ this.captureScenarios = toScenarioQueue(normalized.scenarios?.capture);
3962
+ this.refundScenarios = toScenarioQueue(normalized.scenarios?.refund);
3963
+ this.voidScenarios = toScenarioQueue(normalized.scenarios?.void);
3964
+ this.statusScenarios = toScenarioQueue(normalized.scenarios?.getStatus);
3965
+ this.paymentMethodsScenarios = toScenarioQueue(
3966
+ normalized.scenarios?.listPaymentMethods
3967
+ );
3968
+ this.webhookScenarios = toScenarioQueue(
3969
+ normalized.scenarios?.handleWebhook
3970
+ );
3971
+ }
3972
+ enqueue(method, scenario) {
3973
+ switch (method) {
3974
+ case "charge":
3975
+ this.chargeScenarios.push(
3976
+ ...toScenarioQueue([
3977
+ scenario
3978
+ ])
3979
+ );
3980
+ break;
3981
+ case "authorize":
3982
+ this.authorizeScenarios.push(
3983
+ ...toScenarioQueue([
3984
+ scenario
3985
+ ])
3986
+ );
3987
+ break;
3988
+ case "capture":
3989
+ this.captureScenarios.push(
3990
+ ...toScenarioQueue([
3991
+ scenario
3992
+ ])
3993
+ );
3994
+ break;
3995
+ case "refund":
3996
+ this.refundScenarios.push(
3997
+ ...toScenarioQueue([
3998
+ scenario
3999
+ ])
4000
+ );
4001
+ break;
4002
+ case "void":
4003
+ this.voidScenarios.push(
4004
+ ...toScenarioQueue([
4005
+ scenario
4006
+ ])
4007
+ );
4008
+ break;
4009
+ case "getStatus":
4010
+ this.statusScenarios.push(
4011
+ ...toScenarioQueue([
4012
+ scenario
4013
+ ])
4014
+ );
4015
+ break;
4016
+ case "listPaymentMethods":
4017
+ this.paymentMethodsScenarios.push(
4018
+ ...toScenarioQueue([
4019
+ scenario
4020
+ ])
4021
+ );
4022
+ break;
4023
+ case "handleWebhook":
4024
+ this.webhookScenarios.push(
4025
+ ...toScenarioQueue([
4026
+ scenario
4027
+ ])
4028
+ );
4029
+ break;
4030
+ default:
4031
+ unsupported(method);
4032
+ }
4033
+ return this;
4034
+ }
4035
+ async charge(request) {
4036
+ const scenario = this.chargeScenarios.shift();
4037
+ if (scenario) {
4038
+ return scenario(request);
4039
+ }
4040
+ const handler = this.handlers.charge;
4041
+ if (!handler) {
4042
+ unsupported("charge");
4043
+ }
4044
+ return handler(request);
4045
+ }
4046
+ async authorize(request) {
4047
+ const scenario = this.authorizeScenarios.shift();
4048
+ if (scenario) {
4049
+ return scenario(request);
4050
+ }
4051
+ const handler = this.handlers.authorize;
4052
+ if (!handler) {
4053
+ unsupported("authorize");
4054
+ }
4055
+ return handler(request);
4056
+ }
4057
+ async capture(request) {
4058
+ const scenario = this.captureScenarios.shift();
4059
+ if (scenario) {
4060
+ return scenario(request);
4061
+ }
4062
+ const handler = this.handlers.capture;
4063
+ if (!handler) {
4064
+ unsupported("capture");
4065
+ }
4066
+ return handler(request);
4067
+ }
4068
+ async refund(request) {
4069
+ const scenario = this.refundScenarios.shift();
4070
+ if (scenario) {
4071
+ return scenario(request);
4072
+ }
4073
+ const handler = this.handlers.refund;
4074
+ if (!handler) {
4075
+ unsupported("refund");
4076
+ }
4077
+ return handler(request);
4078
+ }
4079
+ async void(request) {
4080
+ const scenario = this.voidScenarios.shift();
4081
+ if (scenario) {
4082
+ return scenario(request);
4083
+ }
4084
+ const handler = this.handlers.void;
4085
+ if (!handler) {
4086
+ unsupported("void");
4087
+ }
4088
+ return handler(request);
4089
+ }
4090
+ async getStatus(transactionId) {
4091
+ const scenario = this.statusScenarios.shift();
4092
+ if (scenario) {
4093
+ return scenario(transactionId);
4094
+ }
4095
+ const handler = this.handlers.getStatus;
4096
+ if (!handler) {
4097
+ unsupported("getStatus");
4098
+ }
4099
+ return handler(transactionId);
4100
+ }
4101
+ async listPaymentMethods(country, currency) {
4102
+ const scenario = this.paymentMethodsScenarios.shift();
4103
+ if (scenario) {
4104
+ return scenario({ country, currency });
4105
+ }
4106
+ const handler = this.handlers.listPaymentMethods;
4107
+ if (!handler) {
4108
+ unsupported("listPaymentMethods");
4109
+ }
4110
+ return handler(country, currency);
4111
+ }
4112
+ async handleWebhook(payload, headers) {
4113
+ const scenario = this.webhookScenarios.shift();
4114
+ if (scenario) {
4115
+ return scenario({ payload, headers });
4116
+ }
4117
+ const handler = this.handlers.handleWebhook;
4118
+ if (!handler) {
4119
+ unsupported("handleWebhook");
4120
+ }
4121
+ return handler(payload, headers);
4122
+ }
4123
+ };
4124
+
4125
+ // src/testing/webhook-helper.ts
4126
+ import { createHmac as createHmac2 } from "crypto";
4127
+ function toPayloadString(payload) {
4128
+ return typeof payload === "string" ? payload : JSON.stringify(payload);
4129
+ }
4130
+ function createHmacDigest2(algorithm, secret, content) {
4131
+ return createHmac2(algorithm, secret).update(content).digest("hex");
4132
+ }
4133
+ function createSignedWebhookPayload(payload, secret, options = {}) {
4134
+ const serialized = toPayloadString(payload);
4135
+ const provider = options.provider ?? "generic";
4136
+ switch (provider) {
4137
+ case "stripe": {
4138
+ const timestamp = String(
4139
+ options.timestamp ?? Math.floor(Date.now() / 1e3)
4140
+ );
4141
+ const signature = createHmacDigest2(
4142
+ "sha256",
4143
+ secret,
4144
+ `${timestamp}.${serialized}`
4145
+ );
4146
+ const headerName = options.headerName ?? "stripe-signature";
4147
+ return {
4148
+ payload: serialized,
4149
+ headers: {
4150
+ [headerName]: `t=${timestamp},v1=${signature}`
4151
+ }
4152
+ };
4153
+ }
4154
+ case "dlocal": {
4155
+ const signature = createHmacDigest2("sha256", secret, serialized);
4156
+ const headerName = options.headerName ?? "x-dlocal-signature";
4157
+ return {
4158
+ payload: serialized,
4159
+ headers: {
4160
+ [headerName]: signature
4161
+ }
4162
+ };
4163
+ }
4164
+ case "paystack": {
4165
+ const signature = createHmacDigest2("sha512", secret, serialized);
4166
+ const headerName = options.headerName ?? "x-paystack-signature";
4167
+ return {
4168
+ payload: serialized,
4169
+ headers: {
4170
+ [headerName]: signature
4171
+ }
4172
+ };
4173
+ }
4174
+ case "generic": {
4175
+ const signature = createHmacDigest2("sha256", secret, serialized);
4176
+ const headerName = options.headerName ?? "x-vault-test-signature";
4177
+ return {
4178
+ payload: serialized,
4179
+ headers: {
4180
+ [headerName]: signature
4181
+ }
4182
+ };
4183
+ }
4184
+ default: {
4185
+ const unsupportedProvider = provider;
4186
+ throw new Error(
4187
+ `Unsupported webhook signing provider: ${unsupportedProvider}`
4188
+ );
4189
+ }
4190
+ }
4191
+ }
4192
+ function createStripeSignedWebhookPayload(payload, secret, options = {}) {
4193
+ return createSignedWebhookPayload(payload, secret, {
4194
+ ...options,
4195
+ provider: "stripe"
4196
+ });
4197
+ }
4198
+ function createDLocalSignedWebhookPayload(payload, secret, options = {}) {
4199
+ return createSignedWebhookPayload(payload, secret, {
4200
+ ...options,
4201
+ provider: "dlocal"
4202
+ });
4203
+ }
4204
+ function createPaystackSignedWebhookPayload(payload, secret, options = {}) {
4205
+ return createSignedWebhookPayload(payload, secret, {
4206
+ ...options,
4207
+ provider: "paystack"
4208
+ });
4209
+ }
4210
+ export {
4211
+ AdapterComplianceError,
4212
+ BatchBuffer,
4213
+ DEFAULT_IDEMPOTENCY_TTL_MS,
4214
+ DEFAULT_VAULT_EVENT_TYPES,
4215
+ DLocalAdapter,
4216
+ MemoryIdempotencyStore,
4217
+ MockAdapter,
4218
+ PaystackAdapter,
4219
+ PlatformConnector,
4220
+ Router,
4221
+ StripeAdapter,
4222
+ VAULT_ERROR_CODE_DEFINITIONS,
4223
+ VaultClient,
4224
+ VaultConfigError,
4225
+ VaultError,
4226
+ VaultIdempotencyConflictError,
4227
+ VaultNetworkError,
4228
+ VaultProviderError,
4229
+ VaultRoutingError,
4230
+ WebhookVerificationError,
4231
+ buildVaultErrorDocsUrl,
4232
+ createAdapterComplianceHarness,
4233
+ createDLocalSignedWebhookPayload,
4234
+ createPaystackSignedWebhookPayload,
4235
+ createSignedWebhookPayload,
4236
+ createStripeSignedWebhookPayload,
4237
+ getVaultErrorCodeDefinition,
4238
+ hashIdempotencyPayload,
4239
+ isProviderErrorHint,
4240
+ mapProviderError,
4241
+ normalizeWebhookEvent,
4242
+ ruleMatchesContext,
4243
+ validatePaymentMethods,
4244
+ validatePaymentResult,
4245
+ validateRefundResult,
4246
+ validateTransactionStatus,
4247
+ validateVoidResult,
4248
+ validateWebhookEvent
4249
+ };
4250
+ //# sourceMappingURL=index.js.map