@thepayulink/server 2.0.1 → 2.1.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 +3 -2
- package/dist/index.d.ts +12 -5
- package/dist/payulink-server.cjs +15 -7
- package/package.json +1 -1
- package/src/index.js +22 -8
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ Node 18+ (uses global `fetch`). Works with `import` and `require`.
|
|
|
22
22
|
import PayuLink, { rupees } from '@thepayulink/server';
|
|
23
23
|
|
|
24
24
|
const pl = new PayuLink({
|
|
25
|
-
keyId: process.env.PAYULINK_KEY_ID,
|
|
26
|
-
keySecret: process.env.PAYULINK_KEY_SECRET,
|
|
25
|
+
keyId: process.env.PAYULINK_KEY_ID, // pl_live_… / pl_test_…
|
|
26
|
+
keySecret: process.env.PAYULINK_KEY_SECRET, // backend only — never ship this
|
|
27
|
+
webhookSecret: process.env.PAYULINK_WEBHOOK_SECRET, // whs_… from panel → Config (NOT keySecret)
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
// 1) Create an order for the TRUE amount, computed on your server.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,12 @@ export interface PayuLinkOptions {
|
|
|
7
7
|
keySecret: string;
|
|
8
8
|
/** Gateway base URL. Default https://payulink.io/api */
|
|
9
9
|
apiBase?: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Webhook signing secret (webhook_secret_v2, whs_…) from merchant panel → Config.
|
|
12
|
+
* Required to verify webhooks — it is NOT your keySecret. Pass [newSecret, oldSecret]
|
|
13
|
+
* while rotating.
|
|
14
|
+
*/
|
|
15
|
+
webhookSecret?: string | string[];
|
|
12
16
|
/** Request timeout in ms. Default 15000. */
|
|
13
17
|
timeout?: number;
|
|
14
18
|
}
|
|
@@ -24,7 +28,10 @@ export interface CreateOrderParams {
|
|
|
24
28
|
notes?: Record<string, unknown>;
|
|
25
29
|
/** Optional customer prefill. */
|
|
26
30
|
customer?: { name?: string; contact?: string; email?: string };
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated Only affects legacy version-1 callbacks. Signed (v2) webhooks always go to
|
|
33
|
+
* the URL configured in the merchant panel.
|
|
34
|
+
*/
|
|
28
35
|
notifyUrl?: string;
|
|
29
36
|
}
|
|
30
37
|
|
|
@@ -90,9 +97,9 @@ export default class PayuLink {
|
|
|
90
97
|
};
|
|
91
98
|
|
|
92
99
|
webhooks: {
|
|
93
|
-
verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string): boolean;
|
|
100
|
+
verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string | string[]): boolean;
|
|
94
101
|
/** Verifies signature + ±5min timestamp tolerance. Throws PayuLinkError if invalid. */
|
|
95
|
-
constructEvent(rawBody: string | Buffer, headers: Record<string, any>, secret?: string): WebhookEvent;
|
|
102
|
+
constructEvent(rawBody: string | Buffer, headers: Record<string, any>, secret?: string | string[]): WebhookEvent;
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
/** signature = HMAC_SHA256(order_id + "|" + payment_id, key_secret) */
|
package/dist/payulink-server.cjs
CHANGED
|
@@ -58,8 +58,10 @@ var PayuLink = class {
|
|
|
58
58
|
* @param {string} opts.keyId Publishable key (pl_live_… / pl_test_…).
|
|
59
59
|
* @param {string} opts.keySecret Secret key. BACKEND ONLY — never ship this.
|
|
60
60
|
* @param {string} [opts.apiBase] Default https://payulink.io/api
|
|
61
|
-
* @param {string} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2
|
|
62
|
-
* merchant panel).
|
|
61
|
+
* @param {string|string[]} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2,
|
|
62
|
+
* whs_…, from merchant panel → Config). Required to verify
|
|
63
|
+
* webhooks. Pass [new, old] while rotating — see constructEvent.
|
|
64
|
+
* It is NOT your keySecret: the two are different secrets.
|
|
63
65
|
* @param {number} [opts.timeout] Request timeout ms. Default 15000.
|
|
64
66
|
*/
|
|
65
67
|
constructor(opts = {}) {
|
|
@@ -68,7 +70,7 @@ var PayuLink = class {
|
|
|
68
70
|
this.keyId = opts.keyId;
|
|
69
71
|
this.keySecret = opts.keySecret;
|
|
70
72
|
this.apiBase = String(opts.apiBase || "https://payulink.io/api").replace(/\/$/, "");
|
|
71
|
-
this.webhookSecret = opts.webhookSecret ||
|
|
73
|
+
this.webhookSecret = opts.webhookSecret || null;
|
|
72
74
|
this.timeout = opts.timeout || 15e3;
|
|
73
75
|
this.mode = /^pl_test_/.test(opts.keyId) ? "test" : "live";
|
|
74
76
|
this.orders = {
|
|
@@ -125,6 +127,10 @@ var PayuLink = class {
|
|
|
125
127
|
* @param {string} [p.receipt] Your reference. Unique per merchant — reusing it
|
|
126
128
|
* returns the SAME order, which makes retries safe.
|
|
127
129
|
* @param {object} [p.notes] Arbitrary key/value metadata.
|
|
130
|
+
* @param {object} [p.customer] { name, contact, email } — prefill for the checkout.
|
|
131
|
+
*
|
|
132
|
+
* Webhooks always go to the URL set in the merchant panel. (`notifyUrl` is still sent for
|
|
133
|
+
* backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
|
|
128
134
|
*/
|
|
129
135
|
_createOrder(p = {}) {
|
|
130
136
|
const amount = Math.round(Number(p.amount));
|
|
@@ -176,9 +182,11 @@ var PayuLink = class {
|
|
|
176
182
|
*/
|
|
177
183
|
_verifyWebhook(rawBody, signature, timestamp, secret) {
|
|
178
184
|
if (!signature || !timestamp) return false;
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
const keys = [].concat(secret || this.webhookSecret || []).filter(Boolean);
|
|
186
|
+
if (keys.length === 0) {
|
|
187
|
+
throw new PayuLinkError("No webhook secret: pass `webhookSecret` (whs_\u2026, merchant panel \u2192 Config) to new PayuLink({...}). Webhooks are not signed with your keySecret.");
|
|
188
|
+
}
|
|
189
|
+
return keys.some((key) => safeEqual("sha256=" + import_node_crypto.default.createHmac("sha256", key).update(`${timestamp}.${String(rawBody)}`).digest("hex"), signature));
|
|
182
190
|
}
|
|
183
191
|
/**
|
|
184
192
|
* Verify + parse a webhook. Throws if the signature is bad or the timestamp is
|
|
@@ -194,7 +202,7 @@ var PayuLink = class {
|
|
|
194
202
|
*
|
|
195
203
|
* @param {string|Buffer} rawBody
|
|
196
204
|
* @param {object} headers The request headers object.
|
|
197
|
-
* @param {string} [secret]
|
|
205
|
+
* @param {string|string[]} [secret] Overrides opts.webhookSecret.
|
|
198
206
|
* @param {number} [toleranceSec=300]
|
|
199
207
|
*/
|
|
200
208
|
_constructEvent(rawBody, headers = {}, secret, toleranceSec = 300) {
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -41,8 +41,10 @@ export default class PayuLink {
|
|
|
41
41
|
* @param {string} opts.keyId Publishable key (pl_live_… / pl_test_…).
|
|
42
42
|
* @param {string} opts.keySecret Secret key. BACKEND ONLY — never ship this.
|
|
43
43
|
* @param {string} [opts.apiBase] Default https://payulink.io/api
|
|
44
|
-
* @param {string} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2
|
|
45
|
-
* merchant panel).
|
|
44
|
+
* @param {string|string[]} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2,
|
|
45
|
+
* whs_…, from merchant panel → Config). Required to verify
|
|
46
|
+
* webhooks. Pass [new, old] while rotating — see constructEvent.
|
|
47
|
+
* It is NOT your keySecret: the two are different secrets.
|
|
46
48
|
* @param {number} [opts.timeout] Request timeout ms. Default 15000.
|
|
47
49
|
*/
|
|
48
50
|
constructor(opts = {}) {
|
|
@@ -51,7 +53,9 @@ export default class PayuLink {
|
|
|
51
53
|
this.keyId = opts.keyId;
|
|
52
54
|
this.keySecret = opts.keySecret;
|
|
53
55
|
this.apiBase = String(opts.apiBase || 'https://payulink.io/api').replace(/\/$/, '');
|
|
54
|
-
|
|
56
|
+
// No fallback to keySecret. Webhooks are never signed with it, so the old fallback
|
|
57
|
+
// turned a missing env var into "every webhook fails signature" with no clue why.
|
|
58
|
+
this.webhookSecret = opts.webhookSecret || null;
|
|
55
59
|
this.timeout = opts.timeout || 15000;
|
|
56
60
|
this.mode = /^pl_test_/.test(opts.keyId) ? 'test' : 'live';
|
|
57
61
|
|
|
@@ -109,6 +113,10 @@ export default class PayuLink {
|
|
|
109
113
|
* @param {string} [p.receipt] Your reference. Unique per merchant — reusing it
|
|
110
114
|
* returns the SAME order, which makes retries safe.
|
|
111
115
|
* @param {object} [p.notes] Arbitrary key/value metadata.
|
|
116
|
+
* @param {object} [p.customer] { name, contact, email } — prefill for the checkout.
|
|
117
|
+
*
|
|
118
|
+
* Webhooks always go to the URL set in the merchant panel. (`notifyUrl` is still sent for
|
|
119
|
+
* backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
|
|
112
120
|
*/
|
|
113
121
|
_createOrder(p = {}) {
|
|
114
122
|
const amount = Math.round(Number(p.amount));
|
|
@@ -165,10 +173,16 @@ export default class PayuLink {
|
|
|
165
173
|
*/
|
|
166
174
|
_verifyWebhook(rawBody, signature, timestamp, secret) {
|
|
167
175
|
if (!signature || !timestamp) return false;
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
const keys = [].concat(secret || this.webhookSecret || []).filter(Boolean);
|
|
177
|
+
if (keys.length === 0) {
|
|
178
|
+
throw new PayuLinkError('No webhook secret: pass `webhookSecret` (whs_…, merchant panel → Config) '
|
|
179
|
+
+ 'to new PayuLink({...}). Webhooks are not signed with your keySecret.');
|
|
180
|
+
}
|
|
181
|
+
// Several secrets are accepted so a rotation never drops events: deliveries already
|
|
182
|
+
// queued (and their retries, for up to ~30h) stay signed with the secret that was
|
|
183
|
+
// current when each event was created.
|
|
184
|
+
return keys.some((key) => safeEqual('sha256=' + crypto.createHmac('sha256', key)
|
|
185
|
+
.update(`${timestamp}.${String(rawBody)}`).digest('hex'), signature));
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
/**
|
|
@@ -185,7 +199,7 @@ export default class PayuLink {
|
|
|
185
199
|
*
|
|
186
200
|
* @param {string|Buffer} rawBody
|
|
187
201
|
* @param {object} headers The request headers object.
|
|
188
|
-
* @param {string} [secret]
|
|
202
|
+
* @param {string|string[]} [secret] Overrides opts.webhookSecret.
|
|
189
203
|
* @param {number} [toleranceSec=300]
|
|
190
204
|
*/
|
|
191
205
|
_constructEvent(rawBody, headers = {}, secret, toleranceSec = 300) {
|