@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 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 **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
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
@@ -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
- /** Base product amount. */
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
- /** Base product amount. */
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":";AA0BO,IAAM,eAAA,GAA4C;AAAA,EACvD,IAAA,EAAM,oDAAA;AAAA,EACN,IAAA,EAAM;AACR;AAGO,IAAM,iBAAA,GAA8C;AAAA,EACzD,IAAA,EAAM,sDAAA;AAAA,EACN,IAAA,EAAM;AACR;AAGO,IAAM,iBAAA,GAAoB;AAG1B,IAAM,uBAAA,GAA0B;AAGhC,IAAM,wBAAA,GAA2B;AAMxC,IAAM,GAAA,GAAM,IAAI,WAAA,EAAY;AAC5B,IAAM,GAAA,GAAM,kEAAA;AAGZ,SAAS,SAAS,KAAA,EAA2B;AAC3C,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,EAAG;AACxC,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,GAAK,CAAA;AAClD,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,GAAK,CAAA;AAClD,IAAA,GAAA,IAAO,GAAA,CAAI,MAAM,CAAC,CAAA;AAClB,IAAA,GAAA,IAAO,GAAA,CAAA,CAAM,EAAA,GAAK,CAAA,KAAM,CAAA,GAAM,MAAM,CAAE,CAAA;AACtC,IAAA,GAAA,IAAO,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAA,CAAM,KAAK,EAAA,KAAO,CAAA,GAAM,EAAA,IAAM,CAAE,CAAA,GAAI,GAAA;AAClE,IAAA,GAAA,IAAO,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,GAAA,CAAI,EAAA,GAAK,EAAE,CAAA,GAAI,GAAA;AAAA,EAC/C;AACA,EAAA,OAAO,GAAA;AACT;AAGA,SAAS,WAAW,GAAA,EAAyB;AAC3C,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,OAAA,CAAQ,iBAAA,EAAmB,EAAE,CAAA;AAC/C,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAO,KAAA,CAAM,MAAA,GAAS,IAAK,CAAC,CAAA;AAC7C,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,GAAG,CAAA;AAChC,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,EAAG;AACxC,IAAA,MAAM,EAAA,GAAK,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAE,CAAA;AAChC,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA;AACpC,IAAA,MAAM,EAAA,GAAK,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA,GAAI,EAAA;AAC/D,IAAA,MAAM,EAAA,GAAK,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA,GAAI,EAAA;AAC/D,IAAA,IAAI,IAAI,GAAA,EAAK,KAAA,CAAM,GAAG,CAAA,GAAK,EAAA,IAAM,IAAM,EAAA,IAAM,CAAA;AAC7C,IAAA,IAAI,EAAA,IAAM,CAAA,IAAK,CAAA,GAAI,GAAA,EAAK,KAAA,CAAM,GAAG,CAAA,GAAA,CAAM,EAAA,GAAK,EAAA,KAAO,CAAA,GAAM,EAAA,IAAM,CAAA;AAC/D,IAAA,IAAI,EAAA,IAAM,KAAK,CAAA,GAAI,GAAA,QAAW,CAAA,EAAG,CAAA,GAAA,CAAM,EAAA,GAAK,CAAA,KAAM,CAAA,GAAK,EAAA;AAAA,EACzD;AACA,EAAA,OAAO,KAAA;AACT;AAGA,eAAe,gBAAA,CAAiB,QAAgB,OAAA,EAAkC;AAChF,EAAA,MAAM,MAAA,GAAS,WAAW,MAAA,EAAQ,MAAA;AAClC,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,wFAAwF,CAAA;AACrH,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,SAAA;AAAA,IACvB,KAAA;AAAA,IACA,GAAA,CAAI,OAAO,MAAM,CAAA;AAAA,IACjB,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AACA,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,IAAA,CAAK,QAAQ,GAAA,EAAK,GAAA,CAAI,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9D,EAAA,OAAO,QAAA,CAAS,IAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AACrC;AAGA,SAAS,eAAA,CAAgB,GAAW,CAAA,EAAoB;AACtD,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AACvB,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AAEvB,EAAA,IAAI,IAAA,GAAO,EAAA,CAAG,MAAA,GAAS,EAAA,CAAG,MAAA;AAC1B,EAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,GAAG,MAAM,CAAA;AACvC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK,IAAA,IAAA,CAAS,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,KAAM,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,CAAA;AAC7D,EAAA,OAAO,IAAA,KAAS,CAAA;AAClB;AAwBA,eAAsB,WAAA,CAAY,QAAoB,MAAA,EAAiC;AACrF,EAAA,MAAM,OAAA,GACJ,gBAAgB,MAAA,CAAO,YAAY,qBACf,MAAA,CAAO,gBAAgB,CAAA,cAAA,EAC3B,MAAA,CAAO,YAAY,CAAA,CAAA;AACrC,EAAA,OAAO,gBAAA,CAAiB,QAAQ,OAAO,CAAA;AACzC;AAgDA,eAAsB,SAAA,CACpB,OACA,IAAA,EACoB;AACpB,EAAA,MAAM,GAAA,GAAM,MAAM,SAAA,IAAa,CAAA;AAC/B,EAAA,MAAM,OAAA,GAAU,MAAM,oBAAA,IAAwB,CAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,MAAM,qBAAA,IAAyB,CAAA;AAChD,EAAA,MAAM,QAAQ,KAAA,CAAM,WAAA,IAAe,KAAA,CAAM,MAAA,GAAS,MAAM,OAAA,GAAU,QAAA;AAElE,EAAA,MAAM,YAAY,MAAM,WAAA;AAAA,IACtB;AAAA,MACE,YAAA,EAAc,KAAA;AAAA,MACd,kBAAkB,KAAA,CAAM,eAAA;AAAA,MACxB,cAAc,KAAA,CAAM;AAAA,KACtB;AAAA,IACA,IAAA,CAAK;AAAA,GACP;AAEA,EAAA,MAAM,MAAA,GAAiC;AAAA,IACrC,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA;AAAA,IAC3B,UAAA,EAAY,OAAO,GAAG,CAAA;AAAA,IACtB,YAAA,EAAc,OAAO,KAAK,CAAA;AAAA,IAC1B,kBAAkB,KAAA,CAAM,eAAA;AAAA,IACxB,cAAc,KAAA,CAAM,WAAA;AAAA,IACpB,sBAAA,EAAwB,OAAO,OAAO,CAAA;AAAA,IACtC,uBAAA,EAAyB,OAAO,QAAQ,CAAA;AAAA,IACxC,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,kBAAA,EAAoB,wBAAA;AAAA,IACpB;AAAA,GACF;AAEA,EAAA,OAAO,EAAE,QAAQ,eAAA,CAAgB,IAAA,CAAK,OAAO,MAAM,CAAA,EAAG,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAO;AAC/E;AAsBA,eAAsB,cAAA,CAAe,YAAoB,MAAA,EAAuC;AAC9F,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,IAAA,CAAK,MAAM,IAAI,WAAA,GAAc,MAAA,CAAO,UAAA,CAAW,UAAU,CAAC,CAAC,CAAA;AAAA,EACpE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,EAAC,EAAE;AAAA,EAClC;AAEA,EAAA,MAAM,QAAQ,OAAO,IAAA,CAAK,kBAAA,KAAuB,QAAA,GAAW,KAAK,kBAAA,GAAqB,EAAA;AACtF,EAAA,MAAM,WAAW,OAAO,IAAA,CAAK,SAAA,KAAc,QAAA,GAAW,KAAK,SAAA,GAAY,EAAA;AACvE,EAAA,IAAI,CAAC,SAAS,CAAC,QAAA,SAAiB,EAAE,KAAA,EAAO,OAAO,IAAA,EAAK;AAErD,EAAA,MAAM,UAAU,KAAA,CACb,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,IAAI,CAAA,IAAK,EAAE,CAAA,CAAE,CAAA,CAC3C,KAAK,GAAG,CAAA;AACX,EAAA,MAAM,QAAA,GAAW,MAAM,gBAAA,CAAiB,MAAA,EAAQ,OAAO,CAAA;AAEvD,EAAA,OAAO,EAAE,KAAA,EAAO,eAAA,CAAgB,QAAA,EAAU,QAAQ,GAAG,IAAA,EAAK;AAC5D;AAwBA,eAAsB,WAAA,CACpB,QACA,IAAA,EACkB;AAClB,EAAA,MAAM,IAAA,GAAO,iBAAA,CAAkB,IAAA,EAAM,GAAA,IAAO,MAAM,CAAA;AAClD,EAAA,MAAM,EAAA,GAAK,IAAI,eAAA,CAAgB;AAAA,IAC7B,cAAc,MAAA,CAAO,YAAA;AAAA,IACrB,YAAA,EAAc,MAAA,CAAO,MAAA,CAAO,YAAY,CAAA;AAAA,IACxC,kBAAkB,MAAA,CAAO;AAAA,GAC1B,EAAE,QAAA,EAAS;AACZ,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA;AAEzB,EAAA,MAAM,OAAA,GAAU,IAAA,EAAM,KAAA,IAAS,UAAA,CAAW,KAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,gEAAgE,CAAA;AAC9F,EAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,KAAK,EAAE,MAAA,EAAQ,OAAO,CAAA;AAChD,EAAA,OAAO,IAAI,IAAA,EAAK;AAClB","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":";AA0BO,IAAM,eAAA,GAA4C;AAAA,EACvD,IAAA,EAAM,oDAAA;AAAA,EACN,IAAA,EAAM;AACR;AAGO,IAAM,iBAAA,GAA8C;AAAA,EACzD,IAAA,EAAM,sDAAA;AAAA,EACN,IAAA,EAAM;AACR;AAGO,IAAM,iBAAA,GAAoB;AAG1B,IAAM,uBAAA,GAA0B;AAGhC,IAAM,wBAAA,GAA2B;AAMxC,IAAM,GAAA,GAAM,IAAI,WAAA,EAAY;AAC5B,IAAM,GAAA,GAAM,kEAAA;AAGZ,SAAS,SAAS,KAAA,EAA2B;AAC3C,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,EAAG;AACxC,IAAA,MAAM,EAAA,GAAK,MAAM,CAAC,CAAA;AAClB,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,GAAK,CAAA;AAClD,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,GAAK,CAAA;AAClD,IAAA,GAAA,IAAO,GAAA,CAAI,MAAM,CAAC,CAAA;AAClB,IAAA,GAAA,IAAO,GAAA,CAAA,CAAM,EAAA,GAAK,CAAA,KAAM,CAAA,GAAM,MAAM,CAAE,CAAA;AACtC,IAAA,GAAA,IAAO,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAA,CAAM,KAAK,EAAA,KAAO,CAAA,GAAM,EAAA,IAAM,CAAE,CAAA,GAAI,GAAA;AAClE,IAAA,GAAA,IAAO,IAAI,CAAA,GAAI,KAAA,CAAM,SAAS,GAAA,CAAI,EAAA,GAAK,EAAE,CAAA,GAAI,GAAA;AAAA,EAC/C;AACA,EAAA,OAAO,GAAA;AACT;AAGA,SAAS,WAAW,GAAA,EAAyB;AAC3C,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,OAAA,CAAQ,iBAAA,EAAmB,EAAE,CAAA;AAC/C,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAO,KAAA,CAAM,MAAA,GAAS,IAAK,CAAC,CAAA;AAC7C,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,GAAG,CAAA;AAChC,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,EAAG;AACxC,IAAA,MAAM,EAAA,GAAK,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAE,CAAA;AAChC,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA;AACpC,IAAA,MAAM,EAAA,GAAK,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA,GAAI,EAAA;AAC/D,IAAA,MAAM,EAAA,GAAK,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAE,CAAA,GAAI,EAAA;AAC/D,IAAA,IAAI,IAAI,GAAA,EAAK,KAAA,CAAM,GAAG,CAAA,GAAK,EAAA,IAAM,IAAM,EAAA,IAAM,CAAA;AAC7C,IAAA,IAAI,EAAA,IAAM,CAAA,IAAK,CAAA,GAAI,GAAA,EAAK,KAAA,CAAM,GAAG,CAAA,GAAA,CAAM,EAAA,GAAK,EAAA,KAAO,CAAA,GAAM,EAAA,IAAM,CAAA;AAC/D,IAAA,IAAI,EAAA,IAAM,KAAK,CAAA,GAAI,GAAA,QAAW,CAAA,EAAG,CAAA,GAAA,CAAM,EAAA,GAAK,CAAA,KAAM,CAAA,GAAK,EAAA;AAAA,EACzD;AACA,EAAA,OAAO,KAAA;AACT;AAGA,eAAe,gBAAA,CAAiB,QAAgB,OAAA,EAAkC;AAChF,EAAA,MAAM,MAAA,GAAS,WAAW,MAAA,EAAQ,MAAA;AAClC,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,wFAAwF,CAAA;AACrH,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,SAAA;AAAA,IACvB,KAAA;AAAA,IACA,GAAA,CAAI,OAAO,MAAM,CAAA;AAAA,IACjB,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AACA,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,IAAA,CAAK,QAAQ,GAAA,EAAK,GAAA,CAAI,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9D,EAAA,OAAO,QAAA,CAAS,IAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AACrC;AAGA,SAAS,eAAA,CAAgB,GAAW,CAAA,EAAoB;AACtD,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AACvB,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AAEvB,EAAA,IAAI,IAAA,GAAO,EAAA,CAAG,MAAA,GAAS,EAAA,CAAG,MAAA;AAC1B,EAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,GAAG,MAAM,CAAA;AACvC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK,IAAA,IAAA,CAAS,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,KAAM,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,CAAA;AAC7D,EAAA,OAAO,IAAA,KAAS,CAAA;AAClB;AAwBA,eAAsB,WAAA,CAAY,QAAoB,MAAA,EAAiC;AACrF,EAAA,MAAM,OAAA,GACJ,gBAAgB,MAAA,CAAO,YAAY,qBACf,MAAA,CAAO,gBAAgB,CAAA,cAAA,EAC3B,MAAA,CAAO,YAAY,CAAA,CAAA;AACrC,EAAA,OAAO,gBAAA,CAAiB,QAAQ,OAAO,CAAA;AACzC;AAeO,SAAS,cAAc,KAAA,EAAuB;AACnD,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,GAAI,GAAA;AAC7B;AAgDA,eAAsB,SAAA,CACpB,OACA,IAAA,EACoB;AACpB,EAAA,MAAM,GAAA,GAAM,MAAM,SAAA,IAAa,CAAA;AAC/B,EAAA,MAAM,OAAA,GAAU,MAAM,oBAAA,IAAwB,CAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,MAAM,qBAAA,IAAyB,CAAA;AAChD,EAAA,MAAM,QAAQ,KAAA,CAAM,WAAA,IAAe,KAAA,CAAM,MAAA,GAAS,MAAM,OAAA,GAAU,QAAA;AAElE,EAAA,MAAM,YAAY,MAAM,WAAA;AAAA,IACtB;AAAA,MACE,YAAA,EAAc,KAAA;AAAA,MACd,kBAAkB,KAAA,CAAM,eAAA;AAAA,MACxB,cAAc,KAAA,CAAM;AAAA,KACtB;AAAA,IACA,IAAA,CAAK;AAAA,GACP;AAEA,EAAA,MAAM,MAAA,GAAiC;AAAA,IACrC,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA;AAAA,IAC3B,UAAA,EAAY,OAAO,GAAG,CAAA;AAAA,IACtB,YAAA,EAAc,OAAO,KAAK,CAAA;AAAA,IAC1B,kBAAkB,KAAA,CAAM,eAAA;AAAA,IACxB,cAAc,KAAA,CAAM,WAAA;AAAA,IACpB,sBAAA,EAAwB,OAAO,OAAO,CAAA;AAAA,IACtC,uBAAA,EAAyB,OAAO,QAAQ,CAAA;AAAA,IACxC,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,kBAAA,EAAoB,wBAAA;AAAA,IACpB;AAAA,GACF;AAEA,EAAA,OAAO,EAAE,QAAQ,eAAA,CAAgB,IAAA,CAAK,OAAO,MAAM,CAAA,EAAG,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAO;AAC/E;AAsBA,eAAsB,cAAA,CAAe,YAAoB,MAAA,EAAuC;AAC9F,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,IAAA,CAAK,MAAM,IAAI,WAAA,GAAc,MAAA,CAAO,UAAA,CAAW,UAAU,CAAC,CAAC,CAAA;AAAA,EACpE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,EAAC,EAAE;AAAA,EAClC;AAEA,EAAA,MAAM,QAAQ,OAAO,IAAA,CAAK,kBAAA,KAAuB,QAAA,GAAW,KAAK,kBAAA,GAAqB,EAAA;AACtF,EAAA,MAAM,WAAW,OAAO,IAAA,CAAK,SAAA,KAAc,QAAA,GAAW,KAAK,SAAA,GAAY,EAAA;AACvE,EAAA,IAAI,CAAC,SAAS,CAAC,QAAA,SAAiB,EAAE,KAAA,EAAO,OAAO,IAAA,EAAK;AAErD,EAAA,MAAM,UAAU,KAAA,CACb,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,IAAI,CAAA,IAAK,EAAE,CAAA,CAAE,CAAA,CAC3C,KAAK,GAAG,CAAA;AACX,EAAA,MAAM,QAAA,GAAW,MAAM,gBAAA,CAAiB,MAAA,EAAQ,OAAO,CAAA;AAEvD,EAAA,OAAO,EAAE,KAAA,EAAO,eAAA,CAAgB,QAAA,EAAU,QAAQ,GAAG,IAAA,EAAK;AAC5D;AAwBA,eAAsB,WAAA,CACpB,QACA,IAAA,EACkB;AAClB,EAAA,MAAM,IAAA,GAAO,iBAAA,CAAkB,IAAA,EAAM,GAAA,IAAO,MAAM,CAAA;AAClD,EAAA,MAAM,EAAA,GAAK,IAAI,eAAA,CAAgB;AAAA,IAC7B,cAAc,MAAA,CAAO,YAAA;AAAA,IACrB,YAAA,EAAc,MAAA,CAAO,MAAA,CAAO,YAAY,CAAA;AAAA,IACxC,kBAAkB,MAAA,CAAO;AAAA,GAC1B,EAAE,QAAA,EAAS;AACZ,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA;AAEzB,EAAA,MAAM,OAAA,GAAU,IAAA,EAAM,KAAA,IAAS,UAAA,CAAW,KAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,gEAAgE,CAAA;AAC9F,EAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,KAAK,EAAE,MAAA,EAAQ,OAAO,CAAA;AAChD,EAAA,OAAO,IAAI,IAAA,EAAK;AAClB","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.0.0",
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",