@zendfi/sdk 0.1.1 → 0.2.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/README.md +153 -252
- package/dist/chunk-YFOBPGQE.mjs +152 -0
- package/dist/express.d.mts +46 -0
- package/dist/express.d.ts +46 -0
- package/dist/express.js +220 -0
- package/dist/express.mjs +56 -0
- package/dist/index.d.mts +6 -325
- package/dist/index.d.ts +6 -325
- package/dist/index.js +173 -16
- package/dist/index.mjs +35 -22
- package/dist/nextjs.d.mts +37 -0
- package/dist/nextjs.d.ts +37 -0
- package/dist/nextjs.js +227 -0
- package/dist/nextjs.mjs +63 -0
- package/dist/webhook-handler-BIze3Qop.d.mts +388 -0
- package/dist/webhook-handler-BIze3Qop.d.ts +388 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -7,20 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- **Zero Configuration** -
|
|
11
|
-
- **Type-Safe**
|
|
12
|
-
- **Auto-Retry**
|
|
13
|
-
- **Idempotency**
|
|
14
|
-
- **
|
|
15
|
-
- **Smart Defaults** - Sensible defaults for all options
|
|
10
|
+
- **Zero Configuration** — Auto-detects environment and works with sensible defaults
|
|
11
|
+
- **Type-Safe** — Full TypeScript types for payments, payment links, subscriptions, escrows, invoices and webhook payloads
|
|
12
|
+
- **Auto-Retry** — Built-in retry logic with exponential backoff for network/server errors
|
|
13
|
+
- **Idempotency** — Automatic idempotency key generation for safe retries
|
|
14
|
+
- **Webhook Helpers** — Auto-verified handlers with optional deduplication and framework adapters
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
19
18
|
```bash
|
|
20
19
|
npm install @zendfi/sdk
|
|
21
20
|
# or
|
|
22
|
-
yarn add @zendfi/sdk
|
|
23
|
-
# or
|
|
24
21
|
pnpm add @zendfi/sdk
|
|
25
22
|
```
|
|
26
23
|
|
|
@@ -38,6 +35,7 @@ ZENDFI_API_KEY=zfi_test_your_api_key_here
|
|
|
38
35
|
```typescript
|
|
39
36
|
import { zendfi } from '@zendfi/sdk';
|
|
40
37
|
|
|
38
|
+
// Singleton auto-configured from environment
|
|
41
39
|
const payment = await zendfi.createPayment({
|
|
42
40
|
amount: 50,
|
|
43
41
|
description: 'Premium subscription',
|
|
@@ -46,6 +44,22 @@ const payment = await zendfi.createPayment({
|
|
|
46
44
|
console.log(payment.checkout_url); // Send this to your customer
|
|
47
45
|
```
|
|
48
46
|
|
|
47
|
+
### 3. Or use an explicit client (recommended for server code)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { ZendFiClient } from '@zendfi/sdk';
|
|
51
|
+
|
|
52
|
+
const client = new ZendFiClient({
|
|
53
|
+
apiKey: process.env.ZENDFI_API_KEY
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const payment = await client.createPayment({
|
|
57
|
+
amount: 99.99,
|
|
58
|
+
currency: 'USD',
|
|
59
|
+
description: 'Annual subscription',
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
49
63
|
That's it! The SDK handles everything else automatically. 🎉
|
|
50
64
|
|
|
51
65
|
## Usage
|
|
@@ -72,317 +86,204 @@ const payment = await zendfi.createPayment({
|
|
|
72
86
|
window.location.href = payment.checkout_url;
|
|
73
87
|
```
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
### Payment Links
|
|
76
90
|
|
|
77
91
|
```typescript
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
#### List Payments
|
|
92
|
+
const client = new ZendFiClient({
|
|
93
|
+
apiKey: process.env.ZENDFI_API_KEY
|
|
94
|
+
});
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
from_date: '2025-01-01',
|
|
91
|
-
to_date: '2025-12-31',
|
|
96
|
+
// Create a payment link
|
|
97
|
+
const link = await client.createPaymentLink({
|
|
98
|
+
amount: 20,
|
|
99
|
+
currency: 'USD',
|
|
100
|
+
description: 'Product purchase',
|
|
92
101
|
});
|
|
93
102
|
|
|
94
|
-
console.log(
|
|
95
|
-
|
|
103
|
+
console.log(link.hosted_page_url);
|
|
104
|
+
|
|
105
|
+
// List all payment links
|
|
106
|
+
const links = await client.listPaymentLinks();
|
|
107
|
+
console.log(`Total links: ${links.length}`);
|
|
96
108
|
```
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
> **Note:** If you get a 405 error when calling `listPaymentLinks()` in tests, confirm that `ZENDFI_API_URL` / `baseURL` and your API key point to a server that exposes `GET /api/v1/payment-links`.
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
## Webhooks
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
const plan = await zendfi.createSubscriptionPlan({
|
|
104
|
-
name: 'Pro Plan',
|
|
105
|
-
description: 'Access to all premium features',
|
|
106
|
-
amount: 29.99,
|
|
107
|
-
interval: 'monthly',
|
|
108
|
-
trial_days: 14,
|
|
109
|
-
});
|
|
110
|
-
```
|
|
114
|
+
The SDK includes robust webhook processing with signature verification, optional deduplication, and typed handler dispatch.
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
### Next.js App Router (Recommended)
|
|
113
117
|
|
|
114
118
|
```typescript
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// app/api/webhooks/zendfi/route.ts
|
|
120
|
+
import { createNextWebhookHandler } from '@zendfi/sdk/next';
|
|
121
|
+
|
|
122
|
+
export const POST = createNextWebhookHandler({
|
|
123
|
+
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
124
|
+
handlers: {
|
|
125
|
+
'payment.confirmed': async (payment) => {
|
|
126
|
+
// Payment is already verified and typed
|
|
127
|
+
await db.orders.update({
|
|
128
|
+
where: { id: payment.metadata.orderId },
|
|
129
|
+
data: { status: 'paid' },
|
|
130
|
+
});
|
|
131
|
+
},
|
|
120
132
|
},
|
|
121
133
|
});
|
|
122
134
|
```
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
### Next.js Pages Router (Legacy)
|
|
125
137
|
|
|
126
138
|
```typescript
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
// pages/api/webhooks/zendfi.ts
|
|
140
|
+
import { createPagesWebhookHandler } from '@zendfi/sdk/next';
|
|
141
|
+
|
|
142
|
+
export default createPagesWebhookHandler({
|
|
143
|
+
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
144
|
+
handlers: {
|
|
145
|
+
'payment.confirmed': async (payment) => {
|
|
146
|
+
await fulfillOrder(payment.metadata.orderId);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
129
150
|
|
|
130
|
-
|
|
151
|
+
// Important: Disable body parser for signature verification
|
|
152
|
+
export const config = {
|
|
153
|
+
api: { bodyParser: false },
|
|
154
|
+
};
|
|
155
|
+
```
|
|
131
156
|
|
|
132
|
-
|
|
157
|
+
### Express
|
|
133
158
|
|
|
134
159
|
```typescript
|
|
135
|
-
import
|
|
160
|
+
import express from 'express';
|
|
161
|
+
import { createExpressWebhookHandler } from '@zendfi/sdk/express';
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
export async function POST(request: Request) {
|
|
139
|
-
const payload = await request.text();
|
|
140
|
-
const signature = request.headers.get('x-zendfi-signature');
|
|
163
|
+
const app = express();
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
app.post(
|
|
166
|
+
'/api/webhooks/zendfi',
|
|
167
|
+
express.raw({ type: 'application/json' }), // Preserve raw body
|
|
168
|
+
createExpressWebhookHandler({
|
|
145
169
|
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
switch (event.event) {
|
|
155
|
-
case 'payment.confirmed':
|
|
156
|
-
// Handle payment confirmation
|
|
157
|
-
break;
|
|
158
|
-
case 'subscription.activated':
|
|
159
|
-
// Handle subscription activation
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return new Response('OK', { status: 200 });
|
|
164
|
-
}
|
|
170
|
+
handlers: {
|
|
171
|
+
'payment.confirmed': async (payment) => {
|
|
172
|
+
// Handle confirmed payment
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
);
|
|
165
177
|
```
|
|
166
178
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
### Environment Variables
|
|
170
|
-
|
|
171
|
-
The SDK automatically detects and uses these environment variables:
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
# API Key (required)
|
|
175
|
-
ZENDFI_API_KEY=zfi_test_...
|
|
176
|
-
|
|
177
|
-
# Or for Next.js
|
|
178
|
-
NEXT_PUBLIC_ZENDFI_API_KEY=zfi_test_...
|
|
179
|
+
### Webhook Deduplication
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
REACT_APP_ZENDFI_API_KEY=zfi_test_...
|
|
182
|
-
|
|
183
|
-
# Environment (optional, auto-detected)
|
|
184
|
-
ZENDFI_ENVIRONMENT=development # or staging, production
|
|
185
|
-
|
|
186
|
-
# Custom API URL (optional)
|
|
187
|
-
ZENDFI_API_URL=https://api.zendfi.tech
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Manual Configuration
|
|
181
|
+
By default, the handler uses an in-memory Set for deduplication (suitable for development). For production, supply `isProcessed` and `onProcessed` hooks (or the aliases `checkDuplicate` / `markProcessed`) backed by Redis or your database:
|
|
191
182
|
|
|
192
183
|
```typescript
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
184
|
+
export const POST = createNextWebhookHandler({
|
|
185
|
+
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
186
|
+
isProcessed: async (eventId) => {
|
|
187
|
+
return await redis.exists(`webhook:${eventId}`);
|
|
188
|
+
},
|
|
189
|
+
onProcessed: async (eventId) => {
|
|
190
|
+
await redis.set(`webhook:${eventId}`, '1', 'EX', 86400);
|
|
191
|
+
},
|
|
192
|
+
handlers: {
|
|
193
|
+
'payment.confirmed': async (payment) => {
|
|
194
|
+
// Handle payment
|
|
195
|
+
},
|
|
196
|
+
},
|
|
201
197
|
});
|
|
202
198
|
```
|
|
203
199
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### Auto Environment Detection
|
|
207
|
-
|
|
208
|
-
The SDK automatically detects your environment:
|
|
209
|
-
|
|
210
|
-
| Environment | Detected When |
|
|
211
|
-
| ----------- | -------------------------------------- |
|
|
212
|
-
| Development | `localhost`, `127.0.0.1`, `NODE_ENV=development` |
|
|
213
|
-
| Staging | `*.staging.*`, `*.vercel.app`, `NODE_ENV=staging` |
|
|
214
|
-
| Production | `NODE_ENV=production`, production domains |
|
|
200
|
+
When deduplication is enabled, duplicate requests are rejected with HTTP 409.
|
|
215
201
|
|
|
216
|
-
###
|
|
202
|
+
### Manual Webhook Verification
|
|
217
203
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- **Server errors (5xx)**: Retries up to 3 times with exponential backoff
|
|
221
|
-
- **Network errors**: Retries up to 3 times
|
|
222
|
-
- **Client errors (4xx)**: No retry (fix your request)
|
|
204
|
+
If you prefer manual verification:
|
|
223
205
|
|
|
224
206
|
```typescript
|
|
225
|
-
|
|
226
|
-
const payment = await zendfi.createPayment({ amount: 50 });
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Idempotency Keys
|
|
230
|
-
|
|
231
|
-
Prevent duplicate payments with automatic idempotency:
|
|
207
|
+
import { verifyNextWebhook } from '@zendfi/sdk/webhooks';
|
|
232
208
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
209
|
+
export async function POST(request: Request) {
|
|
210
|
+
const payload = await verifyNextWebhook(request);
|
|
211
|
+
if (!payload) {
|
|
212
|
+
return new Response('Invalid signature', { status: 401 });
|
|
213
|
+
}
|
|
239
214
|
|
|
240
|
-
//
|
|
215
|
+
// Handle verified payload
|
|
216
|
+
return new Response('OK');
|
|
217
|
+
}
|
|
241
218
|
```
|
|
242
219
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
220
|
+
**Available helpers:**
|
|
221
|
+
- `verifyNextWebhook(request, secret?)` — Next.js App Router
|
|
222
|
+
- `verifyExpressWebhook(req, secret?)` — Express
|
|
223
|
+
- `verifyWebhookSignature(payload, signature, secret)` — Low-level verifier
|
|
246
224
|
|
|
247
|
-
|
|
248
|
-
import type {
|
|
249
|
-
Payment,
|
|
250
|
-
PaymentStatus,
|
|
251
|
-
Subscription,
|
|
252
|
-
SubscriptionPlan,
|
|
253
|
-
WebhookEvent,
|
|
254
|
-
} from '@zendfi/sdk';
|
|
225
|
+
## Configuration & Environment Variables
|
|
255
226
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.log(payment.status);
|
|
263
|
-
console.log(payment.checkout_url);
|
|
264
|
-
```
|
|
227
|
+
| Variable | Required | Description |
|
|
228
|
+
|----------|----------|-------------|
|
|
229
|
+
| `ZENDFI_API_KEY` | Yes* | Your ZendFi API key (*unless passed to `ZendFiClient`) |
|
|
230
|
+
| `ZENDFI_WEBHOOK_SECRET` | No | Used by webhook adapters for auto-verification |
|
|
231
|
+
| `ZENDFI_API_URL` | No | Override API base URL (useful for local testing) |
|
|
232
|
+
| `ZENDFI_ENVIRONMENT` | No | Optional environment override |
|
|
265
233
|
|
|
266
234
|
## Error Handling
|
|
267
235
|
|
|
236
|
+
The SDK throws typed errors that you can import and check with `instanceof`:
|
|
237
|
+
|
|
268
238
|
```typescript
|
|
269
|
-
import {
|
|
239
|
+
import {
|
|
240
|
+
AuthenticationError,
|
|
241
|
+
ValidationError,
|
|
242
|
+
NetworkError
|
|
243
|
+
} from '@zendfi/sdk';
|
|
270
244
|
|
|
271
245
|
try {
|
|
272
|
-
|
|
273
|
-
amount: 50,
|
|
274
|
-
});
|
|
246
|
+
await zendfi.createPayment({ amount: 50 });
|
|
275
247
|
} catch (error) {
|
|
276
248
|
if (error instanceof AuthenticationError) {
|
|
277
|
-
|
|
249
|
+
// Handle authentication error
|
|
278
250
|
} else if (error instanceof ValidationError) {
|
|
279
|
-
|
|
280
|
-
} else if (error instanceof NetworkError) {
|
|
281
|
-
console.error('Network error, will retry automatically');
|
|
251
|
+
// Handle validation error
|
|
282
252
|
}
|
|
283
253
|
}
|
|
284
254
|
```
|
|
285
255
|
|
|
286
|
-
##
|
|
287
|
-
|
|
288
|
-
### Next.js App Router
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
// app/api/checkout/route.ts
|
|
292
|
-
import { zendfi } from '@zendfi/sdk';
|
|
293
|
-
import { NextResponse } from 'next/server';
|
|
294
|
-
|
|
295
|
-
export async function POST(request: Request) {
|
|
296
|
-
const { amount } = await request.json();
|
|
297
|
-
|
|
298
|
-
const payment = await zendfi.createPayment({
|
|
299
|
-
amount,
|
|
300
|
-
redirect_url: `${process.env.NEXT_PUBLIC_URL}/success`,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
return NextResponse.json({ url: payment.checkout_url });
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
### Express.js
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
import express from 'express';
|
|
311
|
-
import { zendfi } from '@zendfi/sdk';
|
|
312
|
-
|
|
313
|
-
const app = express();
|
|
314
|
-
|
|
315
|
-
app.post('/api/checkout', async (req, res) => {
|
|
316
|
-
const { amount } = req.body;
|
|
317
|
-
|
|
318
|
-
const payment = await zendfi.createPayment({
|
|
319
|
-
amount,
|
|
320
|
-
redirect_url: 'https://yourapp.com/success',
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
res.json({ url: payment.checkout_url });
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### React
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { useState } from 'react';
|
|
331
|
-
import { zendfi } from '@zendfi/sdk';
|
|
332
|
-
|
|
333
|
-
function CheckoutButton() {
|
|
334
|
-
const [loading, setLoading] = useState(false);
|
|
335
|
-
|
|
336
|
-
const handleCheckout = async () => {
|
|
337
|
-
setLoading(true);
|
|
338
|
-
|
|
339
|
-
const payment = await zendfi.createPayment({
|
|
340
|
-
amount: 50,
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
window.location.href = payment.checkout_url;
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
return (
|
|
347
|
-
<button onClick={handleCheckout} disabled={loading}>
|
|
348
|
-
{loading ? 'Creating checkout...' : 'Pay with Crypto'}
|
|
349
|
-
</button>
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
## API Reference
|
|
256
|
+
## Troubleshooting
|
|
355
257
|
|
|
356
|
-
|
|
258
|
+
**Webhook verification failures:**
|
|
259
|
+
- Ensure you're using `express.raw({ type: 'application/json' })` for Express
|
|
260
|
+
- For Next.js Pages Router, set `export const config = { api: { bodyParser: false } }`
|
|
261
|
+
- Middleware consuming the raw body will break signature verification
|
|
357
262
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
| `getPayment(id)` | Get payment by ID |
|
|
362
|
-
| `listPayments(options)` | List all payments |
|
|
363
|
-
| `createSubscriptionPlan()` | Create subscription plan |
|
|
364
|
-
| `getSubscriptionPlan(id)` | Get plan by ID |
|
|
365
|
-
| `createSubscription()` | Create a subscription |
|
|
366
|
-
| `getSubscription(id)` | Get subscription by ID |
|
|
367
|
-
| `cancelSubscription(id)` | Cancel a subscription |
|
|
368
|
-
| `verifyWebhook()` | Verify webhook signature |
|
|
263
|
+
**405 errors on `listPaymentLinks()`:**
|
|
264
|
+
- Verify your `ZENDFI_API_URL` is correct
|
|
265
|
+
- Confirm your API server exposes `GET /api/v1/payment-links`
|
|
369
266
|
|
|
370
|
-
|
|
267
|
+
**tsup warnings about types condition:**
|
|
268
|
+
- This is a packaging order issue that doesn't affect runtime behavior
|
|
371
269
|
|
|
372
|
-
##
|
|
270
|
+
## Contributing
|
|
373
271
|
|
|
374
|
-
|
|
272
|
+
Run the SDK build and tests locally before opening a PR:
|
|
375
273
|
|
|
376
274
|
```bash
|
|
377
|
-
|
|
275
|
+
cd packages/sdk
|
|
276
|
+
pnpm install
|
|
277
|
+
pnpm run build
|
|
278
|
+
pnpm test
|
|
378
279
|
```
|
|
379
280
|
|
|
380
281
|
## Support
|
|
381
282
|
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
385
|
-
- Email
|
|
283
|
+
- **Documentation:** https://docs.zendfi.tech
|
|
284
|
+
- **API Reference:** https://docs.zendfi.tech/api
|
|
285
|
+
- **GitHub Issues:** https://github.com/zendfi/zendfi-toolkit/issues
|
|
286
|
+
- **Email:** dev@zendfi.tech
|
|
386
287
|
|
|
387
288
|
## License
|
|
388
289
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/webhook-handler.ts
|
|
9
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
10
|
+
var processedWebhooks = /* @__PURE__ */ new Set();
|
|
11
|
+
var defaultIsProcessed = async (webhookId) => {
|
|
12
|
+
return processedWebhooks.has(webhookId);
|
|
13
|
+
};
|
|
14
|
+
var defaultOnProcessed = async (webhookId) => {
|
|
15
|
+
processedWebhooks.add(webhookId);
|
|
16
|
+
if (processedWebhooks.size > 1e4) {
|
|
17
|
+
const iterator = processedWebhooks.values();
|
|
18
|
+
for (let i = 0; i < 1e3; i++) {
|
|
19
|
+
const { value } = iterator.next();
|
|
20
|
+
if (value) processedWebhooks.delete(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function generateWebhookId(payload) {
|
|
25
|
+
return `${payload.merchant_id}:${payload.event}:${payload.timestamp}`;
|
|
26
|
+
}
|
|
27
|
+
async function processPayload(payload, handlers, config) {
|
|
28
|
+
try {
|
|
29
|
+
const webhookId = generateWebhookId(payload);
|
|
30
|
+
const isProcessed = config.isProcessed || config.checkDuplicate || defaultIsProcessed;
|
|
31
|
+
const onProcessed = config.onProcessed || config.markProcessed || defaultOnProcessed;
|
|
32
|
+
const dedupEnabled = !!(config.enableDeduplication || config.isProcessed || config.checkDuplicate);
|
|
33
|
+
if (dedupEnabled && await isProcessed(webhookId)) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
processed: false,
|
|
37
|
+
event: payload.event,
|
|
38
|
+
error: "Duplicate webhook",
|
|
39
|
+
statusCode: 409
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const handler = handlers[payload.event];
|
|
43
|
+
if (!handler) {
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
processed: false,
|
|
47
|
+
event: payload.event,
|
|
48
|
+
statusCode: 200
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
await handler(payload.data, payload);
|
|
52
|
+
await onProcessed(webhookId);
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
processed: true,
|
|
56
|
+
event: payload.event
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const err = error;
|
|
60
|
+
if (config?.onError) {
|
|
61
|
+
await config.onError(err, error?.event);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
processed: false,
|
|
66
|
+
error: err.message,
|
|
67
|
+
event: error?.event,
|
|
68
|
+
statusCode: 500
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function processWebhook(a, b, c) {
|
|
73
|
+
if (a && typeof a === "object" && a.event && b && c) {
|
|
74
|
+
return processPayload(a, b, c);
|
|
75
|
+
}
|
|
76
|
+
const opts = a;
|
|
77
|
+
if (!opts || !opts.signature && !opts.body && !opts.handlers) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
processed: false,
|
|
81
|
+
error: "Invalid arguments to processWebhook",
|
|
82
|
+
statusCode: 400
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const signature = opts.signature;
|
|
86
|
+
const body = opts.body;
|
|
87
|
+
const handlers = opts.handlers || {};
|
|
88
|
+
const cfg = opts.config || {};
|
|
89
|
+
const secret = cfg.webhookSecret || cfg.secret;
|
|
90
|
+
if (!secret) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
processed: false,
|
|
94
|
+
error: "Webhook secret not provided",
|
|
95
|
+
statusCode: 400
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!signature || !body) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
processed: false,
|
|
102
|
+
error: "Missing signature or body",
|
|
103
|
+
statusCode: 400
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const sig = typeof signature === "string" && signature.startsWith("sha256=") ? signature.slice("sha256=".length) : String(signature);
|
|
108
|
+
const hmac = createHmac("sha256", secret).update(body, "utf8").digest("hex");
|
|
109
|
+
let ok = false;
|
|
110
|
+
try {
|
|
111
|
+
const sigBuf = Buffer.from(sig, "hex");
|
|
112
|
+
const hmacBuf = Buffer.from(hmac, "hex");
|
|
113
|
+
if (sigBuf.length === hmacBuf.length) {
|
|
114
|
+
ok = timingSafeEqual(sigBuf, hmacBuf);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
ok = timingSafeEqual(Buffer.from(String(sig), "utf8"), Buffer.from(hmac, "utf8"));
|
|
118
|
+
}
|
|
119
|
+
if (!ok) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
processed: false,
|
|
123
|
+
error: "Invalid signature",
|
|
124
|
+
statusCode: 401
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const payload = JSON.parse(body);
|
|
128
|
+
const fullConfig = {
|
|
129
|
+
secret,
|
|
130
|
+
isProcessed: cfg.isProcessed,
|
|
131
|
+
onProcessed: cfg.onProcessed,
|
|
132
|
+
onError: cfg.onError,
|
|
133
|
+
// Forward compatibility for alternate names and flags
|
|
134
|
+
enableDeduplication: cfg.enableDeduplication,
|
|
135
|
+
checkDuplicate: cfg.checkDuplicate,
|
|
136
|
+
markProcessed: cfg.markProcessed
|
|
137
|
+
};
|
|
138
|
+
return await processPayload(payload, handlers, fullConfig);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
processed: false,
|
|
143
|
+
error: err.message,
|
|
144
|
+
statusCode: 500
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
__require,
|
|
151
|
+
processWebhook
|
|
152
|
+
};
|