@pandait.tech/payment-nuvei 0.4.1 → 1.0.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 +43 -1
- package/dist/handlers/index.cjs +22 -1
- package/dist/handlers/index.cjs.map +1 -1
- package/dist/handlers/index.d.cts +15 -3
- package/dist/handlers/index.d.ts +15 -3
- package/dist/handlers/index.js +22 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/payment-links/index.cjs.map +1 -1
- package/dist/payment-links/index.d.cts +1 -27
- package/dist/payment-links/index.d.ts +1 -27
- package/dist/payment-links/index.js.map +1 -1
- package/package.json +3 -2
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:
|
|
@@ -285,7 +312,22 @@ The handlers read/write a single `orders` collection (plus `promotions` for coup
|
|
|
285
312
|
|
|
286
313
|
Public, supported entry points: the package root (SDK), `./handlers`, `./adapters`, `./payment-links`, `./ui`, and `./ui/styles.css`. Everything else (e.g. internal `http.ts`) is private and may change without notice.
|
|
287
314
|
|
|
288
|
-
|
|
315
|
+
**As of `1.0.0` the public API is frozen under semver** — breaking changes to the
|
|
316
|
+
exported factories, their `*HandlerDeps` interfaces, the SDK functions, or the UI
|
|
317
|
+
component props require a major version bump. See `CHANGELOG.md`.
|
|
318
|
+
|
|
319
|
+
Design notes consumers should know:
|
|
320
|
+
|
|
321
|
+
- **Order shape is generic.** `MinimalOrder` extends `Record<string, unknown>` and
|
|
322
|
+
declares only fields the package itself reads/writes. Your business-specific
|
|
323
|
+
order fields are yours to read off the order inside your callbacks
|
|
324
|
+
(`onPaymentSucceeded`, `validateCustomOrder`, `getRetryUrl`) — narrow them there.
|
|
325
|
+
- **Payment links are primitives only.** The package ships token/expiry/price-bound
|
|
326
|
+
helpers; the payment-link *document shape* (what it points to) is yours to define.
|
|
327
|
+
- **The webhook secret is optional but strongly recommended.** It's left optional so
|
|
328
|
+
the package never forces a specific webhook-URL scheme, but an unconfigured
|
|
329
|
+
webhook is forgeable — always set `webhookSecret` in production (see "Securing the
|
|
330
|
+
webhook" above).
|
|
289
331
|
|
|
290
332
|
## Migration from `pauhenriques-website`
|
|
291
333
|
|
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;
|