@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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.cjs +4314 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +814 -0
- package/dist/index.d.ts +814 -0
- package/dist/index.js +4250 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
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
|