@lacspace/fonepay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/README.md +104 -0
- package/dist/index.cjs +88 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence โ a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/fonepay
|
|
4
|
+
|
|
5
|
+
**Fonepay (Nepal) merchant redirect / Request-To-Pay over Web Crypto โ HMAC-SHA512 request signing + response verification.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/fonepay)
|
|
8
|
+
[](https://packagephobia.com/result?p=@lacspace/fonepay)
|
|
9
|
+
[](https://bundlephobia.com/package/@lacspace/fonepay)
|
|
10
|
+
[](https://www.npmjs.com/package/@lacspace/fonepay)
|
|
11
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> [Fonepay](https://fonepay.com/) merchant redirect ("Request-To-Pay") signs the request with an **HMAC-SHA512** data-validation field (`DV`) over the request values in an exact order, and returns a `DV` on the response you must verify the same way. This package computes both โ correctly, in a few bytes.
|
|
16
|
+
|
|
17
|
+
- ๐ **HMAC-SHA512** (lowercase hex) over the fields in Fonepay's exact order
|
|
18
|
+
- ๐ `buildRedirect()` assembles the full urlencoded gateway URL (incl. `DV`)
|
|
19
|
+
- โ
`verifyResponse()` recomputes the response `DV` with a **constant-time** compare
|
|
20
|
+
- ๐ก๏ธ Built on **Web Crypto** (`globalThis.crypto.subtle`) โ never hand-rolled cryptography
|
|
21
|
+
- โก Isomorphic โ Node 20+, edge runtimes & browsers ยท ๐ฆ ESM + CJS ยท fully typed ยท **zero dependencies**
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @lacspace/fonepay # or pnpm add / yarn add / bun add
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Redirect the payer
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { buildRedirect } from "@lacspace/fonepay";
|
|
33
|
+
|
|
34
|
+
const { url } = await buildRedirect(
|
|
35
|
+
{
|
|
36
|
+
PID: "MERCHANT", // merchant code
|
|
37
|
+
PRN: "prn-0001", // unique product/reference number
|
|
38
|
+
AMT: 1000, // amount
|
|
39
|
+
DT: "09/05/2026", // date
|
|
40
|
+
R1: "order note",
|
|
41
|
+
R2: "buyer ref",
|
|
42
|
+
RU: "https://shop.me/fonepay/return", // return URL
|
|
43
|
+
// MD defaults to "P", CRN defaults to "NPR"
|
|
44
|
+
},
|
|
45
|
+
{ secret: process.env.FONEPAY_SECRET!, env: "prod" },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return Response.redirect(url, 302);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The signed message is HMAC-SHA512 over `PID,MD,PRN,AMT,CRN,DT,R1,R2,RU` (joined by `,`), emitted as lowercase hex โ the `DV` param.
|
|
52
|
+
|
|
53
|
+
## Verify the response
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { verifyResponse } from "@lacspace/fonepay";
|
|
57
|
+
|
|
58
|
+
// query = the params Fonepay sent back to your return URL
|
|
59
|
+
const { valid } = await verifyResponse(
|
|
60
|
+
{
|
|
61
|
+
PRN: query.PRN, PID: query.PID, PS: query.PS, RC: query.RC,
|
|
62
|
+
UID: query.UID, BC: query.BC, INI: query.INI,
|
|
63
|
+
P_AMT: query.P_AMT, R_AMT: query.R_AMT, DV: query.DV,
|
|
64
|
+
},
|
|
65
|
+
process.env.FONEPAY_SECRET!,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!valid) return new Response("Invalid Fonepay response", { status: 400 });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The response `DV` is HMAC-SHA512 over `PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT` โ recomputed and compared in constant time.
|
|
72
|
+
|
|
73
|
+
## Just the signature
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { signRequest } from "@lacspace/fonepay";
|
|
77
|
+
|
|
78
|
+
const dv = await signRequest(params, secret); // 128-char lowercase hex
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
| Export | Description |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `signRequest(params, secret)` | HMAC-SHA512 (hex) request `DV`. `MD` โ `"P"`, `CRN` โ `"NPR"` by default. |
|
|
86
|
+
| `buildRedirect(params, { secret, env? })` | `{ url, params, dv }` โ the full gateway URL with all fields + `DV`. |
|
|
87
|
+
| `verifyResponse(resp, secret)` | `{ valid }` โ constant-time verify of the response `DV`. |
|
|
88
|
+
| `GATEWAY_URL` | `{ test, prod }` endpoint map. |
|
|
89
|
+
|
|
90
|
+
`env` defaults to `"test"` (dev gateway). Keep your Fonepay **secret** on the server only.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## The Lacspace Developer Platform
|
|
95
|
+
|
|
96
|
+
`@lacspace/fonepay` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
97
|
+
|
|
98
|
+
- ๐๏ธ **All packages** โ https://developer.lacspace.com/packages
|
|
99
|
+
- ๐งญ **Developer handbook** โ https://developer.lacspace.com/handbook
|
|
100
|
+
- ๐งช **Live playground** โ https://developer.lacspace.com/playground
|
|
101
|
+
- ๐ฅ๏ธ **Finished app templates** โ https://templates.lacspace.com
|
|
102
|
+
- ๐ **Scaffold a full app** โ `npm create lacspace-app@latest`
|
|
103
|
+
|
|
104
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** โ a permissive, free-to-use licence.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var GATEWAY_URL = {
|
|
5
|
+
test: "https://dev-clientapi.fonepay.com/api/merchantRequest",
|
|
6
|
+
prod: "https://clientapi.fonepay.com/api/merchantRequest"
|
|
7
|
+
};
|
|
8
|
+
var enc = new TextEncoder();
|
|
9
|
+
function toHex(bytes) {
|
|
10
|
+
let hex = "";
|
|
11
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
12
|
+
return hex;
|
|
13
|
+
}
|
|
14
|
+
async function hmacHex(secret, message) {
|
|
15
|
+
const key = await crypto.subtle.importKey(
|
|
16
|
+
"raw",
|
|
17
|
+
enc.encode(secret),
|
|
18
|
+
{ name: "HMAC", hash: "SHA-512" },
|
|
19
|
+
false,
|
|
20
|
+
["sign"]
|
|
21
|
+
);
|
|
22
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
23
|
+
return toHex(new Uint8Array(sig));
|
|
24
|
+
}
|
|
25
|
+
function timingSafeEqual(a, b) {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
let diff = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
29
|
+
return diff === 0;
|
|
30
|
+
}
|
|
31
|
+
function resolveRequest(p) {
|
|
32
|
+
return {
|
|
33
|
+
PID: String(p.PID),
|
|
34
|
+
MD: String(p.MD ?? "P"),
|
|
35
|
+
PRN: String(p.PRN),
|
|
36
|
+
AMT: String(p.AMT),
|
|
37
|
+
CRN: String(p.CRN ?? "NPR"),
|
|
38
|
+
DT: String(p.DT),
|
|
39
|
+
R1: String(p.R1),
|
|
40
|
+
R2: String(p.R2),
|
|
41
|
+
RU: String(p.RU)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function signRequest(params, secret) {
|
|
45
|
+
const r = resolveRequest(params);
|
|
46
|
+
const message = [r.PID, r.MD, r.PRN, r.AMT, r.CRN, r.DT, r.R1, r.R2, r.RU].join(",");
|
|
47
|
+
return hmacHex(secret, message);
|
|
48
|
+
}
|
|
49
|
+
async function buildRedirect(params, opts) {
|
|
50
|
+
const r = resolveRequest(params);
|
|
51
|
+
const dv = await signRequest(params, opts.secret);
|
|
52
|
+
const fields = {
|
|
53
|
+
PID: r.PID,
|
|
54
|
+
MD: r.MD,
|
|
55
|
+
PRN: r.PRN,
|
|
56
|
+
AMT: r.AMT,
|
|
57
|
+
CRN: r.CRN,
|
|
58
|
+
DT: r.DT,
|
|
59
|
+
R1: r.R1,
|
|
60
|
+
R2: r.R2,
|
|
61
|
+
RU: r.RU,
|
|
62
|
+
DV: dv
|
|
63
|
+
};
|
|
64
|
+
const qs = new URLSearchParams(fields).toString();
|
|
65
|
+
return { url: `${GATEWAY_URL[opts.env ?? "test"]}?${qs}`, params: fields, dv };
|
|
66
|
+
}
|
|
67
|
+
async function verifyResponse(resp, secret) {
|
|
68
|
+
const message = [
|
|
69
|
+
String(resp.PRN),
|
|
70
|
+
String(resp.PID),
|
|
71
|
+
String(resp.PS),
|
|
72
|
+
String(resp.RC),
|
|
73
|
+
String(resp.UID),
|
|
74
|
+
String(resp.BC),
|
|
75
|
+
String(resp.INI),
|
|
76
|
+
String(resp.P_AMT),
|
|
77
|
+
String(resp.R_AMT)
|
|
78
|
+
].join(",");
|
|
79
|
+
const expected = await hmacHex(secret, message);
|
|
80
|
+
return { valid: timingSafeEqual(expected, String(resp.DV).toLowerCase()) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
exports.GATEWAY_URL = GATEWAY_URL;
|
|
84
|
+
exports.buildRedirect = buildRedirect;
|
|
85
|
+
exports.signRequest = signRequest;
|
|
86
|
+
exports.verifyResponse = verifyResponse;
|
|
87
|
+
//# sourceMappingURL=index.cjs.map
|
|
88
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/fonepay\n *\n * Fonepay (Nepal) merchant redirect โ \"Request-To-Pay\" โ over Web Crypto.\n * The merchant redirects the payer to Fonepay with an HMAC-SHA512-signed set\n * of request parameters (the `DV` data-validation field), and verifies the\n * `DV` on the response the same way.\n *\n * This package builds both signatures correctly:\n *\n * 1. Request DV โ HMAC-SHA512 over the request fields joined by \",\" in the\n * exact Fonepay order, lowercase hex.\n * 2. Response DV โ HMAC-SHA512 over the response fields, constant-time\n * compared to the returned `DV`.\n *\n * Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers โ all\n * cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.\n */\n\n/* ------------------------------------------------------------------ *\n * Endpoints\n * ------------------------------------------------------------------ */\n\nexport type Env = \"test\" | \"prod\";\n\n/** Fonepay merchant-request gateway URLs, by environment. */\nexport const GATEWAY_URL: Record<Env, string> = {\n test: \"https://dev-clientapi.fonepay.com/api/merchantRequest\",\n prod: \"https://clientapi.fonepay.com/api/merchantRequest\",\n};\n\n/* ------------------------------------------------------------------ *\n * HMAC-SHA512 (hex) helpers โ isomorphic, zero-dep\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction toHex(bytes: Uint8Array): string {\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) hex += bytes[i]!.toString(16).padStart(2, \"0\");\n return hex;\n}\n\n/** HMAC-SHA512 of `message` under `secret`, lowercase hex. */\nasync function hmacHex(secret: string, message: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", key, enc.encode(message));\n return toHex(new Uint8Array(sig));\n}\n\n/** Constant-time comparison of two hex strings. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Request signing\n * ------------------------------------------------------------------ */\n\nexport interface RequestParams {\n /** Merchant code (PID). */\n PID: string;\n /** Method / mode. Default \"P\". */\n MD?: string;\n /** Product / reference number โ unique per transaction. */\n PRN: string;\n /** Amount. */\n AMT: string | number;\n /** Currency. Default \"NPR\". */\n CRN?: string;\n /** Date (as required by Fonepay). */\n DT: string;\n /** Free field R1. */\n R1: string;\n /** Free field R2. */\n R2: string;\n /** Return URL. */\n RU: string;\n}\n\ninterface ResolvedRequest {\n PID: string;\n MD: string;\n PRN: string;\n AMT: string;\n CRN: string;\n DT: string;\n R1: string;\n R2: string;\n RU: string;\n}\n\nfunction resolveRequest(p: RequestParams): ResolvedRequest {\n return {\n PID: String(p.PID),\n MD: String(p.MD ?? \"P\"),\n PRN: String(p.PRN),\n AMT: String(p.AMT),\n CRN: String(p.CRN ?? \"NPR\"),\n DT: String(p.DT),\n R1: String(p.R1),\n R2: String(p.R2),\n RU: String(p.RU),\n };\n}\n\n/**\n * Sign a Fonepay Request-To-Pay: HMAC-SHA512 over the request values joined by\n * \",\" in the exact order `PID,MD,PRN,AMT,CRN,DT,R1,R2,RU`. Returns lowercase\n * hex โ this is the `DV` field. `MD` defaults to `\"P\"`, `CRN` to `\"NPR\"`.\n *\n * @example\n * const dv = await signRequest(\n * { PID: \"MERCHANT\", PRN: \"prn-1\", AMT: 1000, DT: \"09/05/2026\",\n * R1: \"test\", R2: \"test\", RU: \"https://shop.me/return\" },\n * process.env.FONEPAY_SECRET!,\n * );\n */\nexport async function signRequest(params: RequestParams, secret: string): Promise<string> {\n const r = resolveRequest(params);\n const message = [r.PID, r.MD, r.PRN, r.AMT, r.CRN, r.DT, r.R1, r.R2, r.RU].join(\",\");\n return hmacHex(secret, message);\n}\n\n/* ------------------------------------------------------------------ *\n * Redirect builder\n * ------------------------------------------------------------------ */\n\nexport interface BuildRedirectOptions {\n secret: string;\n /** Which gateway to target. Default \"test\" (dev). */\n env?: Env;\n}\n\nexport interface Redirect {\n /** The full redirect URL (gateway + urlencoded params, including `DV`). */\n url: string;\n /** All request params plus the computed `DV`. */\n params: Record<string, string>;\n /** The request signature. */\n dv: string;\n}\n\n/**\n * Build the full Fonepay redirect: signs the request, assembles all params\n * (including `DV`) and produces the gateway URL with everything urlencoded.\n *\n * @example\n * const { url } = await buildRedirect(params, { secret, env: \"prod\" });\n * return Response.redirect(url, 302);\n */\nexport async function buildRedirect(params: RequestParams, opts: BuildRedirectOptions): Promise<Redirect> {\n const r = resolveRequest(params);\n const dv = await signRequest(params, opts.secret);\n const fields: Record<string, string> = {\n PID: r.PID,\n MD: r.MD,\n PRN: r.PRN,\n AMT: r.AMT,\n CRN: r.CRN,\n DT: r.DT,\n R1: r.R1,\n R2: r.R2,\n RU: r.RU,\n DV: dv,\n };\n const qs = new URLSearchParams(fields).toString();\n return { url: `${GATEWAY_URL[opts.env ?? \"test\"]}?${qs}`, params: fields, dv };\n}\n\n/* ------------------------------------------------------------------ *\n * Response verification\n * ------------------------------------------------------------------ */\n\nexport interface ResponseParams {\n PRN: string;\n PID: string;\n PS: string;\n RC: string;\n UID: string;\n BC: string;\n INI: string;\n P_AMT: string | number;\n R_AMT: string | number;\n /** The DV returned by Fonepay to verify. */\n DV: string;\n}\n\n/**\n * Verify a Fonepay response: recompute HMAC-SHA512 over the response fields\n * joined by \",\" in the order `PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT`, then\n * constant-time compare it to the returned `DV`.\n *\n * @example\n * const { valid } = await verifyResponse(query, process.env.FONEPAY_SECRET!);\n * if (!valid) return new Response(\"Invalid Fonepay response\", { status: 400 });\n */\nexport async function verifyResponse(resp: ResponseParams, secret: string): Promise<{ valid: boolean }> {\n const message = [\n String(resp.PRN),\n String(resp.PID),\n String(resp.PS),\n String(resp.RC),\n String(resp.UID),\n String(resp.BC),\n String(resp.INI),\n String(resp.P_AMT),\n String(resp.R_AMT),\n ].join(\",\");\n const expected = await hmacHex(secret, message);\n return { valid: timingSafeEqual(expected, String(resp.DV).toLowerCase()) };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/fonepay
|
|
3
|
+
*
|
|
4
|
+
* Fonepay (Nepal) merchant redirect โ "Request-To-Pay" โ over Web Crypto.
|
|
5
|
+
* The merchant redirects the payer to Fonepay with an HMAC-SHA512-signed set
|
|
6
|
+
* of request parameters (the `DV` data-validation field), and verifies the
|
|
7
|
+
* `DV` on the response the same way.
|
|
8
|
+
*
|
|
9
|
+
* This package builds both signatures correctly:
|
|
10
|
+
*
|
|
11
|
+
* 1. Request DV โ HMAC-SHA512 over the request fields joined by "," in the
|
|
12
|
+
* exact Fonepay order, lowercase hex.
|
|
13
|
+
* 2. Response DV โ HMAC-SHA512 over the response fields, constant-time
|
|
14
|
+
* compared to the returned `DV`.
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers โ all
|
|
17
|
+
* cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.
|
|
18
|
+
*/
|
|
19
|
+
type Env = "test" | "prod";
|
|
20
|
+
/** Fonepay merchant-request gateway URLs, by environment. */
|
|
21
|
+
declare const GATEWAY_URL: Record<Env, string>;
|
|
22
|
+
interface RequestParams {
|
|
23
|
+
/** Merchant code (PID). */
|
|
24
|
+
PID: string;
|
|
25
|
+
/** Method / mode. Default "P". */
|
|
26
|
+
MD?: string;
|
|
27
|
+
/** Product / reference number โ unique per transaction. */
|
|
28
|
+
PRN: string;
|
|
29
|
+
/** Amount. */
|
|
30
|
+
AMT: string | number;
|
|
31
|
+
/** Currency. Default "NPR". */
|
|
32
|
+
CRN?: string;
|
|
33
|
+
/** Date (as required by Fonepay). */
|
|
34
|
+
DT: string;
|
|
35
|
+
/** Free field R1. */
|
|
36
|
+
R1: string;
|
|
37
|
+
/** Free field R2. */
|
|
38
|
+
R2: string;
|
|
39
|
+
/** Return URL. */
|
|
40
|
+
RU: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sign a Fonepay Request-To-Pay: HMAC-SHA512 over the request values joined by
|
|
44
|
+
* "," in the exact order `PID,MD,PRN,AMT,CRN,DT,R1,R2,RU`. Returns lowercase
|
|
45
|
+
* hex โ this is the `DV` field. `MD` defaults to `"P"`, `CRN` to `"NPR"`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const dv = await signRequest(
|
|
49
|
+
* { PID: "MERCHANT", PRN: "prn-1", AMT: 1000, DT: "09/05/2026",
|
|
50
|
+
* R1: "test", R2: "test", RU: "https://shop.me/return" },
|
|
51
|
+
* process.env.FONEPAY_SECRET!,
|
|
52
|
+
* );
|
|
53
|
+
*/
|
|
54
|
+
declare function signRequest(params: RequestParams, secret: string): Promise<string>;
|
|
55
|
+
interface BuildRedirectOptions {
|
|
56
|
+
secret: string;
|
|
57
|
+
/** Which gateway to target. Default "test" (dev). */
|
|
58
|
+
env?: Env;
|
|
59
|
+
}
|
|
60
|
+
interface Redirect {
|
|
61
|
+
/** The full redirect URL (gateway + urlencoded params, including `DV`). */
|
|
62
|
+
url: string;
|
|
63
|
+
/** All request params plus the computed `DV`. */
|
|
64
|
+
params: Record<string, string>;
|
|
65
|
+
/** The request signature. */
|
|
66
|
+
dv: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the full Fonepay redirect: signs the request, assembles all params
|
|
70
|
+
* (including `DV`) and produces the gateway URL with everything urlencoded.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const { url } = await buildRedirect(params, { secret, env: "prod" });
|
|
74
|
+
* return Response.redirect(url, 302);
|
|
75
|
+
*/
|
|
76
|
+
declare function buildRedirect(params: RequestParams, opts: BuildRedirectOptions): Promise<Redirect>;
|
|
77
|
+
interface ResponseParams {
|
|
78
|
+
PRN: string;
|
|
79
|
+
PID: string;
|
|
80
|
+
PS: string;
|
|
81
|
+
RC: string;
|
|
82
|
+
UID: string;
|
|
83
|
+
BC: string;
|
|
84
|
+
INI: string;
|
|
85
|
+
P_AMT: string | number;
|
|
86
|
+
R_AMT: string | number;
|
|
87
|
+
/** The DV returned by Fonepay to verify. */
|
|
88
|
+
DV: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Verify a Fonepay response: recompute HMAC-SHA512 over the response fields
|
|
92
|
+
* joined by "," in the order `PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT`, then
|
|
93
|
+
* constant-time compare it to the returned `DV`.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const { valid } = await verifyResponse(query, process.env.FONEPAY_SECRET!);
|
|
97
|
+
* if (!valid) return new Response("Invalid Fonepay response", { status: 400 });
|
|
98
|
+
*/
|
|
99
|
+
declare function verifyResponse(resp: ResponseParams, secret: string): Promise<{
|
|
100
|
+
valid: boolean;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
export { type BuildRedirectOptions, type Env, GATEWAY_URL, type Redirect, type RequestParams, type ResponseParams, buildRedirect, signRequest, verifyResponse };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/fonepay
|
|
3
|
+
*
|
|
4
|
+
* Fonepay (Nepal) merchant redirect โ "Request-To-Pay" โ over Web Crypto.
|
|
5
|
+
* The merchant redirects the payer to Fonepay with an HMAC-SHA512-signed set
|
|
6
|
+
* of request parameters (the `DV` data-validation field), and verifies the
|
|
7
|
+
* `DV` on the response the same way.
|
|
8
|
+
*
|
|
9
|
+
* This package builds both signatures correctly:
|
|
10
|
+
*
|
|
11
|
+
* 1. Request DV โ HMAC-SHA512 over the request fields joined by "," in the
|
|
12
|
+
* exact Fonepay order, lowercase hex.
|
|
13
|
+
* 2. Response DV โ HMAC-SHA512 over the response fields, constant-time
|
|
14
|
+
* compared to the returned `DV`.
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers โ all
|
|
17
|
+
* cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.
|
|
18
|
+
*/
|
|
19
|
+
type Env = "test" | "prod";
|
|
20
|
+
/** Fonepay merchant-request gateway URLs, by environment. */
|
|
21
|
+
declare const GATEWAY_URL: Record<Env, string>;
|
|
22
|
+
interface RequestParams {
|
|
23
|
+
/** Merchant code (PID). */
|
|
24
|
+
PID: string;
|
|
25
|
+
/** Method / mode. Default "P". */
|
|
26
|
+
MD?: string;
|
|
27
|
+
/** Product / reference number โ unique per transaction. */
|
|
28
|
+
PRN: string;
|
|
29
|
+
/** Amount. */
|
|
30
|
+
AMT: string | number;
|
|
31
|
+
/** Currency. Default "NPR". */
|
|
32
|
+
CRN?: string;
|
|
33
|
+
/** Date (as required by Fonepay). */
|
|
34
|
+
DT: string;
|
|
35
|
+
/** Free field R1. */
|
|
36
|
+
R1: string;
|
|
37
|
+
/** Free field R2. */
|
|
38
|
+
R2: string;
|
|
39
|
+
/** Return URL. */
|
|
40
|
+
RU: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sign a Fonepay Request-To-Pay: HMAC-SHA512 over the request values joined by
|
|
44
|
+
* "," in the exact order `PID,MD,PRN,AMT,CRN,DT,R1,R2,RU`. Returns lowercase
|
|
45
|
+
* hex โ this is the `DV` field. `MD` defaults to `"P"`, `CRN` to `"NPR"`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const dv = await signRequest(
|
|
49
|
+
* { PID: "MERCHANT", PRN: "prn-1", AMT: 1000, DT: "09/05/2026",
|
|
50
|
+
* R1: "test", R2: "test", RU: "https://shop.me/return" },
|
|
51
|
+
* process.env.FONEPAY_SECRET!,
|
|
52
|
+
* );
|
|
53
|
+
*/
|
|
54
|
+
declare function signRequest(params: RequestParams, secret: string): Promise<string>;
|
|
55
|
+
interface BuildRedirectOptions {
|
|
56
|
+
secret: string;
|
|
57
|
+
/** Which gateway to target. Default "test" (dev). */
|
|
58
|
+
env?: Env;
|
|
59
|
+
}
|
|
60
|
+
interface Redirect {
|
|
61
|
+
/** The full redirect URL (gateway + urlencoded params, including `DV`). */
|
|
62
|
+
url: string;
|
|
63
|
+
/** All request params plus the computed `DV`. */
|
|
64
|
+
params: Record<string, string>;
|
|
65
|
+
/** The request signature. */
|
|
66
|
+
dv: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the full Fonepay redirect: signs the request, assembles all params
|
|
70
|
+
* (including `DV`) and produces the gateway URL with everything urlencoded.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const { url } = await buildRedirect(params, { secret, env: "prod" });
|
|
74
|
+
* return Response.redirect(url, 302);
|
|
75
|
+
*/
|
|
76
|
+
declare function buildRedirect(params: RequestParams, opts: BuildRedirectOptions): Promise<Redirect>;
|
|
77
|
+
interface ResponseParams {
|
|
78
|
+
PRN: string;
|
|
79
|
+
PID: string;
|
|
80
|
+
PS: string;
|
|
81
|
+
RC: string;
|
|
82
|
+
UID: string;
|
|
83
|
+
BC: string;
|
|
84
|
+
INI: string;
|
|
85
|
+
P_AMT: string | number;
|
|
86
|
+
R_AMT: string | number;
|
|
87
|
+
/** The DV returned by Fonepay to verify. */
|
|
88
|
+
DV: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Verify a Fonepay response: recompute HMAC-SHA512 over the response fields
|
|
92
|
+
* joined by "," in the order `PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT`, then
|
|
93
|
+
* constant-time compare it to the returned `DV`.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const { valid } = await verifyResponse(query, process.env.FONEPAY_SECRET!);
|
|
97
|
+
* if (!valid) return new Response("Invalid Fonepay response", { status: 400 });
|
|
98
|
+
*/
|
|
99
|
+
declare function verifyResponse(resp: ResponseParams, secret: string): Promise<{
|
|
100
|
+
valid: boolean;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
export { type BuildRedirectOptions, type Env, GATEWAY_URL, type Redirect, type RequestParams, type ResponseParams, buildRedirect, signRequest, verifyResponse };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var GATEWAY_URL = {
|
|
3
|
+
test: "https://dev-clientapi.fonepay.com/api/merchantRequest",
|
|
4
|
+
prod: "https://clientapi.fonepay.com/api/merchantRequest"
|
|
5
|
+
};
|
|
6
|
+
var enc = new TextEncoder();
|
|
7
|
+
function toHex(bytes) {
|
|
8
|
+
let hex = "";
|
|
9
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
10
|
+
return hex;
|
|
11
|
+
}
|
|
12
|
+
async function hmacHex(secret, message) {
|
|
13
|
+
const key = await crypto.subtle.importKey(
|
|
14
|
+
"raw",
|
|
15
|
+
enc.encode(secret),
|
|
16
|
+
{ name: "HMAC", hash: "SHA-512" },
|
|
17
|
+
false,
|
|
18
|
+
["sign"]
|
|
19
|
+
);
|
|
20
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
21
|
+
return toHex(new Uint8Array(sig));
|
|
22
|
+
}
|
|
23
|
+
function timingSafeEqual(a, b) {
|
|
24
|
+
if (a.length !== b.length) return false;
|
|
25
|
+
let diff = 0;
|
|
26
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
27
|
+
return diff === 0;
|
|
28
|
+
}
|
|
29
|
+
function resolveRequest(p) {
|
|
30
|
+
return {
|
|
31
|
+
PID: String(p.PID),
|
|
32
|
+
MD: String(p.MD ?? "P"),
|
|
33
|
+
PRN: String(p.PRN),
|
|
34
|
+
AMT: String(p.AMT),
|
|
35
|
+
CRN: String(p.CRN ?? "NPR"),
|
|
36
|
+
DT: String(p.DT),
|
|
37
|
+
R1: String(p.R1),
|
|
38
|
+
R2: String(p.R2),
|
|
39
|
+
RU: String(p.RU)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function signRequest(params, secret) {
|
|
43
|
+
const r = resolveRequest(params);
|
|
44
|
+
const message = [r.PID, r.MD, r.PRN, r.AMT, r.CRN, r.DT, r.R1, r.R2, r.RU].join(",");
|
|
45
|
+
return hmacHex(secret, message);
|
|
46
|
+
}
|
|
47
|
+
async function buildRedirect(params, opts) {
|
|
48
|
+
const r = resolveRequest(params);
|
|
49
|
+
const dv = await signRequest(params, opts.secret);
|
|
50
|
+
const fields = {
|
|
51
|
+
PID: r.PID,
|
|
52
|
+
MD: r.MD,
|
|
53
|
+
PRN: r.PRN,
|
|
54
|
+
AMT: r.AMT,
|
|
55
|
+
CRN: r.CRN,
|
|
56
|
+
DT: r.DT,
|
|
57
|
+
R1: r.R1,
|
|
58
|
+
R2: r.R2,
|
|
59
|
+
RU: r.RU,
|
|
60
|
+
DV: dv
|
|
61
|
+
};
|
|
62
|
+
const qs = new URLSearchParams(fields).toString();
|
|
63
|
+
return { url: `${GATEWAY_URL[opts.env ?? "test"]}?${qs}`, params: fields, dv };
|
|
64
|
+
}
|
|
65
|
+
async function verifyResponse(resp, secret) {
|
|
66
|
+
const message = [
|
|
67
|
+
String(resp.PRN),
|
|
68
|
+
String(resp.PID),
|
|
69
|
+
String(resp.PS),
|
|
70
|
+
String(resp.RC),
|
|
71
|
+
String(resp.UID),
|
|
72
|
+
String(resp.BC),
|
|
73
|
+
String(resp.INI),
|
|
74
|
+
String(resp.P_AMT),
|
|
75
|
+
String(resp.R_AMT)
|
|
76
|
+
].join(",");
|
|
77
|
+
const expected = await hmacHex(secret, message);
|
|
78
|
+
return { valid: timingSafeEqual(expected, String(resp.DV).toLowerCase()) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { GATEWAY_URL, buildRedirect, signRequest, verifyResponse };
|
|
82
|
+
//# sourceMappingURL=index.js.map
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/fonepay\n *\n * Fonepay (Nepal) merchant redirect โ \"Request-To-Pay\" โ over Web Crypto.\n * The merchant redirects the payer to Fonepay with an HMAC-SHA512-signed set\n * of request parameters (the `DV` data-validation field), and verifies the\n * `DV` on the response the same way.\n *\n * This package builds both signatures correctly:\n *\n * 1. Request DV โ HMAC-SHA512 over the request fields joined by \",\" in the\n * exact Fonepay order, lowercase hex.\n * 2. Response DV โ HMAC-SHA512 over the response fields, constant-time\n * compared to the returned `DV`.\n *\n * Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers โ all\n * cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.\n */\n\n/* ------------------------------------------------------------------ *\n * Endpoints\n * ------------------------------------------------------------------ */\n\nexport type Env = \"test\" | \"prod\";\n\n/** Fonepay merchant-request gateway URLs, by environment. */\nexport const GATEWAY_URL: Record<Env, string> = {\n test: \"https://dev-clientapi.fonepay.com/api/merchantRequest\",\n prod: \"https://clientapi.fonepay.com/api/merchantRequest\",\n};\n\n/* ------------------------------------------------------------------ *\n * HMAC-SHA512 (hex) helpers โ isomorphic, zero-dep\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction toHex(bytes: Uint8Array): string {\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) hex += bytes[i]!.toString(16).padStart(2, \"0\");\n return hex;\n}\n\n/** HMAC-SHA512 of `message` under `secret`, lowercase hex. */\nasync function hmacHex(secret: string, message: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", key, enc.encode(message));\n return toHex(new Uint8Array(sig));\n}\n\n/** Constant-time comparison of two hex strings. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Request signing\n * ------------------------------------------------------------------ */\n\nexport interface RequestParams {\n /** Merchant code (PID). */\n PID: string;\n /** Method / mode. Default \"P\". */\n MD?: string;\n /** Product / reference number โ unique per transaction. */\n PRN: string;\n /** Amount. */\n AMT: string | number;\n /** Currency. Default \"NPR\". */\n CRN?: string;\n /** Date (as required by Fonepay). */\n DT: string;\n /** Free field R1. */\n R1: string;\n /** Free field R2. */\n R2: string;\n /** Return URL. */\n RU: string;\n}\n\ninterface ResolvedRequest {\n PID: string;\n MD: string;\n PRN: string;\n AMT: string;\n CRN: string;\n DT: string;\n R1: string;\n R2: string;\n RU: string;\n}\n\nfunction resolveRequest(p: RequestParams): ResolvedRequest {\n return {\n PID: String(p.PID),\n MD: String(p.MD ?? \"P\"),\n PRN: String(p.PRN),\n AMT: String(p.AMT),\n CRN: String(p.CRN ?? \"NPR\"),\n DT: String(p.DT),\n R1: String(p.R1),\n R2: String(p.R2),\n RU: String(p.RU),\n };\n}\n\n/**\n * Sign a Fonepay Request-To-Pay: HMAC-SHA512 over the request values joined by\n * \",\" in the exact order `PID,MD,PRN,AMT,CRN,DT,R1,R2,RU`. Returns lowercase\n * hex โ this is the `DV` field. `MD` defaults to `\"P\"`, `CRN` to `\"NPR\"`.\n *\n * @example\n * const dv = await signRequest(\n * { PID: \"MERCHANT\", PRN: \"prn-1\", AMT: 1000, DT: \"09/05/2026\",\n * R1: \"test\", R2: \"test\", RU: \"https://shop.me/return\" },\n * process.env.FONEPAY_SECRET!,\n * );\n */\nexport async function signRequest(params: RequestParams, secret: string): Promise<string> {\n const r = resolveRequest(params);\n const message = [r.PID, r.MD, r.PRN, r.AMT, r.CRN, r.DT, r.R1, r.R2, r.RU].join(\",\");\n return hmacHex(secret, message);\n}\n\n/* ------------------------------------------------------------------ *\n * Redirect builder\n * ------------------------------------------------------------------ */\n\nexport interface BuildRedirectOptions {\n secret: string;\n /** Which gateway to target. Default \"test\" (dev). */\n env?: Env;\n}\n\nexport interface Redirect {\n /** The full redirect URL (gateway + urlencoded params, including `DV`). */\n url: string;\n /** All request params plus the computed `DV`. */\n params: Record<string, string>;\n /** The request signature. */\n dv: string;\n}\n\n/**\n * Build the full Fonepay redirect: signs the request, assembles all params\n * (including `DV`) and produces the gateway URL with everything urlencoded.\n *\n * @example\n * const { url } = await buildRedirect(params, { secret, env: \"prod\" });\n * return Response.redirect(url, 302);\n */\nexport async function buildRedirect(params: RequestParams, opts: BuildRedirectOptions): Promise<Redirect> {\n const r = resolveRequest(params);\n const dv = await signRequest(params, opts.secret);\n const fields: Record<string, string> = {\n PID: r.PID,\n MD: r.MD,\n PRN: r.PRN,\n AMT: r.AMT,\n CRN: r.CRN,\n DT: r.DT,\n R1: r.R1,\n R2: r.R2,\n RU: r.RU,\n DV: dv,\n };\n const qs = new URLSearchParams(fields).toString();\n return { url: `${GATEWAY_URL[opts.env ?? \"test\"]}?${qs}`, params: fields, dv };\n}\n\n/* ------------------------------------------------------------------ *\n * Response verification\n * ------------------------------------------------------------------ */\n\nexport interface ResponseParams {\n PRN: string;\n PID: string;\n PS: string;\n RC: string;\n UID: string;\n BC: string;\n INI: string;\n P_AMT: string | number;\n R_AMT: string | number;\n /** The DV returned by Fonepay to verify. */\n DV: string;\n}\n\n/**\n * Verify a Fonepay response: recompute HMAC-SHA512 over the response fields\n * joined by \",\" in the order `PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT`, then\n * constant-time compare it to the returned `DV`.\n *\n * @example\n * const { valid } = await verifyResponse(query, process.env.FONEPAY_SECRET!);\n * if (!valid) return new Response(\"Invalid Fonepay response\", { status: 400 });\n */\nexport async function verifyResponse(resp: ResponseParams, secret: string): Promise<{ valid: boolean }> {\n const message = [\n String(resp.PRN),\n String(resp.PID),\n String(resp.PS),\n String(resp.RC),\n String(resp.UID),\n String(resp.BC),\n String(resp.INI),\n String(resp.P_AMT),\n String(resp.R_AMT),\n ].join(\",\");\n const expected = await hmacHex(secret, message);\n return { valid: timingSafeEqual(expected, String(resp.DV).toLowerCase()) };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/fonepay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fonepay (Nepal) merchant redirect / Request-To-Pay over Web Crypto โ HMAC-SHA512 sign the request DV and verify the response DV. Zero-dep, isomorphic (Node, edge, browser).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"fonepay",
|
|
31
|
+
"nepal",
|
|
32
|
+
"payment",
|
|
33
|
+
"payment-gateway",
|
|
34
|
+
"request-to-pay",
|
|
35
|
+
"hmac",
|
|
36
|
+
"hmac-sha512",
|
|
37
|
+
"web-crypto",
|
|
38
|
+
"isomorphic",
|
|
39
|
+
"edge",
|
|
40
|
+
"typescript"
|
|
41
|
+
],
|
|
42
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
43
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
44
|
+
"homepage": "https://developer.lacspace.com/packages/fonepay",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
48
|
+
"directory": "fonepay"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|