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