@thepayulink/server 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 ADDED
@@ -0,0 +1,134 @@
1
+ # @thepayulink/server
2
+
3
+ Server-side SDK for Elite Yatra / PayuLink. Create orders with a **fixed amount**,
4
+ check payment status, and verify signatures & webhooks — all with your secret key.
5
+ This is the backend half of the checkout, analogous to the `razorpay` Node SDK.
6
+
7
+ > The **secret key never leaves your backend.** The client SDK only ever receives an `order_id`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @thepayulink/server
13
+ ```
14
+
15
+ Node 18+ (uses global `fetch`). Works with `import` and `require`.
16
+
17
+ ## Quick start
18
+
19
+ ```js
20
+ import Payulink from '@thepayulink/server';
21
+
22
+ const ey = new Payulink({
23
+ keyId: process.env.PAYULINK_PUBLISHABLE_KEY, // pl_pk_… (optional)
24
+ keySecret: process.env.PAYULINK_SECRET_KEY, // pl_sk_… (required, backend only)
25
+ apiBase: 'https://eliteyatra.vip',
26
+ });
27
+
28
+ // 1) Create an order for the TRUE amount (computed on your server).
29
+ const order = await ey.orders.create({
30
+ amount: 19900, // rupees
31
+ item: 'Kailash Mansarovar Yatra — 2 travellers',
32
+ customer: { name: 'Asha', mobile: '9876543210' },
33
+ });
34
+ // -> { order_id, key_id, amount, item, status: 'created' }
35
+
36
+ // 2) Send order.order_id to your client → open it in the client SDK.
37
+
38
+ // 3) Later, confirm the payment authoritatively before fulfilment.
39
+ const status = await ey.orders.fetch(order.order_id);
40
+ if (status.paid) {
41
+ // deliver the booking — this reflects PayuLink's webhook-verified status
42
+ }
43
+ ```
44
+
45
+ ### Express example
46
+
47
+ ```js
48
+ import express from 'express';
49
+ import Payulink from '@thepayulink/server';
50
+
51
+ const ey = new Payulink({ keySecret: process.env.PAYULINK_SECRET_KEY });
52
+ const app = express();
53
+ app.use(express.json());
54
+
55
+ // Client asks your backend to start a payment.
56
+ app.post('/create-order', async (req, res) => {
57
+ const pkg = await db.getPackage(req.body.packageId); // trusted price
58
+ const order = await ey.orders.create({
59
+ amount: pkg.bookingAmount * (req.body.travellers || 1),
60
+ item: pkg.title,
61
+ customer: req.body.customer,
62
+ });
63
+ res.json({ orderId: order.order_id, keyId: order.key_id });
64
+ });
65
+
66
+ // Client reports success → confirm before trusting it.
67
+ app.post('/confirm', async (req, res) => {
68
+ const s = await ey.orders.fetch(req.body.orderId);
69
+ res.json({ paid: s.paid, utr: s.utr });
70
+ });
71
+ ```
72
+
73
+ ## Verifying a payment
74
+
75
+ Two equivalent ways, pick one:
76
+
77
+ **A. Fetch the order (simplest, always correct):**
78
+
79
+ ```js
80
+ const s = await ey.orders.fetch(orderId);
81
+ if (s.paid) { /* confirmed */ }
82
+ ```
83
+
84
+ **B. Verify the signature from the client success callback (Razorpay-style):**
85
+
86
+ The client success payload includes `{ order_id, utr, signature }`. Verify it:
87
+
88
+ ```js
89
+ const ok = ey.verifyPaymentSignature({ order_id, utr, signature });
90
+ if (ok) { /* authentic success */ }
91
+ ```
92
+
93
+ ## Webhooks
94
+
95
+ Set `PAYULINK_MERCHANT_WEBHOOK_URL` on the pay server to your endpoint. It receives
96
+ signed events; verify with the raw body and the `x-payulink-signature` header:
97
+
98
+ ```js
99
+ // Use a raw body parser on this route so the signature matches byte-for-byte.
100
+ app.post('/webhooks/eliteyatra', express.raw({ type: '*/*' }), (req, res) => {
101
+ let event;
102
+ try {
103
+ event = ey.webhooks.constructEvent(req.body, req.get('x-payulink-signature'));
104
+ } catch (e) {
105
+ return res.status(400).send('Invalid signature');
106
+ }
107
+
108
+ if (event.event === 'payin.verified') {
109
+ // event.data: { order_id, status, utr, amount, item }
110
+ fulfilBooking(event.data.order_id);
111
+ }
112
+ res.sendStatus(200);
113
+ });
114
+ ```
115
+
116
+ Events: `payin.verified` (paid), `payin.failed`, `payin.expired`, `payin.cancelled`.
117
+
118
+ ## API
119
+
120
+ | Method | Returns | Notes |
121
+ | --- | --- | --- |
122
+ | `orders.create({ amount, item?, customer? })` | `Order` | Amount is fixed server-side. |
123
+ | `orders.fetch(orderId)` | `OrderStatus` | Authoritative status; `.paid` boolean. |
124
+ | `orders.isPaid(orderId)` | `boolean` | Convenience. |
125
+ | `payments.fetch(orderId)` | `OrderStatus` | Alias of `orders.fetch`. |
126
+ | `verifyPaymentSignature({ order_id, utr, signature })` | `boolean` | Verify client success handoff. |
127
+ | `webhooks.constructEvent(rawBody, signature)` | `WebhookEvent` | Throws on bad signature. |
128
+ | `webhooks.verify(rawBody, signature)` | `boolean` | Non-throwing check. |
129
+
130
+ Errors throw `PayulinkError` with `.status` and `.body`.
131
+
132
+ ## License
133
+
134
+ UNLICENSED — internal to Elite Yatra.
@@ -0,0 +1,81 @@
1
+ export interface PayulinkConfig {
2
+ /** Secret key (pl_sk_…). Required. Backend only. */
3
+ keySecret: string;
4
+ /** Publishable key (pl_pk_…). Optional. */
5
+ keyId?: string;
6
+ /** Pay server base URL. Default https://eliteyatra.vip. */
7
+ apiBase?: string;
8
+ /** Secret to verify inbound webhooks (defaults to keySecret). */
9
+ webhookSecret?: string;
10
+ /** Request timeout in ms. Default 15000. */
11
+ timeout?: number;
12
+ }
13
+
14
+ export interface CreateOrderParams {
15
+ /** Amount in rupees. Required. */
16
+ amount: number;
17
+ item?: string;
18
+ customer?: { name?: string; mobile?: string; email?: string };
19
+ }
20
+
21
+ export interface Order {
22
+ order_id: string;
23
+ key_id: string | null;
24
+ amount: number;
25
+ item: string;
26
+ status: string;
27
+ }
28
+
29
+ export interface OrderStatus {
30
+ merchant_order_no: string;
31
+ /** 0=SUCCESS, 1=FAILED, 2=PENDING, 3=EXPIRED, … */
32
+ status: number;
33
+ status_label: string;
34
+ paid: boolean;
35
+ utr: string | null;
36
+ amount: number;
37
+ item: string;
38
+ signature: string | null;
39
+ expires_at: number | null;
40
+ server_now: number;
41
+ }
42
+
43
+ export interface WebhookEvent {
44
+ id: string;
45
+ event: string;
46
+ created_at: number;
47
+ data: {
48
+ order_id: string;
49
+ status: number;
50
+ status_label: string;
51
+ utr: string | null;
52
+ amount: number;
53
+ item: string;
54
+ };
55
+ }
56
+
57
+ export class PayulinkError extends Error {
58
+ status?: number;
59
+ body?: unknown;
60
+ }
61
+
62
+ export default class Payulink {
63
+ constructor(config: PayulinkConfig);
64
+
65
+ orders: {
66
+ create(params: CreateOrderParams): Promise<Order>;
67
+ fetch(orderId: string): Promise<OrderStatus>;
68
+ isPaid(orderId: string): Promise<boolean>;
69
+ };
70
+
71
+ payments: {
72
+ fetch(orderId: string): Promise<OrderStatus>;
73
+ };
74
+
75
+ webhooks: {
76
+ verify(rawBody: string | Buffer, signature: string, secret?: string): boolean;
77
+ constructEvent(rawBody: string | Buffer, signature: string, secret?: string): WebhookEvent;
78
+ };
79
+
80
+ verifyPaymentSignature(p: { order_id: string; utr?: string | null; signature: string }): boolean;
81
+ }
@@ -0,0 +1,162 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.js
30
+ var src_exports = {};
31
+ __export(src_exports, {
32
+ PayulinkError: () => PayulinkError,
33
+ default: () => Payulink
34
+ });
35
+ module.exports = __toCommonJS(src_exports);
36
+ var import_node_crypto = __toESM(require("node:crypto"), 1);
37
+ var PayulinkError = class extends Error {
38
+ constructor(message, { status, body } = {}) {
39
+ super(message);
40
+ this.name = "PayulinkError";
41
+ this.status = status;
42
+ this.body = body;
43
+ }
44
+ };
45
+ function safeEqual(a, b) {
46
+ const ba = Buffer.from(String(a || ""));
47
+ const bb = Buffer.from(String(b || ""));
48
+ return ba.length === bb.length && ba.length > 0 && import_node_crypto.default.timingSafeEqual(ba, bb);
49
+ }
50
+ var Payulink = class {
51
+ /**
52
+ * @param {object} opts
53
+ * @param {string} opts.keySecret Secret key (pl_sk_…). Required. Backend only.
54
+ * @param {string} [opts.keyId] Publishable key (pl_pk_…). Optional.
55
+ * @param {string} [opts.apiBase] Pay server URL. Default https://eliteyatra.vip.
56
+ * @param {string} [opts.webhookSecret] Secret used to verify inbound webhooks (defaults to keySecret).
57
+ * @param {number} [opts.timeout] Request timeout in ms. Default 15000.
58
+ */
59
+ constructor(opts = {}) {
60
+ if (!opts.keySecret) throw new PayulinkError("`keySecret` (pl_sk_\u2026) is required");
61
+ this.keySecret = opts.keySecret;
62
+ this.keyId = opts.keyId || null;
63
+ this.apiBase = String(opts.apiBase || "https://eliteyatra.vip").replace(/\/$/, "");
64
+ this.webhookSecret = opts.webhookSecret || opts.keySecret;
65
+ this.timeout = opts.timeout || 15e3;
66
+ this.orders = {
67
+ create: (params) => this._createOrder(params),
68
+ fetch: (orderId) => this._fetchOrder(orderId),
69
+ isPaid: async (orderId) => (await this._fetchOrder(orderId)).paid === true
70
+ };
71
+ this.payments = { fetch: (orderId) => this._fetchOrder(orderId) };
72
+ this.webhooks = {
73
+ verify: (rawBody, signature, secret) => this._verifyWebhook(rawBody, signature, secret),
74
+ constructEvent: (rawBody, signature, secret) => this._constructEvent(rawBody, signature, secret)
75
+ };
76
+ }
77
+ async _request(method, path, { body, auth } = {}) {
78
+ const ctrl = new AbortController();
79
+ const t = setTimeout(() => ctrl.abort(), this.timeout);
80
+ let res;
81
+ try {
82
+ res = await fetch(`${this.apiBase}${path}`, {
83
+ method,
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ ...auth ? { Authorization: `Bearer ${this.keySecret}` } : {},
87
+ ...this.keyId ? { "x-payulink-key": this.keyId } : {}
88
+ },
89
+ body: body ? JSON.stringify(body) : void 0,
90
+ signal: ctrl.signal
91
+ });
92
+ } catch (e) {
93
+ clearTimeout(t);
94
+ throw new PayulinkError(`Network error: ${e.message}`);
95
+ }
96
+ clearTimeout(t);
97
+ let data = {};
98
+ try {
99
+ data = await res.json();
100
+ } catch {
101
+ }
102
+ if (!res.ok) throw new PayulinkError(data.error || `Request failed (HTTP ${res.status})`, { status: res.status, body: data });
103
+ return data;
104
+ }
105
+ /**
106
+ * Create an order with a server-fixed amount. The client only receives the
107
+ * returned `order_id` and cannot change the amount.
108
+ * @param {object} params { amount (rupees), item?, customer? }
109
+ * @returns {Promise<{order_id, key_id, amount, item, status}>}
110
+ */
111
+ _createOrder(params = {}) {
112
+ const amount = Math.round(Number(params.amount) || 0);
113
+ if (!(amount >= 1)) throw new PayulinkError("`amount` (in rupees) is required and must be >= 1");
114
+ return this._request("POST", "/api/orders", {
115
+ auth: true,
116
+ body: { amount, item: params.item, customer: params.customer }
117
+ });
118
+ }
119
+ /**
120
+ * Fetch the authoritative status of an order.
121
+ * @returns {Promise<{merchant_order_no, status, status_label, paid, utr, amount, signature}>}
122
+ */
123
+ _fetchOrder(orderId) {
124
+ if (!orderId) throw new PayulinkError("orderId is required");
125
+ return this._request("GET", `/api/order/${encodeURIComponent(orderId)}`);
126
+ }
127
+ /**
128
+ * Verify a payment handoff signature returned to the client on success.
129
+ * Mirrors Razorpay's signature check.
130
+ * @param {object} p { order_id, utr, signature }
131
+ * @returns {boolean}
132
+ */
133
+ verifyPaymentSignature({ order_id, utr, signature } = {}) {
134
+ if (!order_id || !signature) return false;
135
+ const expected = import_node_crypto.default.createHmac("sha256", this.keySecret).update(`${order_id}|${utr || ""}`).digest("hex");
136
+ return safeEqual(expected, signature);
137
+ }
138
+ _verifyWebhook(rawBody, signature, secret) {
139
+ const expected = import_node_crypto.default.createHmac("sha256", secret || this.webhookSecret).update(String(rawBody)).digest("hex");
140
+ return safeEqual(expected, signature);
141
+ }
142
+ /**
143
+ * Verify an inbound webhook and return the parsed event. Throws if the
144
+ * signature is invalid. Pass the RAW request body (string/Buffer) and the
145
+ * `x-payulink-signature` header.
146
+ */
147
+ _constructEvent(rawBody, signature, secret) {
148
+ if (!this._verifyWebhook(rawBody, signature, secret)) {
149
+ throw new PayulinkError("Invalid webhook signature", { status: 401 });
150
+ }
151
+ try {
152
+ return JSON.parse(String(rawBody));
153
+ } catch {
154
+ throw new PayulinkError("Invalid webhook JSON", { status: 400 });
155
+ }
156
+ }
157
+ };
158
+ // Annotate the CommonJS export names for ESM import in node:
159
+ 0 && (module.exports = {
160
+ PayulinkError
161
+ });
162
+ (()=>{const ns=module.exports,C=ns.default;if(C){C.PayulinkError=ns.PayulinkError;C.Payulink=C;C.default=C;module.exports=C;}})();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@thepayulink/server",
3
+ "version": "1.0.0",
4
+ "description": "Server-side SDK for Elite Yatra / PayuLink — create orders, check payment status, and verify signatures & webhooks with your secret key.",
5
+ "type": "module",
6
+ "main": "./dist/payulink-server.cjs",
7
+ "module": "./src/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./src/index.js",
13
+ "require": "./dist/payulink-server.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "src",
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "build": "node build.mjs"
26
+ },
27
+ "keywords": [
28
+ "eliteyatra",
29
+ "payulink",
30
+ "payment",
31
+ "server",
32
+ "sdk",
33
+ "upi",
34
+ "orders",
35
+ "webhooks"
36
+ ],
37
+ "author": "Elite Yatra",
38
+ "license": "MIT",
39
+ "devDependencies": {
40
+ "esbuild": "^0.23.0"
41
+ }
42
+ }
package/src/index.js ADDED
@@ -0,0 +1,136 @@
1
+ // @thepayulink/server — backend SDK for Elite Yatra / PayuLink.
2
+ //
3
+ // import Payulink from '@thepayulink/server';
4
+ // const ey = new Payulink({ keyId: 'pl_pk_…', keySecret: 'pl_sk_…' });
5
+ // const order = await ey.orders.create({ amount: 19900, item: 'Kailash Yatra' });
6
+ // // …open `order.order_id` in the client SDK…
7
+ // const status = await ey.orders.fetch(order.order_id); // { paid, status, utr, … }
8
+ //
9
+ // Requires Node 18+ (global fetch).
10
+ import crypto from 'node:crypto';
11
+
12
+ export class PayulinkError extends Error {
13
+ constructor(message, { status, body } = {}) {
14
+ super(message);
15
+ this.name = 'PayulinkError';
16
+ this.status = status;
17
+ this.body = body;
18
+ }
19
+ }
20
+
21
+ function safeEqual(a, b) {
22
+ const ba = Buffer.from(String(a || ''));
23
+ const bb = Buffer.from(String(b || ''));
24
+ return ba.length === bb.length && ba.length > 0 && crypto.timingSafeEqual(ba, bb);
25
+ }
26
+
27
+ export default class Payulink {
28
+ /**
29
+ * @param {object} opts
30
+ * @param {string} opts.keySecret Secret key (pl_sk_…). Required. Backend only.
31
+ * @param {string} [opts.keyId] Publishable key (pl_pk_…). Optional.
32
+ * @param {string} [opts.apiBase] Pay server URL. Default https://eliteyatra.vip.
33
+ * @param {string} [opts.webhookSecret] Secret used to verify inbound webhooks (defaults to keySecret).
34
+ * @param {number} [opts.timeout] Request timeout in ms. Default 15000.
35
+ */
36
+ constructor(opts = {}) {
37
+ if (!opts.keySecret) throw new PayulinkError('`keySecret` (pl_sk_…) is required');
38
+ this.keySecret = opts.keySecret;
39
+ this.keyId = opts.keyId || null;
40
+ this.apiBase = String(opts.apiBase || 'https://eliteyatra.vip').replace(/\/$/, '');
41
+ this.webhookSecret = opts.webhookSecret || opts.keySecret;
42
+ this.timeout = opts.timeout || 15000;
43
+
44
+ this.orders = {
45
+ create: (params) => this._createOrder(params),
46
+ fetch: (orderId) => this._fetchOrder(orderId),
47
+ isPaid: async (orderId) => (await this._fetchOrder(orderId)).paid === true,
48
+ };
49
+ // Alias for Razorpay-familiar naming.
50
+ this.payments = { fetch: (orderId) => this._fetchOrder(orderId) };
51
+ this.webhooks = {
52
+ verify: (rawBody, signature, secret) => this._verifyWebhook(rawBody, signature, secret),
53
+ constructEvent: (rawBody, signature, secret) => this._constructEvent(rawBody, signature, secret),
54
+ };
55
+ }
56
+
57
+ async _request(method, path, { body, auth } = {}) {
58
+ const ctrl = new AbortController();
59
+ const t = setTimeout(() => ctrl.abort(), this.timeout);
60
+ let res;
61
+ try {
62
+ res = await fetch(`${this.apiBase}${path}`, {
63
+ method,
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ ...(auth ? { Authorization: `Bearer ${this.keySecret}` } : {}),
67
+ ...(this.keyId ? { 'x-payulink-key': this.keyId } : {}),
68
+ },
69
+ body: body ? JSON.stringify(body) : undefined,
70
+ signal: ctrl.signal,
71
+ });
72
+ } catch (e) {
73
+ clearTimeout(t);
74
+ throw new PayulinkError(`Network error: ${e.message}`);
75
+ }
76
+ clearTimeout(t);
77
+ let data = {};
78
+ try { data = await res.json(); } catch { /* non-JSON */ }
79
+ if (!res.ok) throw new PayulinkError(data.error || `Request failed (HTTP ${res.status})`, { status: res.status, body: data });
80
+ return data;
81
+ }
82
+
83
+ /**
84
+ * Create an order with a server-fixed amount. The client only receives the
85
+ * returned `order_id` and cannot change the amount.
86
+ * @param {object} params { amount (rupees), item?, customer? }
87
+ * @returns {Promise<{order_id, key_id, amount, item, status}>}
88
+ */
89
+ _createOrder(params = {}) {
90
+ const amount = Math.round(Number(params.amount) || 0);
91
+ if (!(amount >= 1)) throw new PayulinkError('`amount` (in rupees) is required and must be >= 1');
92
+ return this._request('POST', '/api/orders', {
93
+ auth: true,
94
+ body: { amount, item: params.item, customer: params.customer },
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Fetch the authoritative status of an order.
100
+ * @returns {Promise<{merchant_order_no, status, status_label, paid, utr, amount, signature}>}
101
+ */
102
+ _fetchOrder(orderId) {
103
+ if (!orderId) throw new PayulinkError('orderId is required');
104
+ return this._request('GET', `/api/order/${encodeURIComponent(orderId)}`);
105
+ }
106
+
107
+ /**
108
+ * Verify a payment handoff signature returned to the client on success.
109
+ * Mirrors Razorpay's signature check.
110
+ * @param {object} p { order_id, utr, signature }
111
+ * @returns {boolean}
112
+ */
113
+ verifyPaymentSignature({ order_id, utr, signature } = {}) {
114
+ if (!order_id || !signature) return false;
115
+ const expected = crypto.createHmac('sha256', this.keySecret).update(`${order_id}|${utr || ''}`).digest('hex');
116
+ return safeEqual(expected, signature);
117
+ }
118
+
119
+ _verifyWebhook(rawBody, signature, secret) {
120
+ const expected = crypto.createHmac('sha256', secret || this.webhookSecret).update(String(rawBody)).digest('hex');
121
+ return safeEqual(expected, signature);
122
+ }
123
+
124
+ /**
125
+ * Verify an inbound webhook and return the parsed event. Throws if the
126
+ * signature is invalid. Pass the RAW request body (string/Buffer) and the
127
+ * `x-payulink-signature` header.
128
+ */
129
+ _constructEvent(rawBody, signature, secret) {
130
+ if (!this._verifyWebhook(rawBody, signature, secret)) {
131
+ throw new PayulinkError('Invalid webhook signature', { status: 401 });
132
+ }
133
+ try { return JSON.parse(String(rawBody)); }
134
+ catch { throw new PayulinkError('Invalid webhook JSON', { status: 400 }); }
135
+ }
136
+ }