@siyegs/pay-kit 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Iyegere Success Karboloo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # pay-kit
2
+
3
+ > One typed SDK for African payment rails. Unified **Paystack** and **Flutterwave**: initialize a payment, verify it, and handle signature-verified webhooks - with the same API for both.
4
+
5
+ [![CI](https://github.com/siyegs/pay-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/siyegs/pay-kit/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/badge/npm-%40siyegs%2Fpay--kit-cb3837)](https://www.npmjs.com/package/@siyegs/pay-kit)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
8
+ [![types](https://img.shields.io/badge/types-included-3178c6)](./src/types.ts)
9
+
10
+ Most serious African products integrate **both** Paystack and Flutterwave - for coverage, redundancy, and better rates. But their APIs, webhook signatures, error shapes, and currency units all differ, so teams re-write the same fragile glue every time. `pay-kit` gives you **one typed interface** over both.
11
+
12
+ ## Why
13
+
14
+ - **One API, two providers.** Swap `provider: "paystack"` for `"flutterwave"` - your code doesn't change.
15
+ - **Subunits everywhere.** Amounts are always in the smallest unit (kobo/cents), Stripe-style, to kill float-rounding bugs. pay-kit converts per provider.
16
+ - **Signature-verified webhooks.** Paystack HMAC-SHA512 and Flutterwave `verif-hash`, both normalized to the same event shape.
17
+ - **Typed end to end.** Full TypeScript types, one `PayKitError` with a machine-readable `code`.
18
+ - **Tiny + dependency-free.** Uses native `fetch` and `node:crypto`. ESM + CJS.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ bun add @siyegs/pay-kit
24
+ # or: npm install @siyegs/pay-kit / pnpm add @siyegs/pay-kit
25
+ ```
26
+
27
+ Runs on **Bun** and **Node >= 18** (both provide global `fetch` and `node:crypto`). Keep your secret key **server-side only**.
28
+
29
+ ## Quick start
30
+
31
+ ```ts
32
+ import { createPayClient } from "@siyegs/pay-kit";
33
+
34
+ const pay = createPayClient({
35
+ provider: "paystack", // or "flutterwave"
36
+ secretKey: process.env.PAYSTACK_SECRET_KEY!,
37
+ });
38
+
39
+ // 1. Start a payment (amount in subunits: 500000 = NGN 5,000.00)
40
+ const { authorizationUrl, reference } = await pay.initialize({
41
+ amount: 500000,
42
+ email: "customer@example.com",
43
+ currency: "NGN",
44
+ metadata: { orderId: "order_123" },
45
+ });
46
+ // -> redirect the customer to `authorizationUrl`, persist `reference`
47
+
48
+ // 2. Verify after the redirect / callback
49
+ const result = await pay.verify(reference);
50
+ if (result.status === "success") {
51
+ // fulfill the order
52
+ }
53
+ ```
54
+
55
+ ## Webhooks
56
+
57
+ Verify the raw request body against its signature header and get a normalized event. **Always verify before trusting a webhook.**
58
+
59
+ ```ts
60
+ // Express example
61
+ app.post("/webhooks/pay", express.raw({ type: "*/*" }), (req, res) => {
62
+ const signature =
63
+ req.header("x-paystack-signature") ?? req.header("verif-hash") ?? "";
64
+ try {
65
+ const event = pay.webhooks.construct(req.body.toString("utf8"), signature);
66
+ if (event.type === "charge.success") {
67
+ // event.reference, event.amount (subunits), event.currency
68
+ }
69
+ res.sendStatus(200);
70
+ } catch {
71
+ res.sendStatus(400); // invalid signature -> reject
72
+ }
73
+ });
74
+ ```
75
+
76
+ - **Paystack**: signature header is `x-paystack-signature`; verification uses your `secretKey`.
77
+ - **Flutterwave**: header is `verif-hash`; pass your "Secret hash" as `webhookSecret` when creating the client.
78
+
79
+ ## Provider fallback
80
+
81
+ Try one provider, automatically fall through to the next when it is unreachable - so a Paystack outage doesn't stop you taking money.
82
+
83
+ ```ts
84
+ import { createFallbackClient } from "@siyegs/pay-kit";
85
+
86
+ const pay = createFallbackClient({
87
+ providers: [
88
+ { provider: "paystack", secretKey: process.env.PAYSTACK_SECRET_KEY! },
89
+ { provider: "flutterwave", secretKey: process.env.FLW_SECRET_KEY!, webhookSecret: process.env.FLW_HASH },
90
+ ],
91
+ });
92
+
93
+ // initialize tries Paystack, then Flutterwave on an outage
94
+ const { reference, provider } = await pay.initialize({ amount: 500000, email: "a@b.com" });
95
+
96
+ // persist BOTH reference and provider, then route the rest to that provider
97
+ const result = await pay.verify(provider, reference);
98
+ await pay.refund(provider, reference);
99
+ const event = pay.webhooks.construct(provider, rawBody, signature);
100
+ ```
101
+
102
+ - Only **outage-like** failures trigger fallback: network errors, HTTP 5xx, and 429. A 4xx (bad request, invalid key) fails fast - it would fail the same way on the next provider.
103
+ - A charge started on one provider can only be verified/refunded on that provider, so `initialize` returns which `provider` handled it. **Persist `provider` alongside `reference`.**
104
+ - Fallback is safest for *pre-charge* outages (provider unreachable). If a provider accepts the charge then the connection drops, retrying the other provider could double-charge - use idempotency at your order layer for that edge.
105
+
106
+ ## API
107
+
108
+ ### `createPayClient(config)`
109
+
110
+ | option | type | notes |
111
+ | ------------------ | ----------------------------- | -------------------------------------------------- |
112
+ | `provider` | `"paystack" \| "flutterwave"` | required |
113
+ | `secretKey` | `string` | required, server-side only |
114
+ | `webhookSecret` | `string` | required for Flutterwave webhooks (Secret hash) |
115
+ | `baseUrl` | `string` | override API base (tests/proxies) |
116
+ | `fetch` | `typeof fetch` | inject a fetch impl |
117
+ | `generateReference`| `() => string` | customize reference generation |
118
+
119
+ ### Methods
120
+
121
+ - `initialize(params) -> { reference, authorizationUrl, accessCode?, raw }`
122
+ - `verify(reference) -> { reference, status, amount, currency, paidAt?, channel?, customer?, raw }`
123
+ - `refund(reference, options?) -> { reference, status, amount?, raw }` - full refund, or partial with `options.amount` (subunits)
124
+ - `webhooks.construct(rawBody, signature) -> { type, reference, status?, amount?, currency?, raw }`
125
+
126
+ `status` is normalized to `"success" | "failed" | "pending" | "abandoned"`. Errors are thrown as `PayKitError` with `code` in `provider_error | network_error | invalid_signature | config_error | verification_failed`.
127
+
128
+ ## Roadmap
129
+
130
+ - [x] Refunds (full & partial)
131
+ - [x] **Provider fallback** (auto-retry the other provider on outage)
132
+ - [ ] Transfers / payouts
133
+ - [ ] Plans & subscriptions
134
+ - [ ] Framework adapters (NestJS, Hono, Next.js route handlers)
135
+ - [ ] Mock provider for offline development
136
+
137
+ ## Development
138
+
139
+ Built with the [Bun](https://bun.sh) toolchain.
140
+
141
+ ```bash
142
+ bun install # install deps
143
+ bun test # run the test suite (bun:test)
144
+ bun run typecheck # tsc --noEmit
145
+ bun run build # tsup -> dist (ESM + CJS + .d.ts)
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT (c) Iyegere Success Karboloo
package/dist/index.cjs ADDED
@@ -0,0 +1,487 @@
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
+ PayKitError: () => PayKitError,
24
+ createFallbackClient: () => createFallbackClient,
25
+ createPayClient: () => createPayClient,
26
+ isRetryableError: () => isRetryableError
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/client.ts
31
+ var import_node_crypto3 = require("crypto");
32
+
33
+ // src/errors.ts
34
+ function isRetryableError(err) {
35
+ if (!(err instanceof PayKitError)) return false;
36
+ if (err.code === "network_error") return true;
37
+ if (typeof err.statusCode === "number") {
38
+ return err.statusCode >= 500 || err.statusCode === 429;
39
+ }
40
+ return false;
41
+ }
42
+ var PayKitError = class extends Error {
43
+ code;
44
+ provider;
45
+ statusCode;
46
+ raw;
47
+ constructor(message, options) {
48
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
49
+ this.name = "PayKitError";
50
+ this.code = options.code;
51
+ this.provider = options.provider;
52
+ this.statusCode = options.statusCode;
53
+ this.raw = options.raw;
54
+ }
55
+ };
56
+
57
+ // src/providers/paystack.ts
58
+ var import_node_crypto2 = require("crypto");
59
+
60
+ // src/internal.ts
61
+ var import_node_crypto = require("crypto");
62
+ async function providerRequest(ctx, provider, url, init) {
63
+ let res;
64
+ try {
65
+ res = await ctx.fetch(url, {
66
+ ...init,
67
+ headers: {
68
+ Authorization: `Bearer ${ctx.secretKey}`,
69
+ "Content-Type": "application/json",
70
+ Accept: "application/json",
71
+ ...init.headers ?? {}
72
+ }
73
+ });
74
+ } catch (err) {
75
+ throw new PayKitError(`Network error calling ${provider}`, {
76
+ code: "network_error",
77
+ provider,
78
+ cause: err
79
+ });
80
+ }
81
+ let body = {};
82
+ try {
83
+ body = await res.json();
84
+ } catch {
85
+ body = {};
86
+ }
87
+ if (!res.ok || body.status === false || body.status === "error") {
88
+ const message = typeof body.message === "string" ? body.message : `${provider} request failed (${res.status})`;
89
+ throw new PayKitError(message, {
90
+ code: "provider_error",
91
+ provider,
92
+ statusCode: res.status,
93
+ raw: body
94
+ });
95
+ }
96
+ return body;
97
+ }
98
+ function safeEqual(a, b) {
99
+ const ba = Buffer.from(a);
100
+ const bb = Buffer.from(b);
101
+ if (ba.length !== bb.length) return false;
102
+ return (0, import_node_crypto.timingSafeEqual)(ba, bb);
103
+ }
104
+
105
+ // src/providers/paystack.ts
106
+ var PAYSTACK_BASE = "https://api.paystack.co";
107
+ function mapStatus(raw) {
108
+ switch (raw) {
109
+ case "success":
110
+ return "success";
111
+ case "failed":
112
+ return "failed";
113
+ case "abandoned":
114
+ return "abandoned";
115
+ default:
116
+ return "pending";
117
+ }
118
+ }
119
+ function mapEventType(event) {
120
+ return typeof event === "string" && event.length > 0 ? event : "unknown";
121
+ }
122
+ function mapRefundStatus(raw) {
123
+ switch (raw) {
124
+ case "processed":
125
+ case "success":
126
+ return "processed";
127
+ case "failed":
128
+ return "failed";
129
+ default:
130
+ return "pending";
131
+ }
132
+ }
133
+ function createPaystackProvider(ctx) {
134
+ const base = ctx.baseUrl ?? PAYSTACK_BASE;
135
+ return {
136
+ name: "paystack",
137
+ async initialize(params) {
138
+ const reference = params.reference ?? ctx.generateReference();
139
+ const body = await providerRequest(ctx, "paystack", `${base}/transaction/initialize`, {
140
+ method: "POST",
141
+ body: JSON.stringify({
142
+ amount: params.amount,
143
+ email: params.email,
144
+ currency: params.currency ?? "NGN",
145
+ reference,
146
+ callback_url: params.callbackUrl,
147
+ metadata: params.metadata
148
+ })
149
+ });
150
+ const data = body.data ?? {};
151
+ return {
152
+ reference: String(data.reference ?? reference),
153
+ authorizationUrl: String(data.authorization_url ?? ""),
154
+ accessCode: data.access_code ? String(data.access_code) : void 0,
155
+ raw: body
156
+ };
157
+ },
158
+ async verify(reference) {
159
+ const body = await providerRequest(
160
+ ctx,
161
+ "paystack",
162
+ `${base}/transaction/verify/${encodeURIComponent(reference)}`,
163
+ { method: "GET" }
164
+ );
165
+ const data = body.data ?? {};
166
+ const customer = data.customer ?? {};
167
+ return {
168
+ reference: String(data.reference ?? reference),
169
+ status: mapStatus(data.status),
170
+ amount: Number(data.amount ?? 0),
171
+ currency: String(data.currency ?? ""),
172
+ paidAt: data.paid_at ? String(data.paid_at) : void 0,
173
+ channel: data.channel ? String(data.channel) : void 0,
174
+ customer: { email: customer.email ? String(customer.email) : void 0 },
175
+ raw: body
176
+ };
177
+ },
178
+ async refund(reference, options) {
179
+ const body = await providerRequest(ctx, "paystack", `${base}/refund`, {
180
+ method: "POST",
181
+ body: JSON.stringify({
182
+ transaction: reference,
183
+ ...options?.amount !== void 0 ? { amount: options.amount } : {}
184
+ })
185
+ });
186
+ const data = body.data ?? {};
187
+ const transaction = data.transaction ?? {};
188
+ return {
189
+ reference: String(transaction.reference ?? reference),
190
+ status: mapRefundStatus(data.status),
191
+ amount: data.amount !== void 0 ? Number(data.amount) : void 0,
192
+ raw: body
193
+ };
194
+ },
195
+ constructWebhookEvent(rawBody, signature) {
196
+ const expected = (0, import_node_crypto2.createHmac)("sha512", ctx.secretKey).update(rawBody).digest("hex");
197
+ if (!signature || !safeEqual(expected, signature)) {
198
+ throw new PayKitError("Invalid Paystack webhook signature", {
199
+ code: "invalid_signature",
200
+ provider: "paystack"
201
+ });
202
+ }
203
+ let event;
204
+ try {
205
+ event = JSON.parse(rawBody);
206
+ } catch (err) {
207
+ throw new PayKitError("Malformed Paystack webhook body", {
208
+ code: "provider_error",
209
+ provider: "paystack",
210
+ cause: err
211
+ });
212
+ }
213
+ const data = event.data ?? {};
214
+ return {
215
+ type: mapEventType(event.event),
216
+ reference: String(data.reference ?? ""),
217
+ status: data.status ? mapStatus(data.status) : void 0,
218
+ amount: data.amount !== void 0 ? Number(data.amount) : void 0,
219
+ currency: data.currency ? String(data.currency) : void 0,
220
+ raw: event
221
+ };
222
+ }
223
+ };
224
+ }
225
+
226
+ // src/providers/flutterwave.ts
227
+ var FLUTTERWAVE_BASE = "https://api.flutterwave.com";
228
+ function toMajor(subunits) {
229
+ return subunits / 100;
230
+ }
231
+ function toSubunits(major) {
232
+ return Math.round(Number(major ?? 0) * 100);
233
+ }
234
+ function mapStatus2(raw) {
235
+ switch (raw) {
236
+ case "successful":
237
+ case "success":
238
+ return "success";
239
+ case "failed":
240
+ return "failed";
241
+ default:
242
+ return "pending";
243
+ }
244
+ }
245
+ function mapEventType2(status) {
246
+ if (status === "success") return "charge.success";
247
+ if (status === "failed") return "charge.failed";
248
+ return "unknown";
249
+ }
250
+ function mapRefundStatus2(raw) {
251
+ switch (raw) {
252
+ case "completed":
253
+ case "successful":
254
+ case "success":
255
+ case "processed":
256
+ return "processed";
257
+ case "failed":
258
+ return "failed";
259
+ default:
260
+ return "pending";
261
+ }
262
+ }
263
+ function createFlutterwaveProvider(ctx) {
264
+ const base = ctx.baseUrl ?? FLUTTERWAVE_BASE;
265
+ return {
266
+ name: "flutterwave",
267
+ async initialize(params) {
268
+ const reference = params.reference ?? ctx.generateReference();
269
+ const body = await providerRequest(ctx, "flutterwave", `${base}/v3/payments`, {
270
+ method: "POST",
271
+ body: JSON.stringify({
272
+ tx_ref: reference,
273
+ amount: toMajor(params.amount),
274
+ currency: params.currency ?? "NGN",
275
+ redirect_url: params.callbackUrl,
276
+ customer: { email: params.email },
277
+ meta: params.metadata
278
+ })
279
+ });
280
+ const data = body.data ?? {};
281
+ return {
282
+ reference,
283
+ authorizationUrl: String(data.link ?? ""),
284
+ raw: body
285
+ };
286
+ },
287
+ async verify(reference) {
288
+ const body = await providerRequest(
289
+ ctx,
290
+ "flutterwave",
291
+ `${base}/v3/transactions/verify_by_reference?tx_ref=${encodeURIComponent(reference)}`,
292
+ { method: "GET" }
293
+ );
294
+ const data = body.data ?? {};
295
+ const customer = data.customer ?? {};
296
+ return {
297
+ reference: String(data.tx_ref ?? reference),
298
+ status: mapStatus2(data.status),
299
+ amount: toSubunits(data.amount),
300
+ currency: String(data.currency ?? ""),
301
+ paidAt: data.created_at ? String(data.created_at) : void 0,
302
+ channel: data.payment_type ? String(data.payment_type) : void 0,
303
+ customer: { email: customer.email ? String(customer.email) : void 0 },
304
+ raw: body
305
+ };
306
+ },
307
+ async refund(reference, options) {
308
+ const verifyBody = await providerRequest(
309
+ ctx,
310
+ "flutterwave",
311
+ `${base}/v3/transactions/verify_by_reference?tx_ref=${encodeURIComponent(reference)}`,
312
+ { method: "GET" }
313
+ );
314
+ const verifyData = verifyBody.data ?? {};
315
+ const id = verifyData.id;
316
+ if (id === void 0 || id === null) {
317
+ throw new PayKitError(
318
+ `No Flutterwave transaction found for reference "${reference}"`,
319
+ { code: "provider_error", provider: "flutterwave", raw: verifyBody }
320
+ );
321
+ }
322
+ const body = await providerRequest(
323
+ ctx,
324
+ "flutterwave",
325
+ `${base}/v3/transactions/${encodeURIComponent(String(id))}/refund`,
326
+ {
327
+ method: "POST",
328
+ body: JSON.stringify(
329
+ options?.amount !== void 0 ? { amount: toMajor(options.amount) } : {}
330
+ )
331
+ }
332
+ );
333
+ const data = body.data ?? {};
334
+ const refunded = data.amount_refunded ?? data.amount;
335
+ return {
336
+ reference,
337
+ status: mapRefundStatus2(data.status),
338
+ amount: refunded !== void 0 ? toSubunits(refunded) : void 0,
339
+ raw: body
340
+ };
341
+ },
342
+ constructWebhookEvent(rawBody, signature) {
343
+ if (!ctx.webhookSecret) {
344
+ throw new PayKitError(
345
+ "Flutterwave webhook verification requires `webhookSecret` (your Secret hash)",
346
+ { code: "config_error", provider: "flutterwave" }
347
+ );
348
+ }
349
+ if (!signature || !safeEqual(ctx.webhookSecret, signature)) {
350
+ throw new PayKitError("Invalid Flutterwave webhook signature", {
351
+ code: "invalid_signature",
352
+ provider: "flutterwave"
353
+ });
354
+ }
355
+ let event;
356
+ try {
357
+ event = JSON.parse(rawBody);
358
+ } catch (err) {
359
+ throw new PayKitError("Malformed Flutterwave webhook body", {
360
+ code: "provider_error",
361
+ provider: "flutterwave",
362
+ cause: err
363
+ });
364
+ }
365
+ const data = event.data ?? {};
366
+ const status = mapStatus2(data.status);
367
+ return {
368
+ type: mapEventType2(status),
369
+ reference: String(data.tx_ref ?? ""),
370
+ status,
371
+ amount: data.amount !== void 0 ? toSubunits(data.amount) : void 0,
372
+ currency: data.currency ? String(data.currency) : void 0,
373
+ raw: event
374
+ };
375
+ }
376
+ };
377
+ }
378
+
379
+ // src/client.ts
380
+ function resolveProvider(config, ctx) {
381
+ switch (config.provider) {
382
+ case "paystack":
383
+ return createPaystackProvider(ctx);
384
+ case "flutterwave":
385
+ return createFlutterwaveProvider(ctx);
386
+ default:
387
+ throw new PayKitError(`Unknown provider: ${String(config.provider)}`, {
388
+ code: "config_error"
389
+ });
390
+ }
391
+ }
392
+ function createPayClient(config) {
393
+ if (!config.secretKey) {
394
+ throw new PayKitError("`secretKey` is required", { code: "config_error" });
395
+ }
396
+ const fetchImpl = config.fetch ?? globalThis.fetch;
397
+ if (typeof fetchImpl !== "function") {
398
+ throw new PayKitError(
399
+ "No fetch implementation found. Use Node >= 18 or pass `config.fetch`.",
400
+ { code: "config_error" }
401
+ );
402
+ }
403
+ const ctx = {
404
+ secretKey: config.secretKey,
405
+ webhookSecret: config.webhookSecret,
406
+ baseUrl: config.baseUrl,
407
+ fetch: fetchImpl,
408
+ generateReference: config.generateReference ?? (() => `pk_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "")}`)
409
+ };
410
+ const provider = resolveProvider(config, ctx);
411
+ return {
412
+ provider: provider.name,
413
+ initialize: (params) => provider.initialize(params),
414
+ verify: (reference) => provider.verify(reference),
415
+ refund: (reference, options) => provider.refund(reference, options),
416
+ webhooks: {
417
+ construct: (rawBody, signature) => provider.constructWebhookEvent(rawBody, signature)
418
+ }
419
+ };
420
+ }
421
+
422
+ // src/fallback.ts
423
+ function createFallbackClient(config) {
424
+ if (!config.providers || config.providers.length === 0) {
425
+ throw new PayKitError("createFallbackClient requires at least one provider", {
426
+ code: "config_error"
427
+ });
428
+ }
429
+ const clients = /* @__PURE__ */ new Map();
430
+ const order = [];
431
+ for (const entry of config.providers) {
432
+ if (clients.has(entry.provider)) continue;
433
+ clients.set(
434
+ entry.provider,
435
+ createPayClient({
436
+ provider: entry.provider,
437
+ secretKey: entry.secretKey,
438
+ webhookSecret: entry.webhookSecret,
439
+ baseUrl: entry.baseUrl,
440
+ fetch: config.fetch,
441
+ generateReference: config.generateReference
442
+ })
443
+ );
444
+ order.push(entry.provider);
445
+ }
446
+ function getClient(provider) {
447
+ const client = clients.get(provider);
448
+ if (!client) {
449
+ throw new PayKitError(
450
+ `Provider "${provider}" is not configured on this fallback client`,
451
+ { code: "config_error" }
452
+ );
453
+ }
454
+ return client;
455
+ }
456
+ return {
457
+ async initialize(params) {
458
+ let lastError;
459
+ for (const provider of order) {
460
+ try {
461
+ const result = await getClient(provider).initialize(params);
462
+ return { ...result, provider };
463
+ } catch (err) {
464
+ lastError = err;
465
+ if (!isRetryableError(err)) throw err;
466
+ }
467
+ }
468
+ throw lastError ?? new PayKitError("All providers failed to initialize the payment", {
469
+ code: "provider_error"
470
+ });
471
+ },
472
+ verify: (provider, reference) => getClient(provider).verify(reference),
473
+ refund: (provider, reference, options) => getClient(provider).refund(reference, options),
474
+ webhooks: {
475
+ construct: (provider, rawBody, signature) => getClient(provider).webhooks.construct(rawBody, signature)
476
+ },
477
+ client: getClient
478
+ };
479
+ }
480
+ // Annotate the CommonJS export names for ESM import in node:
481
+ 0 && (module.exports = {
482
+ PayKitError,
483
+ createFallbackClient,
484
+ createPayClient,
485
+ isRetryableError
486
+ });
487
+ //# sourceMappingURL=index.cjs.map