@pandait.tech/payment-nuvei 0.4.1 → 0.5.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 +27 -0
- package/dist/handlers/index.cjs +22 -1
- package/dist/handlers/index.cjs.map +1 -1
- package/dist/handlers/index.d.cts +13 -0
- package/dist/handlers/index.d.ts +13 -0
- package/dist/handlers/index.js +22 -1
- package/dist/handlers/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -177,6 +177,19 @@ interface NuveiWebhookPayload {
|
|
|
177
177
|
interface WebhookHandlerDeps {
|
|
178
178
|
firebase: FirebaseDeps;
|
|
179
179
|
email: Pick<EmailService, "sendPaymentConfirmation" | "sendPaymentPending">;
|
|
180
|
+
/**
|
|
181
|
+
* Shared secret used to authenticate the incoming webhook. Paymentez/Nuvei
|
|
182
|
+
* DMNs are NOT signed, so the only robust auth is a secret the merchant
|
|
183
|
+
* embeds in the DMN URL configured in the Nuvei dashboard, e.g.
|
|
184
|
+
* `https://shop.com/api/webhooks/nuvei?key=THE_SECRET` (also accepted as the
|
|
185
|
+
* `x-webhook-key` header). Falls back to `process.env.NUVEI_WEBHOOK_SECRET`.
|
|
186
|
+
*
|
|
187
|
+
* If set, requests without a matching key are rejected with 401. If unset,
|
|
188
|
+
* the handler logs a loud warning and processes anyway (the endpoint is then
|
|
189
|
+
* forgeable — anyone who knows an orderId could fake a "paid" notification).
|
|
190
|
+
* STRONGLY RECOMMENDED in production.
|
|
191
|
+
*/
|
|
192
|
+
webhookSecret?: string;
|
|
180
193
|
/** Optional post-success hook. Fires when the webhook transitions an order to "paid" (either via direct status_detail=3 or via verify BY_CRES). Used for enrollment, fulfillment triggers, paymentLink usage tracking, etc. */
|
|
181
194
|
onPaymentSucceeded?: (order: MinimalOrder & {
|
|
182
195
|
id: string;
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -177,6 +177,19 @@ interface NuveiWebhookPayload {
|
|
|
177
177
|
interface WebhookHandlerDeps {
|
|
178
178
|
firebase: FirebaseDeps;
|
|
179
179
|
email: Pick<EmailService, "sendPaymentConfirmation" | "sendPaymentPending">;
|
|
180
|
+
/**
|
|
181
|
+
* Shared secret used to authenticate the incoming webhook. Paymentez/Nuvei
|
|
182
|
+
* DMNs are NOT signed, so the only robust auth is a secret the merchant
|
|
183
|
+
* embeds in the DMN URL configured in the Nuvei dashboard, e.g.
|
|
184
|
+
* `https://shop.com/api/webhooks/nuvei?key=THE_SECRET` (also accepted as the
|
|
185
|
+
* `x-webhook-key` header). Falls back to `process.env.NUVEI_WEBHOOK_SECRET`.
|
|
186
|
+
*
|
|
187
|
+
* If set, requests without a matching key are rejected with 401. If unset,
|
|
188
|
+
* the handler logs a loud warning and processes anyway (the endpoint is then
|
|
189
|
+
* forgeable — anyone who knows an orderId could fake a "paid" notification).
|
|
190
|
+
* STRONGLY RECOMMENDED in production.
|
|
191
|
+
*/
|
|
192
|
+
webhookSecret?: string;
|
|
180
193
|
/** Optional post-success hook. Fires when the webhook transitions an order to "paid" (either via direct status_detail=3 or via verify BY_CRES). Used for enrollment, fulfillment triggers, paymentLink usage tracking, etc. */
|
|
181
194
|
onPaymentSucceeded?: (order: MinimalOrder & {
|
|
182
195
|
id: string;
|
package/dist/handlers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FieldValue } from 'firebase-admin/firestore';
|
|
2
1
|
import crypto from 'crypto';
|
|
2
|
+
import { FieldValue } from 'firebase-admin/firestore';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
// src/http.ts
|
|
@@ -10,6 +10,15 @@ function json(data, init) {
|
|
|
10
10
|
}
|
|
11
11
|
return new Response(JSON.stringify(data), { ...init, headers });
|
|
12
12
|
}
|
|
13
|
+
function timingSafeEqualStr(a, b) {
|
|
14
|
+
const ab = Buffer.from(a, "utf8");
|
|
15
|
+
const bb = Buffer.from(b, "utf8");
|
|
16
|
+
if (ab.length !== bb.length) {
|
|
17
|
+
crypto.timingSafeEqual(ab, ab);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
21
|
+
}
|
|
13
22
|
function getCookie(request, name) {
|
|
14
23
|
const header = request.headers.get("cookie");
|
|
15
24
|
if (!header) return void 0;
|
|
@@ -688,8 +697,20 @@ function extractCres(payload) {
|
|
|
688
697
|
function createWebhookHandler(deps) {
|
|
689
698
|
const logger = deps.logger ?? console;
|
|
690
699
|
const { db } = deps.firebase;
|
|
700
|
+
const expectedSecret = deps.webhookSecret ?? process.env.NUVEI_WEBHOOK_SECRET;
|
|
691
701
|
return async function POST(request) {
|
|
692
702
|
try {
|
|
703
|
+
if (expectedSecret) {
|
|
704
|
+
const provided = new URL(request.url).searchParams.get("key") ?? request.headers.get("x-webhook-key") ?? "";
|
|
705
|
+
if (!timingSafeEqualStr(provided, expectedSecret)) {
|
|
706
|
+
logger.warn("[webhook] rejected: missing/invalid webhook secret");
|
|
707
|
+
return json({ error: "Unauthorized" }, { status: 401 });
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
logger.warn(
|
|
711
|
+
"[webhook] NUVEI_WEBHOOK_SECRET is not configured \u2014 this endpoint is UNAUTHENTICATED and forgeable. Set a secret (deps.webhookSecret or env) and append it to your DMN URL as ?key=... before going to production."
|
|
712
|
+
);
|
|
713
|
+
}
|
|
693
714
|
const payload = await request.json();
|
|
694
715
|
logger.log("[webhook] Full payload:", JSON.stringify(payload));
|
|
695
716
|
const { transaction } = payload;
|