@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 CHANGED
@@ -7,20 +7,17 @@
7
7
 
8
8
  ## Features
9
9
 
10
- - **Zero Configuration** - Works out of the box, auto-detects environment
11
- - **Type-Safe** - Full TypeScript support with complete type definitions
12
- - **Auto-Retry** - Built-in retry logic with exponential backoff
13
- - **Idempotency** - Automatic idempotency key generation
14
- - **Environment Detection** - Automatically switches between test/production
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
- #### Get Payment Status
89
+ ### Payment Links
76
90
 
77
91
  ```typescript
78
- const payment = await zendfi.getPayment('payment_id');
79
-
80
- console.log(payment.status); // 'pending' | 'confirmed' | 'failed' | 'expired'
81
- ```
82
-
83
- #### List Payments
92
+ const client = new ZendFiClient({
93
+ apiKey: process.env.ZENDFI_API_KEY
94
+ });
84
95
 
85
- ```typescript
86
- const payments = await zendfi.listPayments({
87
- page: 1,
88
- limit: 10,
89
- status: 'confirmed',
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(payments.data); // Array of payments
95
- console.log(payments.pagination); // Pagination info
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
- ### Subscriptions
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
- #### Create a Plan
112
+ ## Webhooks
101
113
 
102
- ```typescript
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
- #### Create a Subscription
116
+ ### Next.js App Router (Recommended)
113
117
 
114
118
  ```typescript
115
- const subscription = await zendfi.createSubscription({
116
- plan_id: plan.id,
117
- customer_email: 'customer@example.com',
118
- metadata: {
119
- userId: 'user_123',
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
- #### Cancel a Subscription
136
+ ### Next.js Pages Router (Legacy)
125
137
 
126
138
  ```typescript
127
- const canceled = await zendfi.cancelSubscription(subscription.id);
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
- ### Webhooks
151
+ // Important: Disable body parser for signature verification
152
+ export const config = {
153
+ api: { bodyParser: false },
154
+ };
155
+ ```
131
156
 
132
- #### Verify Webhook Signature
157
+ ### Express
133
158
 
134
159
  ```typescript
135
- import { zendfi } from '@zendfi/sdk';
160
+ import express from 'express';
161
+ import { createExpressWebhookHandler } from '@zendfi/sdk/express';
136
162
 
137
- // In your webhook handler (e.g., /api/webhooks/zendfi)
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
- const isValid = zendfi.verifyWebhook({
143
- payload,
144
- signature,
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
- if (!isValid) {
149
- return new Response('Invalid signature', { status: 401 });
150
- }
151
-
152
- const event = JSON.parse(payload);
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
- ## Configuration
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
- # Or for Create React App
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
- import { ZendFiClient } from '@zendfi/sdk';
194
-
195
- const client = new ZendFiClient({
196
- apiKey: 'zfi_test_...',
197
- environment: 'development',
198
- timeout: 30000, // 30 seconds
199
- retries: 3,
200
- idempotencyEnabled: true,
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
- ## Advanced Features
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
- ### Automatic Retries
202
+ ### Manual Webhook Verification
217
203
 
218
- The SDK retries failed requests automatically:
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
- // This will retry automatically on network errors
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
- ```typescript
234
- // SDK automatically adds: Idempotency-Key: zfi_idem_1234567890_abc123
235
-
236
- const payment = await zendfi.createPayment({
237
- amount: 50,
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
- // Safe to retry - won't create duplicate payments
215
+ // Handle verified payload
216
+ return new Response('OK');
217
+ }
241
218
  ```
242
219
 
243
- ## 🎯 TypeScript Support
244
-
245
- Full type definitions included:
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
- ```typescript
248
- import type {
249
- Payment,
250
- PaymentStatus,
251
- Subscription,
252
- SubscriptionPlan,
253
- WebhookEvent,
254
- } from '@zendfi/sdk';
225
+ ## Configuration & Environment Variables
255
226
 
256
- const payment: Payment = await zendfi.createPayment({
257
- amount: 50,
258
- });
259
-
260
- // IntelliSense for all fields
261
- console.log(payment.id);
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 { AuthenticationError, ValidationError, NetworkError } from '@zendfi/sdk';
239
+ import {
240
+ AuthenticationError,
241
+ ValidationError,
242
+ NetworkError
243
+ } from '@zendfi/sdk';
270
244
 
271
245
  try {
272
- const payment = await zendfi.createPayment({
273
- amount: 50,
274
- });
246
+ await zendfi.createPayment({ amount: 50 });
275
247
  } catch (error) {
276
248
  if (error instanceof AuthenticationError) {
277
- console.error('Invalid API key');
249
+ // Handle authentication error
278
250
  } else if (error instanceof ValidationError) {
279
- console.error('Invalid request:', error.details);
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
- ## Framework Examples
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
- ### Methods
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
- | Method | Description |
359
- | --------------------------- | -------------------------- |
360
- | `createPayment()` | Create a new payment |
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
- See [full API documentation](https://docs.zendfi.tech/sdk) for detailed reference.
267
+ **tsup warnings about types condition:**
268
+ - This is a packaging order issue that doesn't affect runtime behavior
371
269
 
372
- ## Debugging
270
+ ## Contributing
373
271
 
374
- Enable debug logs:
272
+ Run the SDK build and tests locally before opening a PR:
375
273
 
376
274
  ```bash
377
- DEBUG=zendfi:* node your-app.js
275
+ cd packages/sdk
276
+ pnpm install
277
+ pnpm run build
278
+ pnpm test
378
279
  ```
379
280
 
380
281
  ## Support
381
282
 
382
- - [Documentation](https://docs.zendfi.tech)
383
- - [API Reference](https://docs.zendfi.tech/api)
384
- - [GitHub Issues](https://github.com/zendfi/zendfi-toolkit/issues)
385
- - Email: dev@zendfi.tech
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
+ };