@lacspace/esewa 1.0.0 → 1.1.1
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 +16 -4
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,17 @@ const form = await buildForm(
|
|
|
49
49
|
|
|
50
50
|
`total_amount` defaults to `amount + taxAmount + productServiceCharge + productDeliveryCharge`.
|
|
51
51
|
|
|
52
|
+
> **eSewa amounts are in RUPEES, not paisa.** The rest of the @lacspace commerce suite (cart, order, tax, invoice, money…) stores integer **paisa**. Convert at this boundary with `paisaToRupees` — passing paisa straight through overcharges the customer 100×.
|
|
53
|
+
>
|
|
54
|
+
> ```ts
|
|
55
|
+
> import { buildForm, paisaToRupees } from "@lacspace/esewa";
|
|
56
|
+
>
|
|
57
|
+
> paisaToRupees(12345); // → 123.45
|
|
58
|
+
>
|
|
59
|
+
> // a Rs 123.45 order stored as 12345 paisa:
|
|
60
|
+
> await buildForm({ amount: paisaToRupees(12345), transactionUuid, productCode, successUrl, failureUrl }, { secret, env: "test" });
|
|
61
|
+
> ```
|
|
62
|
+
|
|
52
63
|
## Verify the success redirect
|
|
53
64
|
|
|
54
65
|
```ts
|
|
@@ -80,6 +91,7 @@ const status = await checkStatus(
|
|
|
80
91
|
| Export | Description |
|
|
81
92
|
| --- | --- |
|
|
82
93
|
| `signPayment({ total_amount, transaction_uuid, product_code }, secret)` | base64 HMAC-SHA256 signature |
|
|
94
|
+
| `paisaToRupees(paisa)` | convert integer paisa → rupees for eSewa's amount fields (`12345` → `123.45`) |
|
|
83
95
|
| `buildForm(input, { secret, env? })` | `{ action, method, fields }` ready to POST |
|
|
84
96
|
| `verifyResponse(base64Data, secret)` | `{ valid, data }` — decode + timing-safe verify |
|
|
85
97
|
| `checkStatus(params, { env?, fetch? })` | GET the status API, returns parsed JSON |
|
|
@@ -91,9 +103,9 @@ const status = await checkStatus(
|
|
|
91
103
|
|
|
92
104
|
## Licensing
|
|
93
105
|
|
|
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.
|
|
106
|
+
This package is **free** under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
|
|
95
107
|
|
|
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)**.
|
|
108
|
+
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://developer.lacspace.com/licenses)**.
|
|
97
109
|
|
|
98
110
|
<!-- LACSPACE-DEV-PLATFORM -->
|
|
99
111
|
|
|
@@ -101,7 +113,7 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
101
113
|
|
|
102
114
|
## The Lacspace Developer Platform
|
|
103
115
|
|
|
104
|
-
`@lacspace/esewa` is part of **
|
|
116
|
+
`@lacspace/esewa` is part of **80+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
105
117
|
|
|
106
118
|
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
107
119
|
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
@@ -109,4 +121,4 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
109
121
|
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
110
122
|
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
111
123
|
|
|
112
|
-
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
|
124
|
+
Free under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
package/dist/index.cjs
CHANGED
|
@@ -68,6 +68,9 @@ async function signPayment(fields, secret) {
|
|
|
68
68
|
const message = `total_amount=${fields.total_amount},transaction_uuid=${fields.transaction_uuid},product_code=${fields.product_code}`;
|
|
69
69
|
return hmacSha256Base64(secret, message);
|
|
70
70
|
}
|
|
71
|
+
function paisaToRupees(paisa) {
|
|
72
|
+
return Math.round(paisa) / 100;
|
|
73
|
+
}
|
|
71
74
|
async function buildForm(input, opts) {
|
|
72
75
|
const tax = input.taxAmount ?? 0;
|
|
73
76
|
const service = input.productServiceCharge ?? 0;
|
|
@@ -131,6 +134,7 @@ exports.ESEWA_TEST_PRODUCT_CODE = ESEWA_TEST_PRODUCT_CODE;
|
|
|
131
134
|
exports.ESEWA_TEST_SECRET = ESEWA_TEST_SECRET;
|
|
132
135
|
exports.buildForm = buildForm;
|
|
133
136
|
exports.checkStatus = checkStatus;
|
|
137
|
+
exports.paisaToRupees = paisaToRupees;
|
|
134
138
|
exports.signPayment = signPayment;
|
|
135
139
|
exports.verifyResponse = verifyResponse;
|
|
136
140
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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\n/**\n * Convert integer paisa (minor units — as used across the @lacspace commerce\n * packages: cart, order, tax, invoice, money…) to rupees for eSewa's amount\n * fields. `paisaToRupees(12345)` → `123.45`.\n *\n * eSewa expects amounts in **rupees**, while the rest of the suite stores\n * **paisa** — convert at this boundary so a Rs 123.45 order isn't charged as\n * Rs 12,345.\n */\nexport function paisaToRupees(paisa: number): number {\n return Math.round(paisa) / 100;\n}\n\nexport interface BuildFormInput {\n /**\n * Base product amount, **in rupees** — eSewa's unit, NOT paisa. If you keep\n * integer minor units (paisa) like the other @lacspace commerce packages,\n * convert here with `paisaToRupees(paisa)`. Passing paisa overcharges 100×.\n */\n amount: number;\n /** Tax amount, in rupees (see `amount`). Default 0. */\n taxAmount?: number;\n /** Grand total, in rupees (see `amount`). 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/dist/index.d.cts
CHANGED
|
@@ -45,12 +45,26 @@ interface SignFields {
|
|
|
45
45
|
* );
|
|
46
46
|
*/
|
|
47
47
|
declare function signPayment(fields: SignFields, secret: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Convert integer paisa (minor units — as used across the @lacspace commerce
|
|
50
|
+
* packages: cart, order, tax, invoice, money…) to rupees for eSewa's amount
|
|
51
|
+
* fields. `paisaToRupees(12345)` → `123.45`.
|
|
52
|
+
*
|
|
53
|
+
* eSewa expects amounts in **rupees**, while the rest of the suite stores
|
|
54
|
+
* **paisa** — convert at this boundary so a Rs 123.45 order isn't charged as
|
|
55
|
+
* Rs 12,345.
|
|
56
|
+
*/
|
|
57
|
+
declare function paisaToRupees(paisa: number): number;
|
|
48
58
|
interface BuildFormInput {
|
|
49
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Base product amount, **in rupees** — eSewa's unit, NOT paisa. If you keep
|
|
61
|
+
* integer minor units (paisa) like the other @lacspace commerce packages,
|
|
62
|
+
* convert here with `paisaToRupees(paisa)`. Passing paisa overcharges 100×.
|
|
63
|
+
*/
|
|
50
64
|
amount: number;
|
|
51
|
-
/** Tax amount. Default 0. */
|
|
65
|
+
/** Tax amount, in rupees (see `amount`). Default 0. */
|
|
52
66
|
taxAmount?: number;
|
|
53
|
-
/** Grand total. Defaults to amount + tax + service + delivery. */
|
|
67
|
+
/** Grand total, in rupees (see `amount`). Defaults to amount + tax + service + delivery. */
|
|
54
68
|
totalAmount?: number;
|
|
55
69
|
/** Unique transaction id you generate. */
|
|
56
70
|
transactionUuid: string;
|
|
@@ -127,4 +141,4 @@ declare function checkStatus(params: StatusParams, opts?: {
|
|
|
127
141
|
fetch?: typeof fetch;
|
|
128
142
|
}): Promise<unknown>;
|
|
129
143
|
|
|
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 };
|
|
144
|
+
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, paisaToRupees, signPayment, verifyResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -45,12 +45,26 @@ interface SignFields {
|
|
|
45
45
|
* );
|
|
46
46
|
*/
|
|
47
47
|
declare function signPayment(fields: SignFields, secret: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Convert integer paisa (minor units — as used across the @lacspace commerce
|
|
50
|
+
* packages: cart, order, tax, invoice, money…) to rupees for eSewa's amount
|
|
51
|
+
* fields. `paisaToRupees(12345)` → `123.45`.
|
|
52
|
+
*
|
|
53
|
+
* eSewa expects amounts in **rupees**, while the rest of the suite stores
|
|
54
|
+
* **paisa** — convert at this boundary so a Rs 123.45 order isn't charged as
|
|
55
|
+
* Rs 12,345.
|
|
56
|
+
*/
|
|
57
|
+
declare function paisaToRupees(paisa: number): number;
|
|
48
58
|
interface BuildFormInput {
|
|
49
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Base product amount, **in rupees** — eSewa's unit, NOT paisa. If you keep
|
|
61
|
+
* integer minor units (paisa) like the other @lacspace commerce packages,
|
|
62
|
+
* convert here with `paisaToRupees(paisa)`. Passing paisa overcharges 100×.
|
|
63
|
+
*/
|
|
50
64
|
amount: number;
|
|
51
|
-
/** Tax amount. Default 0. */
|
|
65
|
+
/** Tax amount, in rupees (see `amount`). Default 0. */
|
|
52
66
|
taxAmount?: number;
|
|
53
|
-
/** Grand total. Defaults to amount + tax + service + delivery. */
|
|
67
|
+
/** Grand total, in rupees (see `amount`). Defaults to amount + tax + service + delivery. */
|
|
54
68
|
totalAmount?: number;
|
|
55
69
|
/** Unique transaction id you generate. */
|
|
56
70
|
transactionUuid: string;
|
|
@@ -127,4 +141,4 @@ declare function checkStatus(params: StatusParams, opts?: {
|
|
|
127
141
|
fetch?: typeof fetch;
|
|
128
142
|
}): Promise<unknown>;
|
|
129
143
|
|
|
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 };
|
|
144
|
+
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, paisaToRupees, signPayment, verifyResponse };
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,9 @@ async function signPayment(fields, secret) {
|
|
|
66
66
|
const message = `total_amount=${fields.total_amount},transaction_uuid=${fields.transaction_uuid},product_code=${fields.product_code}`;
|
|
67
67
|
return hmacSha256Base64(secret, message);
|
|
68
68
|
}
|
|
69
|
+
function paisaToRupees(paisa) {
|
|
70
|
+
return Math.round(paisa) / 100;
|
|
71
|
+
}
|
|
69
72
|
async function buildForm(input, opts) {
|
|
70
73
|
const tax = input.taxAmount ?? 0;
|
|
71
74
|
const service = input.productServiceCharge ?? 0;
|
|
@@ -122,6 +125,6 @@ async function checkStatus(params, opts) {
|
|
|
122
125
|
return res.json();
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
export { ESEWA_FORM_URLS, ESEWA_SIGNED_FIELD_NAMES, ESEWA_STATUS_URLS, ESEWA_TEST_PRODUCT_CODE, ESEWA_TEST_SECRET, buildForm, checkStatus, signPayment, verifyResponse };
|
|
128
|
+
export { ESEWA_FORM_URLS, ESEWA_SIGNED_FIELD_NAMES, ESEWA_STATUS_URLS, ESEWA_TEST_PRODUCT_CODE, ESEWA_TEST_SECRET, buildForm, checkStatus, paisaToRupees, signPayment, verifyResponse };
|
|
126
129
|
//# sourceMappingURL=index.js.map
|
|
127
130
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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\n/**\n * Convert integer paisa (minor units — as used across the @lacspace commerce\n * packages: cart, order, tax, invoice, money…) to rupees for eSewa's amount\n * fields. `paisaToRupees(12345)` → `123.45`.\n *\n * eSewa expects amounts in **rupees**, while the rest of the suite stores\n * **paisa** — convert at this boundary so a Rs 123.45 order isn't charged as\n * Rs 12,345.\n */\nexport function paisaToRupees(paisa: number): number {\n return Math.round(paisa) / 100;\n}\n\nexport interface BuildFormInput {\n /**\n * Base product amount, **in rupees** — eSewa's unit, NOT paisa. If you keep\n * integer minor units (paisa) like the other @lacspace commerce packages,\n * convert here with `paisaToRupees(paisa)`. Passing paisa overcharges 100×.\n */\n amount: number;\n /** Tax amount, in rupees (see `amount`). Default 0. */\n taxAmount?: number;\n /** Grand total, in rupees (see `amount`). 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacspace/esewa",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|