@pandait.tech/payment-nuvei 0.4.0 → 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/dist/ui/styles.css +1 -1
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -110,6 +110,33 @@ export const POST = createChargeHandler({
|
|
|
110
110
|
|
|
111
111
|
Apply the same pattern to the remaining handlers — each one is documented inline with its own `XHandlerDeps` interface.
|
|
112
112
|
|
|
113
|
+
### Securing the webhook (important)
|
|
114
|
+
|
|
115
|
+
Paymentez/Nuvei DMNs (webhooks) are **not signed**, so the webhook endpoint must be protected with a shared secret — otherwise anyone who knows an `orderId` could POST a fake "paid" notification.
|
|
116
|
+
|
|
117
|
+
1. Set a secret and pass it to the handler (or via `NUVEI_WEBHOOK_SECRET`):
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
export const POST = createWebhookHandler({
|
|
121
|
+
firebase: { db: dbAdmin, auth: authAdmin },
|
|
122
|
+
email: { sendPaymentConfirmation, sendPaymentPending },
|
|
123
|
+
webhookSecret: process.env.NUVEI_WEBHOOK_SECRET,
|
|
124
|
+
onPaymentSucceeded: async (order) => { /* enrollment, fulfillment, ... */ },
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
2. In the **Nuvei dashboard**, configure your DMN/callback URL with the secret as a query param:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
https://your-shop.com/api/webhooks/nuvei?key=YOUR_SECRET
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
(The secret is also accepted as the `x-webhook-key` header.)
|
|
135
|
+
|
|
136
|
+
Requests without a matching key get `401` and are not processed (constant-time compared). If `webhookSecret` is **unset**, the handler still processes but logs a loud warning — treat that as not-production-ready.
|
|
137
|
+
|
|
138
|
+
> `onPaymentSucceeded` fires once, on the transition into `paid` (idempotent across Nuvei's webhook retries).
|
|
139
|
+
|
|
113
140
|
## Hosting & deployment (3DS callback + Nuvei egress)
|
|
114
141
|
|
|
115
142
|
This package is **hosting-agnostic by design** — the payment logic doesn't care where it runs. But two parts of the Nuvei flow may need to run *outside your app's request edge* depending on **where you host**, because of how the host's infrastructure (not this package) handles certain traffic:
|
package/dist/handlers/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var firestore = require('firebase-admin/firestore');
|
|
4
3
|
var crypto = require('crypto');
|
|
4
|
+
var firestore = require('firebase-admin/firestore');
|
|
5
5
|
var zod = require('zod');
|
|
6
6
|
|
|
7
7
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -16,6 +16,15 @@ function json(data, init) {
|
|
|
16
16
|
}
|
|
17
17
|
return new Response(JSON.stringify(data), { ...init, headers });
|
|
18
18
|
}
|
|
19
|
+
function timingSafeEqualStr(a, b) {
|
|
20
|
+
const ab = Buffer.from(a, "utf8");
|
|
21
|
+
const bb = Buffer.from(b, "utf8");
|
|
22
|
+
if (ab.length !== bb.length) {
|
|
23
|
+
crypto__default.default.timingSafeEqual(ab, ab);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return crypto__default.default.timingSafeEqual(ab, bb);
|
|
27
|
+
}
|
|
19
28
|
function getCookie(request, name) {
|
|
20
29
|
const header = request.headers.get("cookie");
|
|
21
30
|
if (!header) return void 0;
|
|
@@ -694,8 +703,20 @@ function extractCres(payload) {
|
|
|
694
703
|
function createWebhookHandler(deps) {
|
|
695
704
|
const logger = deps.logger ?? console;
|
|
696
705
|
const { db } = deps.firebase;
|
|
706
|
+
const expectedSecret = deps.webhookSecret ?? process.env.NUVEI_WEBHOOK_SECRET;
|
|
697
707
|
return async function POST(request) {
|
|
698
708
|
try {
|
|
709
|
+
if (expectedSecret) {
|
|
710
|
+
const provided = new URL(request.url).searchParams.get("key") ?? request.headers.get("x-webhook-key") ?? "";
|
|
711
|
+
if (!timingSafeEqualStr(provided, expectedSecret)) {
|
|
712
|
+
logger.warn("[webhook] rejected: missing/invalid webhook secret");
|
|
713
|
+
return json({ error: "Unauthorized" }, { status: 401 });
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
logger.warn(
|
|
717
|
+
"[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."
|
|
718
|
+
);
|
|
719
|
+
}
|
|
699
720
|
const payload = await request.json();
|
|
700
721
|
logger.log("[webhook] Full payload:", JSON.stringify(payload));
|
|
701
722
|
const { transaction } = payload;
|