@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/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
ValidationError: () => ValidationError,
|
|
38
38
|
ZendFiClient: () => ZendFiClient,
|
|
39
39
|
ZendFiError: () => ZendFiError,
|
|
40
|
+
processWebhook: () => processWebhook,
|
|
40
41
|
verifyExpressWebhook: () => verifyExpressWebhook,
|
|
41
42
|
verifyNextWebhook: () => verifyNextWebhook,
|
|
42
43
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
@@ -322,14 +323,15 @@ var ZendFiClient = class {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
/**
|
|
325
|
-
* List all payment links
|
|
326
|
+
* List all payment links for the authenticated merchant
|
|
326
327
|
*/
|
|
327
328
|
async listPaymentLinks() {
|
|
328
|
-
|
|
329
|
+
const response = await this.request("GET", "/api/v1/payment-links");
|
|
330
|
+
return response.map((link) => ({
|
|
331
|
+
...link,
|
|
332
|
+
url: link.hosted_page_url
|
|
333
|
+
}));
|
|
329
334
|
}
|
|
330
|
-
// ===================================================================
|
|
331
|
-
// INSTALLMENT PLANS - Pay over time
|
|
332
|
-
// ===================================================================
|
|
333
335
|
/**
|
|
334
336
|
* Create an installment plan
|
|
335
337
|
* Split a purchase into multiple scheduled payments
|
|
@@ -380,9 +382,6 @@ var ZendFiClient = class {
|
|
|
380
382
|
`/api/v1/installment-plans/${planId}/cancel`
|
|
381
383
|
);
|
|
382
384
|
}
|
|
383
|
-
// ===================================================================
|
|
384
|
-
// ESCROW - Secure fund holding
|
|
385
|
-
// ===================================================================
|
|
386
385
|
/**
|
|
387
386
|
* Create an escrow transaction
|
|
388
387
|
* Hold funds until conditions are met
|
|
@@ -432,9 +431,6 @@ var ZendFiClient = class {
|
|
|
432
431
|
async disputeEscrow(escrowId, request) {
|
|
433
432
|
return this.request("POST", `/api/v1/escrows/${escrowId}/dispute`, request);
|
|
434
433
|
}
|
|
435
|
-
// ===================================================================
|
|
436
|
-
// INVOICES - Professional billing
|
|
437
|
-
// ===================================================================
|
|
438
434
|
/**
|
|
439
435
|
* Create an invoice
|
|
440
436
|
*/
|
|
@@ -486,15 +482,34 @@ var ZendFiClient = class {
|
|
|
486
482
|
if (!request.payload || !request.signature || !request.secret) {
|
|
487
483
|
return false;
|
|
488
484
|
}
|
|
489
|
-
|
|
490
|
-
|
|
485
|
+
let payloadString;
|
|
486
|
+
let parsedPayload = null;
|
|
487
|
+
if (typeof request.payload === "string") {
|
|
488
|
+
payloadString = request.payload;
|
|
489
|
+
try {
|
|
490
|
+
parsedPayload = JSON.parse(payloadString);
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
} else if (typeof request.payload === "object") {
|
|
495
|
+
parsedPayload = request.payload;
|
|
496
|
+
try {
|
|
497
|
+
payloadString = JSON.stringify(request.payload);
|
|
498
|
+
} catch (e) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (!parsedPayload || !parsedPayload.event || !parsedPayload.merchant_id || !parsedPayload.timestamp) {
|
|
491
505
|
return false;
|
|
492
506
|
}
|
|
493
|
-
const computedSignature = this.computeHmacSignature(
|
|
507
|
+
const computedSignature = this.computeHmacSignature(payloadString, request.secret);
|
|
494
508
|
return this.timingSafeEqual(request.signature, computedSignature);
|
|
495
|
-
} catch (
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const error = err;
|
|
496
511
|
if (this.config.environment === "development") {
|
|
497
|
-
console.error("Webhook verification error:", error);
|
|
512
|
+
console.error("Webhook verification error:", error?.message || String(error));
|
|
498
513
|
}
|
|
499
514
|
return false;
|
|
500
515
|
}
|
|
@@ -664,6 +679,147 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
664
679
|
secret
|
|
665
680
|
});
|
|
666
681
|
}
|
|
682
|
+
|
|
683
|
+
// src/webhook-handler.ts
|
|
684
|
+
var import_crypto2 = require("crypto");
|
|
685
|
+
var processedWebhooks = /* @__PURE__ */ new Set();
|
|
686
|
+
var defaultIsProcessed = async (webhookId) => {
|
|
687
|
+
return processedWebhooks.has(webhookId);
|
|
688
|
+
};
|
|
689
|
+
var defaultOnProcessed = async (webhookId) => {
|
|
690
|
+
processedWebhooks.add(webhookId);
|
|
691
|
+
if (processedWebhooks.size > 1e4) {
|
|
692
|
+
const iterator = processedWebhooks.values();
|
|
693
|
+
for (let i = 0; i < 1e3; i++) {
|
|
694
|
+
const { value } = iterator.next();
|
|
695
|
+
if (value) processedWebhooks.delete(value);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
function generateWebhookId(payload) {
|
|
700
|
+
return `${payload.merchant_id}:${payload.event}:${payload.timestamp}`;
|
|
701
|
+
}
|
|
702
|
+
async function processPayload(payload, handlers, config) {
|
|
703
|
+
try {
|
|
704
|
+
const webhookId = generateWebhookId(payload);
|
|
705
|
+
const isProcessed = config.isProcessed || config.checkDuplicate || defaultIsProcessed;
|
|
706
|
+
const onProcessed = config.onProcessed || config.markProcessed || defaultOnProcessed;
|
|
707
|
+
const dedupEnabled = !!(config.enableDeduplication || config.isProcessed || config.checkDuplicate);
|
|
708
|
+
if (dedupEnabled && await isProcessed(webhookId)) {
|
|
709
|
+
return {
|
|
710
|
+
success: false,
|
|
711
|
+
processed: false,
|
|
712
|
+
event: payload.event,
|
|
713
|
+
error: "Duplicate webhook",
|
|
714
|
+
statusCode: 409
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const handler = handlers[payload.event];
|
|
718
|
+
if (!handler) {
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
processed: false,
|
|
722
|
+
event: payload.event,
|
|
723
|
+
statusCode: 200
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
await handler(payload.data, payload);
|
|
727
|
+
await onProcessed(webhookId);
|
|
728
|
+
return {
|
|
729
|
+
success: true,
|
|
730
|
+
processed: true,
|
|
731
|
+
event: payload.event
|
|
732
|
+
};
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const err = error;
|
|
735
|
+
if (config?.onError) {
|
|
736
|
+
await config.onError(err, error?.event);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
success: false,
|
|
740
|
+
processed: false,
|
|
741
|
+
error: err.message,
|
|
742
|
+
event: error?.event,
|
|
743
|
+
statusCode: 500
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function processWebhook(a, b, c) {
|
|
748
|
+
if (a && typeof a === "object" && a.event && b && c) {
|
|
749
|
+
return processPayload(a, b, c);
|
|
750
|
+
}
|
|
751
|
+
const opts = a;
|
|
752
|
+
if (!opts || !opts.signature && !opts.body && !opts.handlers) {
|
|
753
|
+
return {
|
|
754
|
+
success: false,
|
|
755
|
+
processed: false,
|
|
756
|
+
error: "Invalid arguments to processWebhook",
|
|
757
|
+
statusCode: 400
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const signature = opts.signature;
|
|
761
|
+
const body = opts.body;
|
|
762
|
+
const handlers = opts.handlers || {};
|
|
763
|
+
const cfg = opts.config || {};
|
|
764
|
+
const secret = cfg.webhookSecret || cfg.secret;
|
|
765
|
+
if (!secret) {
|
|
766
|
+
return {
|
|
767
|
+
success: false,
|
|
768
|
+
processed: false,
|
|
769
|
+
error: "Webhook secret not provided",
|
|
770
|
+
statusCode: 400
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (!signature || !body) {
|
|
774
|
+
return {
|
|
775
|
+
success: false,
|
|
776
|
+
processed: false,
|
|
777
|
+
error: "Missing signature or body",
|
|
778
|
+
statusCode: 400
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const sig = typeof signature === "string" && signature.startsWith("sha256=") ? signature.slice("sha256=".length) : String(signature);
|
|
783
|
+
const hmac = (0, import_crypto2.createHmac)("sha256", secret).update(body, "utf8").digest("hex");
|
|
784
|
+
let ok = false;
|
|
785
|
+
try {
|
|
786
|
+
const sigBuf = Buffer.from(sig, "hex");
|
|
787
|
+
const hmacBuf = Buffer.from(hmac, "hex");
|
|
788
|
+
if (sigBuf.length === hmacBuf.length) {
|
|
789
|
+
ok = (0, import_crypto2.timingSafeEqual)(sigBuf, hmacBuf);
|
|
790
|
+
}
|
|
791
|
+
} catch (e) {
|
|
792
|
+
ok = (0, import_crypto2.timingSafeEqual)(Buffer.from(String(sig), "utf8"), Buffer.from(hmac, "utf8"));
|
|
793
|
+
}
|
|
794
|
+
if (!ok) {
|
|
795
|
+
return {
|
|
796
|
+
success: false,
|
|
797
|
+
processed: false,
|
|
798
|
+
error: "Invalid signature",
|
|
799
|
+
statusCode: 401
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const payload = JSON.parse(body);
|
|
803
|
+
const fullConfig = {
|
|
804
|
+
secret,
|
|
805
|
+
isProcessed: cfg.isProcessed,
|
|
806
|
+
onProcessed: cfg.onProcessed,
|
|
807
|
+
onError: cfg.onError,
|
|
808
|
+
// Forward compatibility for alternate names and flags
|
|
809
|
+
enableDeduplication: cfg.enableDeduplication,
|
|
810
|
+
checkDuplicate: cfg.checkDuplicate,
|
|
811
|
+
markProcessed: cfg.markProcessed
|
|
812
|
+
};
|
|
813
|
+
return await processPayload(payload, handlers, fullConfig);
|
|
814
|
+
} catch (err) {
|
|
815
|
+
return {
|
|
816
|
+
success: false,
|
|
817
|
+
processed: false,
|
|
818
|
+
error: err.message,
|
|
819
|
+
statusCode: 500
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
}
|
|
667
823
|
// Annotate the CommonJS export names for ESM import in node:
|
|
668
824
|
0 && (module.exports = {
|
|
669
825
|
AuthenticationError,
|
|
@@ -673,6 +829,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
673
829
|
ValidationError,
|
|
674
830
|
ZendFiClient,
|
|
675
831
|
ZendFiError,
|
|
832
|
+
processWebhook,
|
|
676
833
|
verifyExpressWebhook,
|
|
677
834
|
verifyNextWebhook,
|
|
678
835
|
verifyWebhookSignature,
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
1
|
+
import {
|
|
2
|
+
__require,
|
|
3
|
+
processWebhook
|
|
4
|
+
} from "./chunk-YFOBPGQE.mjs";
|
|
7
5
|
|
|
8
6
|
// src/client.ts
|
|
9
7
|
import fetch from "cross-fetch";
|
|
@@ -283,14 +281,15 @@ var ZendFiClient = class {
|
|
|
283
281
|
};
|
|
284
282
|
}
|
|
285
283
|
/**
|
|
286
|
-
* List all payment links
|
|
284
|
+
* List all payment links for the authenticated merchant
|
|
287
285
|
*/
|
|
288
286
|
async listPaymentLinks() {
|
|
289
|
-
|
|
287
|
+
const response = await this.request("GET", "/api/v1/payment-links");
|
|
288
|
+
return response.map((link) => ({
|
|
289
|
+
...link,
|
|
290
|
+
url: link.hosted_page_url
|
|
291
|
+
}));
|
|
290
292
|
}
|
|
291
|
-
// ===================================================================
|
|
292
|
-
// INSTALLMENT PLANS - Pay over time
|
|
293
|
-
// ===================================================================
|
|
294
293
|
/**
|
|
295
294
|
* Create an installment plan
|
|
296
295
|
* Split a purchase into multiple scheduled payments
|
|
@@ -341,9 +340,6 @@ var ZendFiClient = class {
|
|
|
341
340
|
`/api/v1/installment-plans/${planId}/cancel`
|
|
342
341
|
);
|
|
343
342
|
}
|
|
344
|
-
// ===================================================================
|
|
345
|
-
// ESCROW - Secure fund holding
|
|
346
|
-
// ===================================================================
|
|
347
343
|
/**
|
|
348
344
|
* Create an escrow transaction
|
|
349
345
|
* Hold funds until conditions are met
|
|
@@ -393,9 +389,6 @@ var ZendFiClient = class {
|
|
|
393
389
|
async disputeEscrow(escrowId, request) {
|
|
394
390
|
return this.request("POST", `/api/v1/escrows/${escrowId}/dispute`, request);
|
|
395
391
|
}
|
|
396
|
-
// ===================================================================
|
|
397
|
-
// INVOICES - Professional billing
|
|
398
|
-
// ===================================================================
|
|
399
392
|
/**
|
|
400
393
|
* Create an invoice
|
|
401
394
|
*/
|
|
@@ -447,15 +440,34 @@ var ZendFiClient = class {
|
|
|
447
440
|
if (!request.payload || !request.signature || !request.secret) {
|
|
448
441
|
return false;
|
|
449
442
|
}
|
|
450
|
-
|
|
451
|
-
|
|
443
|
+
let payloadString;
|
|
444
|
+
let parsedPayload = null;
|
|
445
|
+
if (typeof request.payload === "string") {
|
|
446
|
+
payloadString = request.payload;
|
|
447
|
+
try {
|
|
448
|
+
parsedPayload = JSON.parse(payloadString);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
} else if (typeof request.payload === "object") {
|
|
453
|
+
parsedPayload = request.payload;
|
|
454
|
+
try {
|
|
455
|
+
payloadString = JSON.stringify(request.payload);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
452
460
|
return false;
|
|
453
461
|
}
|
|
454
|
-
|
|
462
|
+
if (!parsedPayload || !parsedPayload.event || !parsedPayload.merchant_id || !parsedPayload.timestamp) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const computedSignature = this.computeHmacSignature(payloadString, request.secret);
|
|
455
466
|
return this.timingSafeEqual(request.signature, computedSignature);
|
|
456
|
-
} catch (
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const error = err;
|
|
457
469
|
if (this.config.environment === "development") {
|
|
458
|
-
console.error("Webhook verification error:", error);
|
|
470
|
+
console.error("Webhook verification error:", error?.message || String(error));
|
|
459
471
|
}
|
|
460
472
|
return false;
|
|
461
473
|
}
|
|
@@ -633,6 +645,7 @@ export {
|
|
|
633
645
|
ValidationError,
|
|
634
646
|
ZendFiClient,
|
|
635
647
|
ZendFiError,
|
|
648
|
+
processWebhook,
|
|
636
649
|
verifyExpressWebhook,
|
|
637
650
|
verifyNextWebhook,
|
|
638
651
|
verifyWebhookSignature,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { W as WebhookHandlerConfig, a as WebhookHandlers } from './webhook-handler-BIze3Qop.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js Webhook Handler for App Router
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // app/api/webhooks/zendfi/route.ts
|
|
9
|
+
* import { createNextWebhookHandler } from '@zendfi/sdk/nextjs';
|
|
10
|
+
*
|
|
11
|
+
* export const POST = createNextWebhookHandler({
|
|
12
|
+
* secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
13
|
+
* handlers: {
|
|
14
|
+
* 'payment.confirmed': async (payment) => {
|
|
15
|
+
* await db.orders.update({
|
|
16
|
+
* where: { id: payment.metadata.orderId },
|
|
17
|
+
* data: { status: 'paid' },
|
|
18
|
+
* });
|
|
19
|
+
* },
|
|
20
|
+
* 'payment.failed': async (payment) => {
|
|
21
|
+
* await sendFailureEmail(payment);
|
|
22
|
+
* },
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type NextRequest = any;
|
|
29
|
+
interface NextWebhookHandlerConfig extends WebhookHandlerConfig {
|
|
30
|
+
handlers: WebhookHandlers;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a Next.js App Router webhook handler
|
|
34
|
+
*/
|
|
35
|
+
declare function createNextWebhookHandler(config: NextWebhookHandlerConfig): (request: NextRequest) => Promise<Response>;
|
|
36
|
+
|
|
37
|
+
export { type NextWebhookHandlerConfig, createNextWebhookHandler };
|
package/dist/nextjs.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { W as WebhookHandlerConfig, a as WebhookHandlers } from './webhook-handler-BIze3Qop.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js Webhook Handler for App Router
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // app/api/webhooks/zendfi/route.ts
|
|
9
|
+
* import { createNextWebhookHandler } from '@zendfi/sdk/nextjs';
|
|
10
|
+
*
|
|
11
|
+
* export const POST = createNextWebhookHandler({
|
|
12
|
+
* secret: process.env.ZENDFI_WEBHOOK_SECRET!,
|
|
13
|
+
* handlers: {
|
|
14
|
+
* 'payment.confirmed': async (payment) => {
|
|
15
|
+
* await db.orders.update({
|
|
16
|
+
* where: { id: payment.metadata.orderId },
|
|
17
|
+
* data: { status: 'paid' },
|
|
18
|
+
* });
|
|
19
|
+
* },
|
|
20
|
+
* 'payment.failed': async (payment) => {
|
|
21
|
+
* await sendFailureEmail(payment);
|
|
22
|
+
* },
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type NextRequest = any;
|
|
29
|
+
interface NextWebhookHandlerConfig extends WebhookHandlerConfig {
|
|
30
|
+
handlers: WebhookHandlers;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a Next.js App Router webhook handler
|
|
34
|
+
*/
|
|
35
|
+
declare function createNextWebhookHandler(config: NextWebhookHandlerConfig): (request: NextRequest) => Promise<Response>;
|
|
36
|
+
|
|
37
|
+
export { type NextWebhookHandlerConfig, createNextWebhookHandler };
|
package/dist/nextjs.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
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/nextjs.ts
|
|
21
|
+
var nextjs_exports = {};
|
|
22
|
+
__export(nextjs_exports, {
|
|
23
|
+
createNextWebhookHandler: () => createNextWebhookHandler
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(nextjs_exports);
|
|
26
|
+
var import_crypto2 = require("crypto");
|
|
27
|
+
|
|
28
|
+
// src/webhook-handler.ts
|
|
29
|
+
var import_crypto = require("crypto");
|
|
30
|
+
var processedWebhooks = /* @__PURE__ */ new Set();
|
|
31
|
+
var defaultIsProcessed = async (webhookId) => {
|
|
32
|
+
return processedWebhooks.has(webhookId);
|
|
33
|
+
};
|
|
34
|
+
var defaultOnProcessed = async (webhookId) => {
|
|
35
|
+
processedWebhooks.add(webhookId);
|
|
36
|
+
if (processedWebhooks.size > 1e4) {
|
|
37
|
+
const iterator = processedWebhooks.values();
|
|
38
|
+
for (let i = 0; i < 1e3; i++) {
|
|
39
|
+
const { value } = iterator.next();
|
|
40
|
+
if (value) processedWebhooks.delete(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function generateWebhookId(payload) {
|
|
45
|
+
return `${payload.merchant_id}:${payload.event}:${payload.timestamp}`;
|
|
46
|
+
}
|
|
47
|
+
async function processPayload(payload, handlers, config) {
|
|
48
|
+
try {
|
|
49
|
+
const webhookId = generateWebhookId(payload);
|
|
50
|
+
const isProcessed = config.isProcessed || config.checkDuplicate || defaultIsProcessed;
|
|
51
|
+
const onProcessed = config.onProcessed || config.markProcessed || defaultOnProcessed;
|
|
52
|
+
const dedupEnabled = !!(config.enableDeduplication || config.isProcessed || config.checkDuplicate);
|
|
53
|
+
if (dedupEnabled && await isProcessed(webhookId)) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
processed: false,
|
|
57
|
+
event: payload.event,
|
|
58
|
+
error: "Duplicate webhook",
|
|
59
|
+
statusCode: 409
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const handler = handlers[payload.event];
|
|
63
|
+
if (!handler) {
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
processed: false,
|
|
67
|
+
event: payload.event,
|
|
68
|
+
statusCode: 200
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
await handler(payload.data, payload);
|
|
72
|
+
await onProcessed(webhookId);
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
processed: true,
|
|
76
|
+
event: payload.event
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const err = error;
|
|
80
|
+
if (config?.onError) {
|
|
81
|
+
await config.onError(err, error?.event);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
processed: false,
|
|
86
|
+
error: err.message,
|
|
87
|
+
event: error?.event,
|
|
88
|
+
statusCode: 500
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function processWebhook(a, b, c) {
|
|
93
|
+
if (a && typeof a === "object" && a.event && b && c) {
|
|
94
|
+
return processPayload(a, b, c);
|
|
95
|
+
}
|
|
96
|
+
const opts = a;
|
|
97
|
+
if (!opts || !opts.signature && !opts.body && !opts.handlers) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
processed: false,
|
|
101
|
+
error: "Invalid arguments to processWebhook",
|
|
102
|
+
statusCode: 400
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const signature = opts.signature;
|
|
106
|
+
const body = opts.body;
|
|
107
|
+
const handlers = opts.handlers || {};
|
|
108
|
+
const cfg = opts.config || {};
|
|
109
|
+
const secret = cfg.webhookSecret || cfg.secret;
|
|
110
|
+
if (!secret) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
processed: false,
|
|
114
|
+
error: "Webhook secret not provided",
|
|
115
|
+
statusCode: 400
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (!signature || !body) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
processed: false,
|
|
122
|
+
error: "Missing signature or body",
|
|
123
|
+
statusCode: 400
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const sig = typeof signature === "string" && signature.startsWith("sha256=") ? signature.slice("sha256=".length) : String(signature);
|
|
128
|
+
const hmac = (0, import_crypto.createHmac)("sha256", secret).update(body, "utf8").digest("hex");
|
|
129
|
+
let ok = false;
|
|
130
|
+
try {
|
|
131
|
+
const sigBuf = Buffer.from(sig, "hex");
|
|
132
|
+
const hmacBuf = Buffer.from(hmac, "hex");
|
|
133
|
+
if (sigBuf.length === hmacBuf.length) {
|
|
134
|
+
ok = (0, import_crypto.timingSafeEqual)(sigBuf, hmacBuf);
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
ok = (0, import_crypto.timingSafeEqual)(Buffer.from(String(sig), "utf8"), Buffer.from(hmac, "utf8"));
|
|
138
|
+
}
|
|
139
|
+
if (!ok) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
processed: false,
|
|
143
|
+
error: "Invalid signature",
|
|
144
|
+
statusCode: 401
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const payload = JSON.parse(body);
|
|
148
|
+
const fullConfig = {
|
|
149
|
+
secret,
|
|
150
|
+
isProcessed: cfg.isProcessed,
|
|
151
|
+
onProcessed: cfg.onProcessed,
|
|
152
|
+
onError: cfg.onError,
|
|
153
|
+
// Forward compatibility for alternate names and flags
|
|
154
|
+
enableDeduplication: cfg.enableDeduplication,
|
|
155
|
+
checkDuplicate: cfg.checkDuplicate,
|
|
156
|
+
markProcessed: cfg.markProcessed
|
|
157
|
+
};
|
|
158
|
+
return await processPayload(payload, handlers, fullConfig);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
processed: false,
|
|
163
|
+
error: err.message,
|
|
164
|
+
statusCode: 500
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/nextjs.ts
|
|
170
|
+
function createNextWebhookHandler(config) {
|
|
171
|
+
return async (request) => {
|
|
172
|
+
try {
|
|
173
|
+
const signature = request.headers.get("x-zendfi-signature");
|
|
174
|
+
if (!signature) {
|
|
175
|
+
return new Response(
|
|
176
|
+
JSON.stringify({ error: "Missing signature" }),
|
|
177
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const body = await request.text();
|
|
181
|
+
const computedSignature = (0, import_crypto2.createHmac)("sha256", config.secret).update(body, "utf8").digest("hex");
|
|
182
|
+
const sigBuffer = Buffer.from(signature, "utf8");
|
|
183
|
+
const compBuffer = Buffer.from(computedSignature, "utf8");
|
|
184
|
+
if (sigBuffer.length !== compBuffer.length || !(0, import_crypto2.timingSafeEqual)(sigBuffer, compBuffer)) {
|
|
185
|
+
return new Response(
|
|
186
|
+
JSON.stringify({ error: "Invalid signature" }),
|
|
187
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
let payload;
|
|
191
|
+
try {
|
|
192
|
+
payload = JSON.parse(body);
|
|
193
|
+
} catch {
|
|
194
|
+
return new Response(
|
|
195
|
+
JSON.stringify({ error: "Invalid JSON" }),
|
|
196
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const result = await processWebhook(payload, config.handlers, config);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
return new Response(
|
|
202
|
+
JSON.stringify({ error: result.error || "Webhook processing failed" }),
|
|
203
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return new Response(
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
received: true,
|
|
209
|
+
processed: result.processed,
|
|
210
|
+
event: result.event
|
|
211
|
+
}),
|
|
212
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
213
|
+
);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const err = error;
|
|
216
|
+
console.error("Webhook handler error:", err);
|
|
217
|
+
return new Response(
|
|
218
|
+
JSON.stringify({ error: "Internal server error" }),
|
|
219
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
225
|
+
0 && (module.exports = {
|
|
226
|
+
createNextWebhookHandler
|
|
227
|
+
});
|