@lacspace/esewa 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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/esewa
4
+
5
+ **eSewa ePay v2 (Nepal) — HMAC-SHA256 signing, checkout-form building, response verification & status checks, over Web Crypto.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/esewa?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/esewa)
8
+ [![install size](https://packagephobia.com/badge?p=@lacspace/esewa)](https://packagephobia.com/result?p=@lacspace/esewa)
9
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/esewa?label=minzip)](https://bundlephobia.com/package/@lacspace/esewa)
10
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/esewa)
11
+ [![license](https://img.shields.io/npm/l/@lacspace/esewa?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
12
+
13
+ </div>
14
+
15
+ > Integrate **eSewa** — Nepal's most-used payment gateway — the correct way. The signature eSewa demands (`HMAC-SHA256` over `total_amount,transaction_uuid,product_code`, base64) is trivial to get subtly wrong. This gets it right, builds the whole checkout form for you, verifies the signed response, and checks transaction status. Zero dependencies, isomorphic, fully typed.
16
+
17
+ - ✍️ **Correct signatures** — the exact `signed_field_names` message order, HMAC-SHA256, standard base64
18
+ - 🧾 **Form builder** — a ready-to-POST `{ action, method, fields }` with every field + a valid signature
19
+ - 🔎 **Verify responses** — decode & timing-safe-verify the base64 `data` payload eSewa returns on success
20
+ - 📡 **Status API** — query the transaction-status endpoint with an injectable `fetch`
21
+ - ⚡ Isomorphic — Node 20+, edge runtimes & browsers · Web Crypto only · 📦 ESM + CJS · zero deps
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @lacspace/esewa # or pnpm add / yarn add / bun add
27
+ ```
28
+
29
+ ## Build & post the checkout form
30
+
31
+ ```ts
32
+ import { buildForm, ESEWA_TEST_SECRET, ESEWA_TEST_PRODUCT_CODE } from "@lacspace/esewa";
33
+
34
+ const form = await buildForm(
35
+ {
36
+ amount: 100,
37
+ taxAmount: 0,
38
+ transactionUuid: crypto.randomUUID(),
39
+ productCode: ESEWA_TEST_PRODUCT_CODE,
40
+ successUrl: "https://myshop.np/esewa/success",
41
+ failureUrl: "https://myshop.np/esewa/failure",
42
+ },
43
+ { secret: ESEWA_TEST_SECRET, env: "test" },
44
+ );
45
+
46
+ // form.action → the eSewa endpoint, form.method → "POST"
47
+ // render form.fields as hidden <input>s and auto-submit.
48
+ ```
49
+
50
+ `total_amount` defaults to `amount + taxAmount + productServiceCharge + productDeliveryCharge`.
51
+
52
+ ## Verify the success redirect
53
+
54
+ ```ts
55
+ import { verifyResponse } from "@lacspace/esewa";
56
+
57
+ // eSewa redirects to your success_url with ?data=<base64 JSON>
58
+ const { valid, data } = await verifyResponse(url.searchParams.get("data")!, secret);
59
+ if (valid && data.status === "COMPLETE") {
60
+ fulfilOrder(data.transaction_uuid);
61
+ }
62
+ ```
63
+
64
+ `verifyResponse()` recomputes the signature over the fields named in the payload's own `signed_field_names` and compares it **timing-safe** — it never throws.
65
+
66
+ ## Check transaction status
67
+
68
+ ```ts
69
+ import { checkStatus } from "@lacspace/esewa";
70
+
71
+ const status = await checkStatus(
72
+ { product_code: "EPAYTEST", total_amount: 100, transaction_uuid: "11-201" },
73
+ { env: "test" },
74
+ );
75
+ // → { status: "COMPLETE", ... }
76
+ ```
77
+
78
+ ## API
79
+
80
+ | Export | Description |
81
+ | --- | --- |
82
+ | `signPayment({ total_amount, transaction_uuid, product_code }, secret)` | base64 HMAC-SHA256 signature |
83
+ | `buildForm(input, { secret, env? })` | `{ action, method, fields }` ready to POST |
84
+ | `verifyResponse(base64Data, secret)` | `{ valid, data }` — decode + timing-safe verify |
85
+ | `checkStatus(params, { env?, fetch? })` | GET the status API, returns parsed JSON |
86
+ | `ESEWA_FORM_URLS` / `ESEWA_STATUS_URLS` | `{ test, prod }` endpoint maps |
87
+ | `ESEWA_TEST_SECRET` / `ESEWA_TEST_PRODUCT_CODE` | sandbox credentials |
88
+ | `ESEWA_SIGNED_FIELD_NAMES` | `"total_amount,transaction_uuid,product_code"` |
89
+
90
+ `env` is `"test"` (default) or `"prod"`. Signatures use standard base64 (not url-safe), exactly as eSewa expects.
91
+
92
+ ## Licensing
93
+
94
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
95
+
96
+ Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
97
+
98
+ <!-- LACSPACE-DEV-PLATFORM -->
99
+
100
+ ---
101
+
102
+ ## The Lacspace Developer Platform
103
+
104
+ `@lacspace/esewa` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
105
+
106
+ - 🗂️ **All packages** — https://developer.lacspace.com/packages
107
+ - 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
108
+ - 🧪 **Live playground** — https://developer.lacspace.com/playground
109
+ - 🖥️ **Finished app templates** — https://templates.lacspace.com
110
+ - 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
111
+
112
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
package/dist/index.cjs ADDED
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var ESEWA_FORM_URLS = {
5
+ test: "https://rc-epay.esewa.com.np/api/epay/main/v2/form",
6
+ prod: "https://epay.esewa.com.np/api/epay/main/v2/form"
7
+ };
8
+ var ESEWA_STATUS_URLS = {
9
+ test: "https://rc.esewa.com.np/api/epay/transaction/status/",
10
+ prod: "https://epay.esewa.com.np/api/epay/transaction/status/"
11
+ };
12
+ var ESEWA_TEST_SECRET = "8gBm/:&EnhH.1/q@K@";
13
+ var ESEWA_TEST_PRODUCT_CODE = "EPAYTEST";
14
+ var ESEWA_SIGNED_FIELD_NAMES = "total_amount,transaction_uuid,product_code";
15
+ var enc = new TextEncoder();
16
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
17
+ function toBase64(bytes) {
18
+ let out = "";
19
+ for (let i = 0; i < bytes.length; i += 3) {
20
+ const b0 = bytes[i];
21
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
22
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
23
+ out += B64[b0 >> 2];
24
+ out += B64[(b0 & 3) << 4 | b1 >> 4];
25
+ out += i + 1 < bytes.length ? B64[(b1 & 15) << 2 | b2 >> 6] : "=";
26
+ out += i + 2 < bytes.length ? B64[b2 & 63] : "=";
27
+ }
28
+ return out;
29
+ }
30
+ function fromBase64(b64) {
31
+ const clean = b64.replace(/[^A-Za-z0-9+/]/g, "");
32
+ const len = Math.floor(clean.length * 3 / 4);
33
+ const bytes = new Uint8Array(len);
34
+ let p = 0;
35
+ for (let i = 0; i < clean.length; i += 4) {
36
+ const c0 = B64.indexOf(clean[i]);
37
+ const c1 = B64.indexOf(clean[i + 1]);
38
+ const c2 = i + 2 < clean.length ? B64.indexOf(clean[i + 2]) : -1;
39
+ const c3 = i + 3 < clean.length ? B64.indexOf(clean[i + 3]) : -1;
40
+ if (p < len) bytes[p++] = c0 << 2 | c1 >> 4;
41
+ if (c2 >= 0 && p < len) bytes[p++] = (c1 & 15) << 4 | c2 >> 2;
42
+ if (c3 >= 0 && p < len) bytes[p++] = (c2 & 3) << 6 | c3;
43
+ }
44
+ return bytes;
45
+ }
46
+ async function hmacSha256Base64(secret, message) {
47
+ const subtle = globalThis.crypto?.subtle;
48
+ if (!subtle) throw new Error("@lacspace/esewa: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.");
49
+ const key = await subtle.importKey(
50
+ "raw",
51
+ enc.encode(secret),
52
+ { name: "HMAC", hash: "SHA-256" },
53
+ false,
54
+ ["sign"]
55
+ );
56
+ const sig = await subtle.sign("HMAC", key, enc.encode(message));
57
+ return toBase64(new Uint8Array(sig));
58
+ }
59
+ function timingSafeEqual(a, b) {
60
+ const ab = enc.encode(a);
61
+ const bb = enc.encode(b);
62
+ let diff = ab.length ^ bb.length;
63
+ const n = Math.max(ab.length, bb.length);
64
+ for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
65
+ return diff === 0;
66
+ }
67
+ async function signPayment(fields, secret) {
68
+ const message = `total_amount=${fields.total_amount},transaction_uuid=${fields.transaction_uuid},product_code=${fields.product_code}`;
69
+ return hmacSha256Base64(secret, message);
70
+ }
71
+ async function buildForm(input, opts) {
72
+ const tax = input.taxAmount ?? 0;
73
+ const service = input.productServiceCharge ?? 0;
74
+ const delivery = input.productDeliveryCharge ?? 0;
75
+ const total = input.totalAmount ?? input.amount + tax + service + delivery;
76
+ const signature = await signPayment(
77
+ {
78
+ total_amount: total,
79
+ transaction_uuid: input.transactionUuid,
80
+ product_code: input.productCode
81
+ },
82
+ opts.secret
83
+ );
84
+ const fields = {
85
+ amount: String(input.amount),
86
+ tax_amount: String(tax),
87
+ total_amount: String(total),
88
+ transaction_uuid: input.transactionUuid,
89
+ product_code: input.productCode,
90
+ product_service_charge: String(service),
91
+ product_delivery_charge: String(delivery),
92
+ success_url: input.successUrl,
93
+ failure_url: input.failureUrl,
94
+ signed_field_names: ESEWA_SIGNED_FIELD_NAMES,
95
+ signature
96
+ };
97
+ return { action: ESEWA_FORM_URLS[opts.env ?? "test"], method: "POST", fields };
98
+ }
99
+ async function verifyResponse(base64Data, secret) {
100
+ let data;
101
+ try {
102
+ data = JSON.parse(new TextDecoder().decode(fromBase64(base64Data)));
103
+ } catch {
104
+ return { valid: false, data: {} };
105
+ }
106
+ const names = typeof data.signed_field_names === "string" ? data.signed_field_names : "";
107
+ const provided = typeof data.signature === "string" ? data.signature : "";
108
+ if (!names || !provided) return { valid: false, data };
109
+ const message = names.split(",").map((name) => `${name}=${data[name] ?? ""}`).join(",");
110
+ const expected = await hmacSha256Base64(secret, message);
111
+ return { valid: timingSafeEqual(expected, provided), data };
112
+ }
113
+ async function checkStatus(params, opts) {
114
+ const base = ESEWA_STATUS_URLS[opts?.env ?? "test"];
115
+ const qs = new URLSearchParams({
116
+ product_code: params.product_code,
117
+ total_amount: String(params.total_amount),
118
+ transaction_uuid: params.transaction_uuid
119
+ }).toString();
120
+ const url = `${base}?${qs}`;
121
+ const doFetch = opts?.fetch ?? globalThis.fetch;
122
+ if (!doFetch) throw new Error("@lacspace/esewa: global fetch is unavailable; pass opts.fetch.");
123
+ const res = await doFetch(url, { method: "GET" });
124
+ return res.json();
125
+ }
126
+
127
+ exports.ESEWA_FORM_URLS = ESEWA_FORM_URLS;
128
+ exports.ESEWA_SIGNED_FIELD_NAMES = ESEWA_SIGNED_FIELD_NAMES;
129
+ exports.ESEWA_STATUS_URLS = ESEWA_STATUS_URLS;
130
+ exports.ESEWA_TEST_PRODUCT_CODE = ESEWA_TEST_PRODUCT_CODE;
131
+ exports.ESEWA_TEST_SECRET = ESEWA_TEST_SECRET;
132
+ exports.buildForm = buildForm;
133
+ exports.checkStatus = checkStatus;
134
+ exports.signPayment = signPayment;
135
+ exports.verifyResponse = verifyResponse;
136
+ //# sourceMappingURL=index.cjs.map
137
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/esewa\n *\n * eSewa ePay v2 (Nepal) — the correct, tiny way to integrate Nepal's most-used\n * payment gateway. Handles the three things every integration re-implements:\n *\n * 1. **Signing** — the HMAC-SHA256 signature eSewa requires on the checkout\n * form, computed over `total_amount,transaction_uuid,product_code` in the\n * exact order of `signed_field_names`, base64-encoded.\n * 2. **Form building** — a ready-to-POST `{ action, method, fields }` object\n * with every field eSewa expects, including a valid `signature`.\n * 3. **Verification & status** — decode and verify the signed base64 `data`\n * payload eSewa returns on success (timing-safe), and query the\n * transaction-status API.\n *\n * Built on Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled\n * cryptography. Isomorphic: Node 20+, edge runtimes and browsers. Zero deps.\n */\n\n/* ------------------------------------------------------------------ *\n * Endpoints & test credentials\n * ------------------------------------------------------------------ */\n\nexport type EsewaEnv = \"test\" | \"prod\";\n\n/** eSewa checkout form endpoints (the URL you POST the form to). */\nexport const ESEWA_FORM_URLS: Record<EsewaEnv, string> = {\n test: \"https://rc-epay.esewa.com.np/api/epay/main/v2/form\",\n prod: \"https://epay.esewa.com.np/api/epay/main/v2/form\",\n};\n\n/** eSewa transaction-status API endpoints. */\nexport const ESEWA_STATUS_URLS: Record<EsewaEnv, string> = {\n test: \"https://rc.esewa.com.np/api/epay/transaction/status/\",\n prod: \"https://epay.esewa.com.np/api/epay/transaction/status/\",\n};\n\n/** eSewa-published sandbox secret key (test environment only). */\nexport const ESEWA_TEST_SECRET = \"8gBm/:&EnhH.1/q@K@\";\n\n/** eSewa-published sandbox merchant/product code (test environment only). */\nexport const ESEWA_TEST_PRODUCT_CODE = \"EPAYTEST\";\n\n/** The fields eSewa signs, in the required order. */\nexport const ESEWA_SIGNED_FIELD_NAMES = \"total_amount,transaction_uuid,product_code\";\n\n/* ------------------------------------------------------------------ *\n * Web Crypto + base64 helpers (isomorphic, zero-dependency)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** Standard base64 (NOT url-safe) encode of raw bytes. */\nfunction toBase64(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i]!;\n const b1 = i + 1 < bytes.length ? bytes[i + 1]! : 0;\n const b2 = i + 2 < bytes.length ? bytes[i + 2]! : 0;\n out += B64[b0 >> 2];\n out += B64[((b0 & 3) << 4) | (b1 >> 4)];\n out += i + 1 < bytes.length ? B64[((b1 & 15) << 2) | (b2 >> 6)] : \"=\";\n out += i + 2 < bytes.length ? B64[b2 & 63] : \"=\";\n }\n return out;\n}\n\n/** Standard base64 decode to raw bytes. */\nfunction fromBase64(b64: string): Uint8Array {\n const clean = b64.replace(/[^A-Za-z0-9+/]/g, \"\");\n const len = Math.floor((clean.length * 3) / 4);\n const bytes = new Uint8Array(len);\n let p = 0;\n for (let i = 0; i < clean.length; i += 4) {\n const c0 = B64.indexOf(clean[i]!);\n const c1 = B64.indexOf(clean[i + 1]!);\n const c2 = i + 2 < clean.length ? B64.indexOf(clean[i + 2]!) : -1;\n const c3 = i + 3 < clean.length ? B64.indexOf(clean[i + 3]!) : -1;\n if (p < len) bytes[p++] = (c0 << 2) | (c1 >> 4);\n if (c2 >= 0 && p < len) bytes[p++] = ((c1 & 15) << 4) | (c2 >> 2);\n if (c3 >= 0 && p < len) bytes[p++] = ((c2 & 3) << 6) | c3;\n }\n return bytes;\n}\n\n/** HMAC-SHA256 over `message` with `secret`, returned as standard base64. */\nasync function hmacSha256Base64(secret: string, message: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) throw new Error(\"@lacspace/esewa: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.\");\n const key = await subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await subtle.sign(\"HMAC\", key, enc.encode(message));\n return toBase64(new Uint8Array(sig));\n}\n\n/** Constant-time comparison of two strings (avoids signature-timing leaks). */\nfunction timingSafeEqual(a: string, b: string): boolean {\n const ab = enc.encode(a);\n const bb = enc.encode(b);\n // Compare a fixed number of bytes; length mismatch still fails.\n let diff = ab.length ^ bb.length;\n const n = Math.max(ab.length, bb.length);\n for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Signing\n * ------------------------------------------------------------------ */\n\nexport interface SignFields {\n total_amount: string | number;\n transaction_uuid: string;\n product_code: string;\n}\n\n/**\n * Compute the eSewa signature for the required signed fields. The message is\n * `total_amount=<v>,transaction_uuid=<v>,product_code=<v>` (the exact order of\n * `signed_field_names`), HMAC-SHA256'd with the merchant secret and returned as\n * standard base64.\n *\n * @example\n * const sig = await signPayment(\n * { total_amount: 100, transaction_uuid: \"11-201\", product_code: \"EPAYTEST\" },\n * ESEWA_TEST_SECRET,\n * );\n */\nexport async function signPayment(fields: SignFields, secret: string): Promise<string> {\n const message =\n `total_amount=${fields.total_amount},` +\n `transaction_uuid=${fields.transaction_uuid},` +\n `product_code=${fields.product_code}`;\n return hmacSha256Base64(secret, message);\n}\n\n/* ------------------------------------------------------------------ *\n * Form building\n * ------------------------------------------------------------------ */\n\nexport interface BuildFormInput {\n /** Base product amount. */\n amount: number;\n /** Tax amount. Default 0. */\n taxAmount?: number;\n /** Grand total. Defaults to amount + tax + service + delivery. */\n totalAmount?: number;\n /** Unique transaction id you generate. */\n transactionUuid: string;\n /** Merchant product code (e.g. \"EPAYTEST\" in test). */\n productCode: string;\n /** Where eSewa redirects on success. */\n successUrl: string;\n /** Where eSewa redirects on failure. */\n failureUrl: string;\n /** Product service charge. Default 0. */\n productServiceCharge?: number;\n /** Product delivery charge. Default 0. */\n productDeliveryCharge?: number;\n}\n\nexport interface EsewaForm {\n /** URL to POST the form to. */\n action: string;\n method: \"POST\";\n /** All fields eSewa expects, as strings ready for form inputs. */\n fields: Record<string, string>;\n}\n\n/**\n * Build a ready-to-POST eSewa checkout form: `{ action, method, fields }`. The\n * `total_amount` defaults to `amount + taxAmount + serviceCharge + deliveryCharge`,\n * and a valid `signature` is computed for you.\n *\n * @example\n * const form = await buildForm(\n * { amount: 100, transactionUuid: \"11-201\", productCode: ESEWA_TEST_PRODUCT_CODE,\n * successUrl: \"https://me/ok\", failureUrl: \"https://me/fail\" },\n * { secret: ESEWA_TEST_SECRET, env: \"test\" },\n * );\n * // render form.fields as hidden inputs and auto-submit to form.action\n */\nexport async function buildForm(\n input: BuildFormInput,\n opts: { secret: string; env?: EsewaEnv },\n): Promise<EsewaForm> {\n const tax = input.taxAmount ?? 0;\n const service = input.productServiceCharge ?? 0;\n const delivery = input.productDeliveryCharge ?? 0;\n const total = input.totalAmount ?? input.amount + tax + service + delivery;\n\n const signature = await signPayment(\n {\n total_amount: total,\n transaction_uuid: input.transactionUuid,\n product_code: input.productCode,\n },\n opts.secret,\n );\n\n const fields: Record<string, string> = {\n amount: String(input.amount),\n tax_amount: String(tax),\n total_amount: String(total),\n transaction_uuid: input.transactionUuid,\n product_code: input.productCode,\n product_service_charge: String(service),\n product_delivery_charge: String(delivery),\n success_url: input.successUrl,\n failure_url: input.failureUrl,\n signed_field_names: ESEWA_SIGNED_FIELD_NAMES,\n signature,\n };\n\n return { action: ESEWA_FORM_URLS[opts.env ?? \"test\"], method: \"POST\", fields };\n}\n\n/* ------------------------------------------------------------------ *\n * Response verification\n * ------------------------------------------------------------------ */\n\nexport interface VerifyResult {\n valid: boolean;\n /** The decoded response fields. */\n data: Record<string, unknown>;\n}\n\n/**\n * Verify the signed base64 `data` payload eSewa appends to the success redirect.\n * Decodes the JSON, recomputes the signature over the fields named in its own\n * `signed_field_names`, and compares (timing-safe) against its `signature`.\n * Never throws — returns `{ valid, data }`.\n *\n * @example\n * const { valid, data } = await verifyResponse(url.searchParams.get(\"data\")!, secret);\n * if (valid && data.status === \"COMPLETE\") fulfilOrder(data.transaction_uuid);\n */\nexport async function verifyResponse(base64Data: string, secret: string): Promise<VerifyResult> {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(new TextDecoder().decode(fromBase64(base64Data))) as Record<string, unknown>;\n } catch {\n return { valid: false, data: {} };\n }\n\n const names = typeof data.signed_field_names === \"string\" ? data.signed_field_names : \"\";\n const provided = typeof data.signature === \"string\" ? data.signature : \"\";\n if (!names || !provided) return { valid: false, data };\n\n const message = names\n .split(\",\")\n .map((name) => `${name}=${data[name] ?? \"\"}`)\n .join(\",\");\n const expected = await hmacSha256Base64(secret, message);\n\n return { valid: timingSafeEqual(expected, provided), data };\n}\n\n/* ------------------------------------------------------------------ *\n * Transaction status\n * ------------------------------------------------------------------ */\n\nexport interface StatusParams {\n product_code: string;\n total_amount: string | number;\n transaction_uuid: string;\n}\n\n/**\n * Query the eSewa transaction-status API. GETs the status endpoint with\n * `product_code`, `total_amount` and `transaction_uuid` as query params and\n * returns the parsed JSON. Inject a custom `fetch` for tests or non-global\n * runtimes.\n *\n * @example\n * const status = await checkStatus(\n * { product_code: \"EPAYTEST\", total_amount: 100, transaction_uuid: \"11-201\" },\n * { env: \"test\" },\n * );\n */\nexport async function checkStatus(\n params: StatusParams,\n opts?: { env?: EsewaEnv; fetch?: typeof fetch },\n): Promise<unknown> {\n const base = ESEWA_STATUS_URLS[opts?.env ?? \"test\"];\n const qs = new URLSearchParams({\n product_code: params.product_code,\n total_amount: String(params.total_amount),\n transaction_uuid: params.transaction_uuid,\n }).toString();\n const url = `${base}?${qs}`;\n\n const doFetch = opts?.fetch ?? globalThis.fetch;\n if (!doFetch) throw new Error(\"@lacspace/esewa: global fetch is unavailable; pass opts.fetch.\");\n const res = await doFetch(url, { method: \"GET\" });\n return res.json();\n}\n"]}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @lacspace/esewa
3
+ *
4
+ * eSewa ePay v2 (Nepal) — the correct, tiny way to integrate Nepal's most-used
5
+ * payment gateway. Handles the three things every integration re-implements:
6
+ *
7
+ * 1. **Signing** — the HMAC-SHA256 signature eSewa requires on the checkout
8
+ * form, computed over `total_amount,transaction_uuid,product_code` in the
9
+ * exact order of `signed_field_names`, base64-encoded.
10
+ * 2. **Form building** — a ready-to-POST `{ action, method, fields }` object
11
+ * with every field eSewa expects, including a valid `signature`.
12
+ * 3. **Verification & status** — decode and verify the signed base64 `data`
13
+ * payload eSewa returns on success (timing-safe), and query the
14
+ * transaction-status API.
15
+ *
16
+ * Built on Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled
17
+ * cryptography. Isomorphic: Node 20+, edge runtimes and browsers. Zero deps.
18
+ */
19
+ type EsewaEnv = "test" | "prod";
20
+ /** eSewa checkout form endpoints (the URL you POST the form to). */
21
+ declare const ESEWA_FORM_URLS: Record<EsewaEnv, string>;
22
+ /** eSewa transaction-status API endpoints. */
23
+ declare const ESEWA_STATUS_URLS: Record<EsewaEnv, string>;
24
+ /** eSewa-published sandbox secret key (test environment only). */
25
+ declare const ESEWA_TEST_SECRET = "8gBm/:&EnhH.1/q@K@";
26
+ /** eSewa-published sandbox merchant/product code (test environment only). */
27
+ declare const ESEWA_TEST_PRODUCT_CODE = "EPAYTEST";
28
+ /** The fields eSewa signs, in the required order. */
29
+ declare const ESEWA_SIGNED_FIELD_NAMES = "total_amount,transaction_uuid,product_code";
30
+ interface SignFields {
31
+ total_amount: string | number;
32
+ transaction_uuid: string;
33
+ product_code: string;
34
+ }
35
+ /**
36
+ * Compute the eSewa signature for the required signed fields. The message is
37
+ * `total_amount=<v>,transaction_uuid=<v>,product_code=<v>` (the exact order of
38
+ * `signed_field_names`), HMAC-SHA256'd with the merchant secret and returned as
39
+ * standard base64.
40
+ *
41
+ * @example
42
+ * const sig = await signPayment(
43
+ * { total_amount: 100, transaction_uuid: "11-201", product_code: "EPAYTEST" },
44
+ * ESEWA_TEST_SECRET,
45
+ * );
46
+ */
47
+ declare function signPayment(fields: SignFields, secret: string): Promise<string>;
48
+ interface BuildFormInput {
49
+ /** Base product amount. */
50
+ amount: number;
51
+ /** Tax amount. Default 0. */
52
+ taxAmount?: number;
53
+ /** Grand total. Defaults to amount + tax + service + delivery. */
54
+ totalAmount?: number;
55
+ /** Unique transaction id you generate. */
56
+ transactionUuid: string;
57
+ /** Merchant product code (e.g. "EPAYTEST" in test). */
58
+ productCode: string;
59
+ /** Where eSewa redirects on success. */
60
+ successUrl: string;
61
+ /** Where eSewa redirects on failure. */
62
+ failureUrl: string;
63
+ /** Product service charge. Default 0. */
64
+ productServiceCharge?: number;
65
+ /** Product delivery charge. Default 0. */
66
+ productDeliveryCharge?: number;
67
+ }
68
+ interface EsewaForm {
69
+ /** URL to POST the form to. */
70
+ action: string;
71
+ method: "POST";
72
+ /** All fields eSewa expects, as strings ready for form inputs. */
73
+ fields: Record<string, string>;
74
+ }
75
+ /**
76
+ * Build a ready-to-POST eSewa checkout form: `{ action, method, fields }`. The
77
+ * `total_amount` defaults to `amount + taxAmount + serviceCharge + deliveryCharge`,
78
+ * and a valid `signature` is computed for you.
79
+ *
80
+ * @example
81
+ * const form = await buildForm(
82
+ * { amount: 100, transactionUuid: "11-201", productCode: ESEWA_TEST_PRODUCT_CODE,
83
+ * successUrl: "https://me/ok", failureUrl: "https://me/fail" },
84
+ * { secret: ESEWA_TEST_SECRET, env: "test" },
85
+ * );
86
+ * // render form.fields as hidden inputs and auto-submit to form.action
87
+ */
88
+ declare function buildForm(input: BuildFormInput, opts: {
89
+ secret: string;
90
+ env?: EsewaEnv;
91
+ }): Promise<EsewaForm>;
92
+ interface VerifyResult {
93
+ valid: boolean;
94
+ /** The decoded response fields. */
95
+ data: Record<string, unknown>;
96
+ }
97
+ /**
98
+ * Verify the signed base64 `data` payload eSewa appends to the success redirect.
99
+ * Decodes the JSON, recomputes the signature over the fields named in its own
100
+ * `signed_field_names`, and compares (timing-safe) against its `signature`.
101
+ * Never throws — returns `{ valid, data }`.
102
+ *
103
+ * @example
104
+ * const { valid, data } = await verifyResponse(url.searchParams.get("data")!, secret);
105
+ * if (valid && data.status === "COMPLETE") fulfilOrder(data.transaction_uuid);
106
+ */
107
+ declare function verifyResponse(base64Data: string, secret: string): Promise<VerifyResult>;
108
+ interface StatusParams {
109
+ product_code: string;
110
+ total_amount: string | number;
111
+ transaction_uuid: string;
112
+ }
113
+ /**
114
+ * Query the eSewa transaction-status API. GETs the status endpoint with
115
+ * `product_code`, `total_amount` and `transaction_uuid` as query params and
116
+ * returns the parsed JSON. Inject a custom `fetch` for tests or non-global
117
+ * runtimes.
118
+ *
119
+ * @example
120
+ * const status = await checkStatus(
121
+ * { product_code: "EPAYTEST", total_amount: 100, transaction_uuid: "11-201" },
122
+ * { env: "test" },
123
+ * );
124
+ */
125
+ declare function checkStatus(params: StatusParams, opts?: {
126
+ env?: EsewaEnv;
127
+ fetch?: typeof fetch;
128
+ }): Promise<unknown>;
129
+
130
+ export { type BuildFormInput, ESEWA_FORM_URLS, ESEWA_SIGNED_FIELD_NAMES, ESEWA_STATUS_URLS, ESEWA_TEST_PRODUCT_CODE, ESEWA_TEST_SECRET, type EsewaEnv, type EsewaForm, type SignFields, type StatusParams, type VerifyResult, buildForm, checkStatus, signPayment, verifyResponse };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @lacspace/esewa
3
+ *
4
+ * eSewa ePay v2 (Nepal) — the correct, tiny way to integrate Nepal's most-used
5
+ * payment gateway. Handles the three things every integration re-implements:
6
+ *
7
+ * 1. **Signing** — the HMAC-SHA256 signature eSewa requires on the checkout
8
+ * form, computed over `total_amount,transaction_uuid,product_code` in the
9
+ * exact order of `signed_field_names`, base64-encoded.
10
+ * 2. **Form building** — a ready-to-POST `{ action, method, fields }` object
11
+ * with every field eSewa expects, including a valid `signature`.
12
+ * 3. **Verification & status** — decode and verify the signed base64 `data`
13
+ * payload eSewa returns on success (timing-safe), and query the
14
+ * transaction-status API.
15
+ *
16
+ * Built on Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled
17
+ * cryptography. Isomorphic: Node 20+, edge runtimes and browsers. Zero deps.
18
+ */
19
+ type EsewaEnv = "test" | "prod";
20
+ /** eSewa checkout form endpoints (the URL you POST the form to). */
21
+ declare const ESEWA_FORM_URLS: Record<EsewaEnv, string>;
22
+ /** eSewa transaction-status API endpoints. */
23
+ declare const ESEWA_STATUS_URLS: Record<EsewaEnv, string>;
24
+ /** eSewa-published sandbox secret key (test environment only). */
25
+ declare const ESEWA_TEST_SECRET = "8gBm/:&EnhH.1/q@K@";
26
+ /** eSewa-published sandbox merchant/product code (test environment only). */
27
+ declare const ESEWA_TEST_PRODUCT_CODE = "EPAYTEST";
28
+ /** The fields eSewa signs, in the required order. */
29
+ declare const ESEWA_SIGNED_FIELD_NAMES = "total_amount,transaction_uuid,product_code";
30
+ interface SignFields {
31
+ total_amount: string | number;
32
+ transaction_uuid: string;
33
+ product_code: string;
34
+ }
35
+ /**
36
+ * Compute the eSewa signature for the required signed fields. The message is
37
+ * `total_amount=<v>,transaction_uuid=<v>,product_code=<v>` (the exact order of
38
+ * `signed_field_names`), HMAC-SHA256'd with the merchant secret and returned as
39
+ * standard base64.
40
+ *
41
+ * @example
42
+ * const sig = await signPayment(
43
+ * { total_amount: 100, transaction_uuid: "11-201", product_code: "EPAYTEST" },
44
+ * ESEWA_TEST_SECRET,
45
+ * );
46
+ */
47
+ declare function signPayment(fields: SignFields, secret: string): Promise<string>;
48
+ interface BuildFormInput {
49
+ /** Base product amount. */
50
+ amount: number;
51
+ /** Tax amount. Default 0. */
52
+ taxAmount?: number;
53
+ /** Grand total. Defaults to amount + tax + service + delivery. */
54
+ totalAmount?: number;
55
+ /** Unique transaction id you generate. */
56
+ transactionUuid: string;
57
+ /** Merchant product code (e.g. "EPAYTEST" in test). */
58
+ productCode: string;
59
+ /** Where eSewa redirects on success. */
60
+ successUrl: string;
61
+ /** Where eSewa redirects on failure. */
62
+ failureUrl: string;
63
+ /** Product service charge. Default 0. */
64
+ productServiceCharge?: number;
65
+ /** Product delivery charge. Default 0. */
66
+ productDeliveryCharge?: number;
67
+ }
68
+ interface EsewaForm {
69
+ /** URL to POST the form to. */
70
+ action: string;
71
+ method: "POST";
72
+ /** All fields eSewa expects, as strings ready for form inputs. */
73
+ fields: Record<string, string>;
74
+ }
75
+ /**
76
+ * Build a ready-to-POST eSewa checkout form: `{ action, method, fields }`. The
77
+ * `total_amount` defaults to `amount + taxAmount + serviceCharge + deliveryCharge`,
78
+ * and a valid `signature` is computed for you.
79
+ *
80
+ * @example
81
+ * const form = await buildForm(
82
+ * { amount: 100, transactionUuid: "11-201", productCode: ESEWA_TEST_PRODUCT_CODE,
83
+ * successUrl: "https://me/ok", failureUrl: "https://me/fail" },
84
+ * { secret: ESEWA_TEST_SECRET, env: "test" },
85
+ * );
86
+ * // render form.fields as hidden inputs and auto-submit to form.action
87
+ */
88
+ declare function buildForm(input: BuildFormInput, opts: {
89
+ secret: string;
90
+ env?: EsewaEnv;
91
+ }): Promise<EsewaForm>;
92
+ interface VerifyResult {
93
+ valid: boolean;
94
+ /** The decoded response fields. */
95
+ data: Record<string, unknown>;
96
+ }
97
+ /**
98
+ * Verify the signed base64 `data` payload eSewa appends to the success redirect.
99
+ * Decodes the JSON, recomputes the signature over the fields named in its own
100
+ * `signed_field_names`, and compares (timing-safe) against its `signature`.
101
+ * Never throws — returns `{ valid, data }`.
102
+ *
103
+ * @example
104
+ * const { valid, data } = await verifyResponse(url.searchParams.get("data")!, secret);
105
+ * if (valid && data.status === "COMPLETE") fulfilOrder(data.transaction_uuid);
106
+ */
107
+ declare function verifyResponse(base64Data: string, secret: string): Promise<VerifyResult>;
108
+ interface StatusParams {
109
+ product_code: string;
110
+ total_amount: string | number;
111
+ transaction_uuid: string;
112
+ }
113
+ /**
114
+ * Query the eSewa transaction-status API. GETs the status endpoint with
115
+ * `product_code`, `total_amount` and `transaction_uuid` as query params and
116
+ * returns the parsed JSON. Inject a custom `fetch` for tests or non-global
117
+ * runtimes.
118
+ *
119
+ * @example
120
+ * const status = await checkStatus(
121
+ * { product_code: "EPAYTEST", total_amount: 100, transaction_uuid: "11-201" },
122
+ * { env: "test" },
123
+ * );
124
+ */
125
+ declare function checkStatus(params: StatusParams, opts?: {
126
+ env?: EsewaEnv;
127
+ fetch?: typeof fetch;
128
+ }): Promise<unknown>;
129
+
130
+ export { type BuildFormInput, ESEWA_FORM_URLS, ESEWA_SIGNED_FIELD_NAMES, ESEWA_STATUS_URLS, ESEWA_TEST_PRODUCT_CODE, ESEWA_TEST_SECRET, type EsewaEnv, type EsewaForm, type SignFields, type StatusParams, type VerifyResult, buildForm, checkStatus, signPayment, verifyResponse };
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ // src/index.ts
2
+ var ESEWA_FORM_URLS = {
3
+ test: "https://rc-epay.esewa.com.np/api/epay/main/v2/form",
4
+ prod: "https://epay.esewa.com.np/api/epay/main/v2/form"
5
+ };
6
+ var ESEWA_STATUS_URLS = {
7
+ test: "https://rc.esewa.com.np/api/epay/transaction/status/",
8
+ prod: "https://epay.esewa.com.np/api/epay/transaction/status/"
9
+ };
10
+ var ESEWA_TEST_SECRET = "8gBm/:&EnhH.1/q@K@";
11
+ var ESEWA_TEST_PRODUCT_CODE = "EPAYTEST";
12
+ var ESEWA_SIGNED_FIELD_NAMES = "total_amount,transaction_uuid,product_code";
13
+ var enc = new TextEncoder();
14
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
+ function toBase64(bytes) {
16
+ let out = "";
17
+ for (let i = 0; i < bytes.length; i += 3) {
18
+ const b0 = bytes[i];
19
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
20
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
21
+ out += B64[b0 >> 2];
22
+ out += B64[(b0 & 3) << 4 | b1 >> 4];
23
+ out += i + 1 < bytes.length ? B64[(b1 & 15) << 2 | b2 >> 6] : "=";
24
+ out += i + 2 < bytes.length ? B64[b2 & 63] : "=";
25
+ }
26
+ return out;
27
+ }
28
+ function fromBase64(b64) {
29
+ const clean = b64.replace(/[^A-Za-z0-9+/]/g, "");
30
+ const len = Math.floor(clean.length * 3 / 4);
31
+ const bytes = new Uint8Array(len);
32
+ let p = 0;
33
+ for (let i = 0; i < clean.length; i += 4) {
34
+ const c0 = B64.indexOf(clean[i]);
35
+ const c1 = B64.indexOf(clean[i + 1]);
36
+ const c2 = i + 2 < clean.length ? B64.indexOf(clean[i + 2]) : -1;
37
+ const c3 = i + 3 < clean.length ? B64.indexOf(clean[i + 3]) : -1;
38
+ if (p < len) bytes[p++] = c0 << 2 | c1 >> 4;
39
+ if (c2 >= 0 && p < len) bytes[p++] = (c1 & 15) << 4 | c2 >> 2;
40
+ if (c3 >= 0 && p < len) bytes[p++] = (c2 & 3) << 6 | c3;
41
+ }
42
+ return bytes;
43
+ }
44
+ async function hmacSha256Base64(secret, message) {
45
+ const subtle = globalThis.crypto?.subtle;
46
+ if (!subtle) throw new Error("@lacspace/esewa: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.");
47
+ const key = await subtle.importKey(
48
+ "raw",
49
+ enc.encode(secret),
50
+ { name: "HMAC", hash: "SHA-256" },
51
+ false,
52
+ ["sign"]
53
+ );
54
+ const sig = await subtle.sign("HMAC", key, enc.encode(message));
55
+ return toBase64(new Uint8Array(sig));
56
+ }
57
+ function timingSafeEqual(a, b) {
58
+ const ab = enc.encode(a);
59
+ const bb = enc.encode(b);
60
+ let diff = ab.length ^ bb.length;
61
+ const n = Math.max(ab.length, bb.length);
62
+ for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
63
+ return diff === 0;
64
+ }
65
+ async function signPayment(fields, secret) {
66
+ const message = `total_amount=${fields.total_amount},transaction_uuid=${fields.transaction_uuid},product_code=${fields.product_code}`;
67
+ return hmacSha256Base64(secret, message);
68
+ }
69
+ async function buildForm(input, opts) {
70
+ const tax = input.taxAmount ?? 0;
71
+ const service = input.productServiceCharge ?? 0;
72
+ const delivery = input.productDeliveryCharge ?? 0;
73
+ const total = input.totalAmount ?? input.amount + tax + service + delivery;
74
+ const signature = await signPayment(
75
+ {
76
+ total_amount: total,
77
+ transaction_uuid: input.transactionUuid,
78
+ product_code: input.productCode
79
+ },
80
+ opts.secret
81
+ );
82
+ const fields = {
83
+ amount: String(input.amount),
84
+ tax_amount: String(tax),
85
+ total_amount: String(total),
86
+ transaction_uuid: input.transactionUuid,
87
+ product_code: input.productCode,
88
+ product_service_charge: String(service),
89
+ product_delivery_charge: String(delivery),
90
+ success_url: input.successUrl,
91
+ failure_url: input.failureUrl,
92
+ signed_field_names: ESEWA_SIGNED_FIELD_NAMES,
93
+ signature
94
+ };
95
+ return { action: ESEWA_FORM_URLS[opts.env ?? "test"], method: "POST", fields };
96
+ }
97
+ async function verifyResponse(base64Data, secret) {
98
+ let data;
99
+ try {
100
+ data = JSON.parse(new TextDecoder().decode(fromBase64(base64Data)));
101
+ } catch {
102
+ return { valid: false, data: {} };
103
+ }
104
+ const names = typeof data.signed_field_names === "string" ? data.signed_field_names : "";
105
+ const provided = typeof data.signature === "string" ? data.signature : "";
106
+ if (!names || !provided) return { valid: false, data };
107
+ const message = names.split(",").map((name) => `${name}=${data[name] ?? ""}`).join(",");
108
+ const expected = await hmacSha256Base64(secret, message);
109
+ return { valid: timingSafeEqual(expected, provided), data };
110
+ }
111
+ async function checkStatus(params, opts) {
112
+ const base = ESEWA_STATUS_URLS[opts?.env ?? "test"];
113
+ const qs = new URLSearchParams({
114
+ product_code: params.product_code,
115
+ total_amount: String(params.total_amount),
116
+ transaction_uuid: params.transaction_uuid
117
+ }).toString();
118
+ const url = `${base}?${qs}`;
119
+ const doFetch = opts?.fetch ?? globalThis.fetch;
120
+ if (!doFetch) throw new Error("@lacspace/esewa: global fetch is unavailable; pass opts.fetch.");
121
+ const res = await doFetch(url, { method: "GET" });
122
+ return res.json();
123
+ }
124
+
125
+ export { ESEWA_FORM_URLS, ESEWA_SIGNED_FIELD_NAMES, ESEWA_STATUS_URLS, ESEWA_TEST_PRODUCT_CODE, ESEWA_TEST_SECRET, buildForm, checkStatus, signPayment, verifyResponse };
126
+ //# sourceMappingURL=index.js.map
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/esewa\n *\n * eSewa ePay v2 (Nepal) — the correct, tiny way to integrate Nepal's most-used\n * payment gateway. Handles the three things every integration re-implements:\n *\n * 1. **Signing** — the HMAC-SHA256 signature eSewa requires on the checkout\n * form, computed over `total_amount,transaction_uuid,product_code` in the\n * exact order of `signed_field_names`, base64-encoded.\n * 2. **Form building** — a ready-to-POST `{ action, method, fields }` object\n * with every field eSewa expects, including a valid `signature`.\n * 3. **Verification & status** — decode and verify the signed base64 `data`\n * payload eSewa returns on success (timing-safe), and query the\n * transaction-status API.\n *\n * Built on Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled\n * cryptography. Isomorphic: Node 20+, edge runtimes and browsers. Zero deps.\n */\n\n/* ------------------------------------------------------------------ *\n * Endpoints & test credentials\n * ------------------------------------------------------------------ */\n\nexport type EsewaEnv = \"test\" | \"prod\";\n\n/** eSewa checkout form endpoints (the URL you POST the form to). */\nexport const ESEWA_FORM_URLS: Record<EsewaEnv, string> = {\n test: \"https://rc-epay.esewa.com.np/api/epay/main/v2/form\",\n prod: \"https://epay.esewa.com.np/api/epay/main/v2/form\",\n};\n\n/** eSewa transaction-status API endpoints. */\nexport const ESEWA_STATUS_URLS: Record<EsewaEnv, string> = {\n test: \"https://rc.esewa.com.np/api/epay/transaction/status/\",\n prod: \"https://epay.esewa.com.np/api/epay/transaction/status/\",\n};\n\n/** eSewa-published sandbox secret key (test environment only). */\nexport const ESEWA_TEST_SECRET = \"8gBm/:&EnhH.1/q@K@\";\n\n/** eSewa-published sandbox merchant/product code (test environment only). */\nexport const ESEWA_TEST_PRODUCT_CODE = \"EPAYTEST\";\n\n/** The fields eSewa signs, in the required order. */\nexport const ESEWA_SIGNED_FIELD_NAMES = \"total_amount,transaction_uuid,product_code\";\n\n/* ------------------------------------------------------------------ *\n * Web Crypto + base64 helpers (isomorphic, zero-dependency)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** Standard base64 (NOT url-safe) encode of raw bytes. */\nfunction toBase64(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i]!;\n const b1 = i + 1 < bytes.length ? bytes[i + 1]! : 0;\n const b2 = i + 2 < bytes.length ? bytes[i + 2]! : 0;\n out += B64[b0 >> 2];\n out += B64[((b0 & 3) << 4) | (b1 >> 4)];\n out += i + 1 < bytes.length ? B64[((b1 & 15) << 2) | (b2 >> 6)] : \"=\";\n out += i + 2 < bytes.length ? B64[b2 & 63] : \"=\";\n }\n return out;\n}\n\n/** Standard base64 decode to raw bytes. */\nfunction fromBase64(b64: string): Uint8Array {\n const clean = b64.replace(/[^A-Za-z0-9+/]/g, \"\");\n const len = Math.floor((clean.length * 3) / 4);\n const bytes = new Uint8Array(len);\n let p = 0;\n for (let i = 0; i < clean.length; i += 4) {\n const c0 = B64.indexOf(clean[i]!);\n const c1 = B64.indexOf(clean[i + 1]!);\n const c2 = i + 2 < clean.length ? B64.indexOf(clean[i + 2]!) : -1;\n const c3 = i + 3 < clean.length ? B64.indexOf(clean[i + 3]!) : -1;\n if (p < len) bytes[p++] = (c0 << 2) | (c1 >> 4);\n if (c2 >= 0 && p < len) bytes[p++] = ((c1 & 15) << 4) | (c2 >> 2);\n if (c3 >= 0 && p < len) bytes[p++] = ((c2 & 3) << 6) | c3;\n }\n return bytes;\n}\n\n/** HMAC-SHA256 over `message` with `secret`, returned as standard base64. */\nasync function hmacSha256Base64(secret: string, message: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) throw new Error(\"@lacspace/esewa: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.\");\n const key = await subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await subtle.sign(\"HMAC\", key, enc.encode(message));\n return toBase64(new Uint8Array(sig));\n}\n\n/** Constant-time comparison of two strings (avoids signature-timing leaks). */\nfunction timingSafeEqual(a: string, b: string): boolean {\n const ab = enc.encode(a);\n const bb = enc.encode(b);\n // Compare a fixed number of bytes; length mismatch still fails.\n let diff = ab.length ^ bb.length;\n const n = Math.max(ab.length, bb.length);\n for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Signing\n * ------------------------------------------------------------------ */\n\nexport interface SignFields {\n total_amount: string | number;\n transaction_uuid: string;\n product_code: string;\n}\n\n/**\n * Compute the eSewa signature for the required signed fields. The message is\n * `total_amount=<v>,transaction_uuid=<v>,product_code=<v>` (the exact order of\n * `signed_field_names`), HMAC-SHA256'd with the merchant secret and returned as\n * standard base64.\n *\n * @example\n * const sig = await signPayment(\n * { total_amount: 100, transaction_uuid: \"11-201\", product_code: \"EPAYTEST\" },\n * ESEWA_TEST_SECRET,\n * );\n */\nexport async function signPayment(fields: SignFields, secret: string): Promise<string> {\n const message =\n `total_amount=${fields.total_amount},` +\n `transaction_uuid=${fields.transaction_uuid},` +\n `product_code=${fields.product_code}`;\n return hmacSha256Base64(secret, message);\n}\n\n/* ------------------------------------------------------------------ *\n * Form building\n * ------------------------------------------------------------------ */\n\nexport interface BuildFormInput {\n /** Base product amount. */\n amount: number;\n /** Tax amount. Default 0. */\n taxAmount?: number;\n /** Grand total. Defaults to amount + tax + service + delivery. */\n totalAmount?: number;\n /** Unique transaction id you generate. */\n transactionUuid: string;\n /** Merchant product code (e.g. \"EPAYTEST\" in test). */\n productCode: string;\n /** Where eSewa redirects on success. */\n successUrl: string;\n /** Where eSewa redirects on failure. */\n failureUrl: string;\n /** Product service charge. Default 0. */\n productServiceCharge?: number;\n /** Product delivery charge. Default 0. */\n productDeliveryCharge?: number;\n}\n\nexport interface EsewaForm {\n /** URL to POST the form to. */\n action: string;\n method: \"POST\";\n /** All fields eSewa expects, as strings ready for form inputs. */\n fields: Record<string, string>;\n}\n\n/**\n * Build a ready-to-POST eSewa checkout form: `{ action, method, fields }`. The\n * `total_amount` defaults to `amount + taxAmount + serviceCharge + deliveryCharge`,\n * and a valid `signature` is computed for you.\n *\n * @example\n * const form = await buildForm(\n * { amount: 100, transactionUuid: \"11-201\", productCode: ESEWA_TEST_PRODUCT_CODE,\n * successUrl: \"https://me/ok\", failureUrl: \"https://me/fail\" },\n * { secret: ESEWA_TEST_SECRET, env: \"test\" },\n * );\n * // render form.fields as hidden inputs and auto-submit to form.action\n */\nexport async function buildForm(\n input: BuildFormInput,\n opts: { secret: string; env?: EsewaEnv },\n): Promise<EsewaForm> {\n const tax = input.taxAmount ?? 0;\n const service = input.productServiceCharge ?? 0;\n const delivery = input.productDeliveryCharge ?? 0;\n const total = input.totalAmount ?? input.amount + tax + service + delivery;\n\n const signature = await signPayment(\n {\n total_amount: total,\n transaction_uuid: input.transactionUuid,\n product_code: input.productCode,\n },\n opts.secret,\n );\n\n const fields: Record<string, string> = {\n amount: String(input.amount),\n tax_amount: String(tax),\n total_amount: String(total),\n transaction_uuid: input.transactionUuid,\n product_code: input.productCode,\n product_service_charge: String(service),\n product_delivery_charge: String(delivery),\n success_url: input.successUrl,\n failure_url: input.failureUrl,\n signed_field_names: ESEWA_SIGNED_FIELD_NAMES,\n signature,\n };\n\n return { action: ESEWA_FORM_URLS[opts.env ?? \"test\"], method: \"POST\", fields };\n}\n\n/* ------------------------------------------------------------------ *\n * Response verification\n * ------------------------------------------------------------------ */\n\nexport interface VerifyResult {\n valid: boolean;\n /** The decoded response fields. */\n data: Record<string, unknown>;\n}\n\n/**\n * Verify the signed base64 `data` payload eSewa appends to the success redirect.\n * Decodes the JSON, recomputes the signature over the fields named in its own\n * `signed_field_names`, and compares (timing-safe) against its `signature`.\n * Never throws — returns `{ valid, data }`.\n *\n * @example\n * const { valid, data } = await verifyResponse(url.searchParams.get(\"data\")!, secret);\n * if (valid && data.status === \"COMPLETE\") fulfilOrder(data.transaction_uuid);\n */\nexport async function verifyResponse(base64Data: string, secret: string): Promise<VerifyResult> {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(new TextDecoder().decode(fromBase64(base64Data))) as Record<string, unknown>;\n } catch {\n return { valid: false, data: {} };\n }\n\n const names = typeof data.signed_field_names === \"string\" ? data.signed_field_names : \"\";\n const provided = typeof data.signature === \"string\" ? data.signature : \"\";\n if (!names || !provided) return { valid: false, data };\n\n const message = names\n .split(\",\")\n .map((name) => `${name}=${data[name] ?? \"\"}`)\n .join(\",\");\n const expected = await hmacSha256Base64(secret, message);\n\n return { valid: timingSafeEqual(expected, provided), data };\n}\n\n/* ------------------------------------------------------------------ *\n * Transaction status\n * ------------------------------------------------------------------ */\n\nexport interface StatusParams {\n product_code: string;\n total_amount: string | number;\n transaction_uuid: string;\n}\n\n/**\n * Query the eSewa transaction-status API. GETs the status endpoint with\n * `product_code`, `total_amount` and `transaction_uuid` as query params and\n * returns the parsed JSON. Inject a custom `fetch` for tests or non-global\n * runtimes.\n *\n * @example\n * const status = await checkStatus(\n * { product_code: \"EPAYTEST\", total_amount: 100, transaction_uuid: \"11-201\" },\n * { env: \"test\" },\n * );\n */\nexport async function checkStatus(\n params: StatusParams,\n opts?: { env?: EsewaEnv; fetch?: typeof fetch },\n): Promise<unknown> {\n const base = ESEWA_STATUS_URLS[opts?.env ?? \"test\"];\n const qs = new URLSearchParams({\n product_code: params.product_code,\n total_amount: String(params.total_amount),\n transaction_uuid: params.transaction_uuid,\n }).toString();\n const url = `${base}?${qs}`;\n\n const doFetch = opts?.fetch ?? globalThis.fetch;\n if (!doFetch) throw new Error(\"@lacspace/esewa: global fetch is unavailable; pass opts.fetch.\");\n const res = await doFetch(url, { method: \"GET\" });\n return res.json();\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lacspace/esewa",
3
+ "version": "1.0.0",
4
+ "description": "eSewa ePay v2 (Nepal) payment gateway toolkit over Web Crypto — HMAC-SHA256 signing, form building, response verification and transaction status checks. Zero-dependency, isomorphic (Node, edge, browser).",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "nepal",
31
+ "payment",
32
+ "payment-gateway",
33
+ "esewa",
34
+ "epay",
35
+ "epay-v2",
36
+ "hmac-sha256",
37
+ "web-crypto",
38
+ "khalti",
39
+ "edge",
40
+ "isomorphic",
41
+ "typescript"
42
+ ],
43
+ "author": "Lacspace <contact@lacspace.com>",
44
+ "license": "SEE LICENSE IN LICENSE",
45
+ "homepage": "https://developer.lacspace.com/packages/esewa",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/lacspace/npm-packages.git",
49
+ "directory": "esewa"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/lacspace/npm-packages/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "dependencies": {},
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }