@lacspace/connectips 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 +114 -0
- package/dist/index.cjs +139 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -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,114 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/connectips
|
|
4
|
+
|
|
5
|
+
**Connect IPS (Nepal) merchant integration over Web Crypto — RSA-signed redirect tokens + server-to-server transaction validation.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/connectips)
|
|
8
|
+
[](https://packagephobia.com/result?p=@lacspace/connectips)
|
|
9
|
+
[](https://bundlephobia.com/package/@lacspace/connectips)
|
|
10
|
+
[](https://www.npmjs.com/package/@lacspace/connectips)
|
|
11
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> [Connect IPS](https://www.connectips.com/) is Nepal's inter-bank payment gateway (operated by NCHL). Integrating it means building an exact `KEY=VALUE,…,TOKEN=TOKEN` message, signing it with your merchant **RSA private key** (RSA-SHA256), redirecting the payer, and later validating the transaction server-to-server. This package does all of that correctly.
|
|
16
|
+
|
|
17
|
+
- 🔐 **RSA-SHA256** (RSASSA-PKCS1-v1_5) signing straight from a **PKCS#8 PEM** private key
|
|
18
|
+
- 🧾 Canonical token message built for you — no fragile string concatenation
|
|
19
|
+
- 🔁 `buildForm()` for the redirect and `validateTxn()` for the callback check
|
|
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/connectips # or pnpm add / yarn add / bun add
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Redirect the payer
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { buildForm } from "@lacspace/connectips";
|
|
33
|
+
|
|
34
|
+
const form = await buildForm(
|
|
35
|
+
{
|
|
36
|
+
MERCHANTID: "123",
|
|
37
|
+
APPID: "APP123",
|
|
38
|
+
APPNAME: "lacspace-shop",
|
|
39
|
+
TXNID: "TXN001",
|
|
40
|
+
TXNDATE: "05-09-2026",
|
|
41
|
+
TXNCRNCY: "NPR",
|
|
42
|
+
TXNAMT: 100000, // paisa
|
|
43
|
+
REFERENCEID: "REF001",
|
|
44
|
+
REMARKS: "order-1",
|
|
45
|
+
PARTICULARS: "order-1",
|
|
46
|
+
},
|
|
47
|
+
{ privateKeyPem: process.env.CONNECTIPS_PRIVATE_KEY_PEM!, env: "prod" },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Render an auto-submitting form:
|
|
51
|
+
// <form action={form.action} method="POST">
|
|
52
|
+
// {Object.entries(form.fields).map(([k, v]) => <input type="hidden" name={k} value={v} />)}
|
|
53
|
+
// </form>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`form.fields` is your transaction params plus a base64 `TOKEN` (the RSA-SHA256 signature over the canonical message ending in the literal `TOKEN=TOKEN`).
|
|
57
|
+
|
|
58
|
+
## Validate the transaction (server-to-server)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { validateTxn } from "@lacspace/connectips";
|
|
62
|
+
|
|
63
|
+
const result = await validateTxn(
|
|
64
|
+
{ merchantId: "123", appId: "APP123", referenceId: "REF001", txnAmt: 100000 },
|
|
65
|
+
{
|
|
66
|
+
user: process.env.CONNECTIPS_API_USER!,
|
|
67
|
+
password: process.env.CONNECTIPS_API_PASSWORD!,
|
|
68
|
+
privateKeyPem: process.env.CONNECTIPS_PRIVATE_KEY_PEM!,
|
|
69
|
+
env: "prod",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
// POSTs { merchantId, appId, referenceId, txnAmt, token } with a Basic-auth header
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The validation `token` signs `MERCHANTID=…,APPID=…,REFERENCEID=…,TXNAMT=…`.
|
|
76
|
+
|
|
77
|
+
## Just the token
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { signToken } from "@lacspace/connectips";
|
|
81
|
+
|
|
82
|
+
const token = await signToken(params, privateKeyPem); // base64 RSA-SHA256 signature
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
| Export | Description |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `signToken(params, privateKeyPem)` | Base64 RSA-SHA256 signature of the canonical redirect message. |
|
|
90
|
+
| `buildForm(params, { privateKeyPem, env? })` | `{ action, method: "POST", fields }` — params + signed `TOKEN`. |
|
|
91
|
+
| `validateTxn(params, { user, password, privateKeyPem, env?, fetch? })` | POSTs the validation payload with Basic auth; returns the parsed response. |
|
|
92
|
+
| `verifyToken(message, signatureB64, publicKeyPem)` | Verify an RSA-SHA256 signature against an SPKI PEM public key. |
|
|
93
|
+
| `LOGIN_URL`, `VALIDATE_URL` | `{ test, prod }` endpoint maps. |
|
|
94
|
+
|
|
95
|
+
`env` defaults to `"test"` (UAT). All signing uses your PKCS#8 PEM private key; all HTTP goes through the global `fetch` (injectable for tests).
|
|
96
|
+
|
|
97
|
+
## Security notes
|
|
98
|
+
|
|
99
|
+
- Keep your **RSA private key** on the server only — never ship it to the browser.
|
|
100
|
+
- The signed message must match Connect IPS's expected field order exactly; this package builds it for you.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The Lacspace Developer Platform
|
|
105
|
+
|
|
106
|
+
`@lacspace/connectips` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
107
|
+
|
|
108
|
+
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
109
|
+
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
110
|
+
- 🧪 **Live playground** — https://developer.lacspace.com/playground
|
|
111
|
+
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
112
|
+
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
113
|
+
|
|
114
|
+
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,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var LOGIN_URL = {
|
|
5
|
+
test: "https://uat.connectips.com/connectipswebgw/loginpage",
|
|
6
|
+
prod: "https://login.connectips.com/connectipswebgw/loginpage"
|
|
7
|
+
};
|
|
8
|
+
var VALIDATE_URL = {
|
|
9
|
+
test: "https://uat.connectips.com/connectipswebws/api/creditor/validatetxn",
|
|
10
|
+
prod: "https://login.connectips.com/connectipswebws/api/creditor/validatetxn"
|
|
11
|
+
};
|
|
12
|
+
var enc = new TextEncoder();
|
|
13
|
+
function bytesToBase64(bytes) {
|
|
14
|
+
let bin = "";
|
|
15
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
16
|
+
return btoa(bin);
|
|
17
|
+
}
|
|
18
|
+
function base64ToBytes(b64) {
|
|
19
|
+
const bin = atob(b64);
|
|
20
|
+
const out = new Uint8Array(bin.length);
|
|
21
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function pemToBytes(pem, label) {
|
|
25
|
+
const body = pem.replace(`-----BEGIN ${label}-----`, "").replace(`-----END ${label}-----`, "").replace(/\s+/g, "");
|
|
26
|
+
return base64ToBytes(body);
|
|
27
|
+
}
|
|
28
|
+
async function importPrivateKey(pem) {
|
|
29
|
+
const der = pemToBytes(pem, "PRIVATE KEY");
|
|
30
|
+
return crypto.subtle.importKey(
|
|
31
|
+
"pkcs8",
|
|
32
|
+
der,
|
|
33
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
34
|
+
false,
|
|
35
|
+
["sign"]
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
async function importPublicKey(pem) {
|
|
39
|
+
const der = pemToBytes(pem, "PUBLIC KEY");
|
|
40
|
+
return crypto.subtle.importKey(
|
|
41
|
+
"spki",
|
|
42
|
+
der,
|
|
43
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
44
|
+
false,
|
|
45
|
+
["verify"]
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
async function signMessage(message, privateKeyPem) {
|
|
49
|
+
const key = await importPrivateKey(privateKeyPem);
|
|
50
|
+
const sig = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, enc.encode(message));
|
|
51
|
+
return bytesToBase64(new Uint8Array(sig));
|
|
52
|
+
}
|
|
53
|
+
function tokenMessage(p) {
|
|
54
|
+
return [
|
|
55
|
+
`MERCHANTID=${p.MERCHANTID}`,
|
|
56
|
+
`APPID=${p.APPID}`,
|
|
57
|
+
`APPNAME=${p.APPNAME}`,
|
|
58
|
+
`TXNID=${p.TXNID}`,
|
|
59
|
+
`TXNDATE=${p.TXNDATE}`,
|
|
60
|
+
`TXNCRNCY=${p.TXNCRNCY}`,
|
|
61
|
+
`TXNAMT=${p.TXNAMT}`,
|
|
62
|
+
`REFERENCEID=${p.REFERENCEID}`,
|
|
63
|
+
`REMARKS=${p.REMARKS}`,
|
|
64
|
+
`PARTICULARS=${p.PARTICULARS}`,
|
|
65
|
+
`TOKEN=TOKEN`
|
|
66
|
+
].join(",");
|
|
67
|
+
}
|
|
68
|
+
async function signToken(params, privateKeyPem) {
|
|
69
|
+
return signMessage(tokenMessage(params), privateKeyPem);
|
|
70
|
+
}
|
|
71
|
+
async function buildForm(params, opts) {
|
|
72
|
+
const token = await signToken(params, opts.privateKeyPem);
|
|
73
|
+
const fields = {
|
|
74
|
+
MERCHANTID: String(params.MERCHANTID),
|
|
75
|
+
APPID: String(params.APPID),
|
|
76
|
+
APPNAME: String(params.APPNAME),
|
|
77
|
+
TXNID: String(params.TXNID),
|
|
78
|
+
TXNDATE: String(params.TXNDATE),
|
|
79
|
+
TXNCRNCY: String(params.TXNCRNCY),
|
|
80
|
+
TXNAMT: String(params.TXNAMT),
|
|
81
|
+
REFERENCEID: String(params.REFERENCEID),
|
|
82
|
+
REMARKS: String(params.REMARKS),
|
|
83
|
+
PARTICULARS: String(params.PARTICULARS),
|
|
84
|
+
TOKEN: token
|
|
85
|
+
};
|
|
86
|
+
return { action: LOGIN_URL[opts.env ?? "test"], method: "POST", fields };
|
|
87
|
+
}
|
|
88
|
+
function validateMessage(p) {
|
|
89
|
+
return [
|
|
90
|
+
`MERCHANTID=${p.merchantId}`,
|
|
91
|
+
`APPID=${p.appId}`,
|
|
92
|
+
`REFERENCEID=${p.referenceId}`,
|
|
93
|
+
`TXNAMT=${p.txnAmt}`
|
|
94
|
+
].join(",");
|
|
95
|
+
}
|
|
96
|
+
async function validateTxn(params, opts) {
|
|
97
|
+
const token = await signMessage(validateMessage(params), opts.privateKeyPem);
|
|
98
|
+
const doFetch = opts.fetch ?? fetch;
|
|
99
|
+
const basic = bytesToBase64(enc.encode(`${opts.user}:${opts.password}`));
|
|
100
|
+
const res = await doFetch(VALIDATE_URL[opts.env ?? "test"], {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
Authorization: `Basic ${basic}`
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
merchantId: params.merchantId,
|
|
108
|
+
appId: params.appId,
|
|
109
|
+
referenceId: params.referenceId,
|
|
110
|
+
txnAmt: params.txnAmt,
|
|
111
|
+
token
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function verifyToken(message, signatureB64, publicKeyPem) {
|
|
122
|
+
const key = await importPublicKey(publicKeyPem);
|
|
123
|
+
const sig = base64ToBytes(signatureB64);
|
|
124
|
+
return crypto.subtle.verify(
|
|
125
|
+
"RSASSA-PKCS1-v1_5",
|
|
126
|
+
key,
|
|
127
|
+
sig,
|
|
128
|
+
enc.encode(message)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
exports.LOGIN_URL = LOGIN_URL;
|
|
133
|
+
exports.VALIDATE_URL = VALIDATE_URL;
|
|
134
|
+
exports.buildForm = buildForm;
|
|
135
|
+
exports.signToken = signToken;
|
|
136
|
+
exports.validateTxn = validateTxn;
|
|
137
|
+
exports.verifyToken = verifyToken;
|
|
138
|
+
//# sourceMappingURL=index.cjs.map
|
|
139
|
+
//# 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/connectips\n *\n * Connect IPS (Nepal) merchant integration over Web Crypto. Connect IPS is the\n * inter-bank payment gateway operated by Nepal Clearing House (NCHL): the\n * merchant redirects the payer to the Connect IPS login page with a\n * digitally-signed transaction token, and later validates the completed\n * transaction server-to-server.\n *\n * This package handles the awkward parts correctly:\n *\n * 1. Building the canonical `KEY=VALUE,…,TOKEN=TOKEN` message string.\n * 2. Signing it with the merchant's RSA private key using RSA-SHA256\n * (RSASSA-PKCS1-v1_5) — imported straight from a PKCS#8 PEM.\n * 3. Emitting the redirect form and calling the validate-txn API.\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/** Connect IPS gateway login (redirect) pages, by environment. */\nexport const LOGIN_URL: Record<Env, string> = {\n test: \"https://uat.connectips.com/connectipswebgw/loginpage\",\n prod: \"https://login.connectips.com/connectipswebgw/loginpage\",\n};\n\n/** Connect IPS transaction-validation API endpoints, by environment. */\nexport const VALIDATE_URL: Record<Env, string> = {\n test: \"https://uat.connectips.com/connectipswebws/api/creditor/validatetxn\",\n prod: \"https://login.connectips.com/connectipswebws/api/creditor/validatetxn\",\n};\n\n/* ------------------------------------------------------------------ *\n * Base64 + PEM helpers (isomorphic, zero-dep)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let bin = \"\";\n for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);\n return btoa(bin);\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Strip the PEM armour + whitespace and base64-decode the DER body. */\nfunction pemToBytes(pem: string, label: string): Uint8Array {\n const body = pem\n .replace(`-----BEGIN ${label}-----`, \"\")\n .replace(`-----END ${label}-----`, \"\")\n .replace(/\\s+/g, \"\");\n return base64ToBytes(body);\n}\n\n/** Import a PKCS#8 PEM RSA private key for RSASSA-PKCS1-v1_5 / SHA-256 signing. */\nasync function importPrivateKey(pem: string): Promise<CryptoKey> {\n const der = pemToBytes(pem, \"PRIVATE KEY\");\n return crypto.subtle.importKey(\n \"pkcs8\",\n der as unknown as ArrayBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n}\n\n/** Import an SPKI PEM RSA public key for RSASSA-PKCS1-v1_5 / SHA-256 verification. */\nasync function importPublicKey(pem: string): Promise<CryptoKey> {\n const der = pemToBytes(pem, \"PUBLIC KEY\");\n return crypto.subtle.importKey(\n \"spki\",\n der as unknown as ArrayBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"verify\"],\n );\n}\n\n/** Sign a message with the merchant RSA private key → base64 signature. */\nasync function signMessage(message: string, privateKeyPem: string): Promise<string> {\n const key = await importPrivateKey(privateKeyPem);\n const sig = await crypto.subtle.sign(\"RSASSA-PKCS1-v1_5\", key, enc.encode(message));\n return bytesToBase64(new Uint8Array(sig));\n}\n\n/* ------------------------------------------------------------------ *\n * Token signing\n * ------------------------------------------------------------------ */\n\n/** The transaction parameters that go into the redirect token, in order. */\nexport interface TokenParams {\n MERCHANTID: string;\n APPID: string;\n APPNAME: string;\n TXNID: string;\n TXNDATE: string;\n TXNCRNCY: string;\n TXNAMT: string | number;\n REFERENCEID: string;\n REMARKS: string;\n PARTICULARS: string;\n}\n\n/** Build the canonical token message: `KEY=VALUE,…,TOKEN=TOKEN`. */\nfunction tokenMessage(p: TokenParams): string {\n return [\n `MERCHANTID=${p.MERCHANTID}`,\n `APPID=${p.APPID}`,\n `APPNAME=${p.APPNAME}`,\n `TXNID=${p.TXNID}`,\n `TXNDATE=${p.TXNDATE}`,\n `TXNCRNCY=${p.TXNCRNCY}`,\n `TXNAMT=${p.TXNAMT}`,\n `REFERENCEID=${p.REFERENCEID}`,\n `REMARKS=${p.REMARKS}`,\n `PARTICULARS=${p.PARTICULARS}`,\n `TOKEN=TOKEN`,\n ].join(\",\");\n}\n\n/**\n * Sign a Connect IPS transaction token with the merchant RSA private key.\n * Builds the canonical `KEY=VALUE,…,TOKEN=TOKEN` message, signs it with\n * RSA-SHA256, and returns the base64 signature to send as the `TOKEN` field.\n *\n * @example\n * const token = await signToken(\n * { MERCHANTID: \"123\", APPID: \"APP\", APPNAME: \"shop\", TXNID: \"T1\",\n * TXNDATE: \"05-09-2026\", TXNCRNCY: \"NPR\", TXNAMT: 1000,\n * REFERENCEID: \"R1\", REMARKS: \"order\", PARTICULARS: \"order\" },\n * process.env.CONNECTIPS_PRIVATE_KEY_PEM!,\n * );\n */\nexport async function signToken(params: TokenParams, privateKeyPem: string): Promise<string> {\n return signMessage(tokenMessage(params), privateKeyPem);\n}\n\n/* ------------------------------------------------------------------ *\n * Redirect form\n * ------------------------------------------------------------------ */\n\nexport interface BuildFormOptions {\n privateKeyPem: string;\n /** Which gateway to target. Default \"test\" (UAT). */\n env?: Env;\n}\n\nexport interface RedirectForm {\n /** The Connect IPS login page URL to POST to. */\n action: string;\n method: \"POST\";\n /** The transaction params plus the signed `TOKEN`. */\n fields: Record<string, string>;\n}\n\n/**\n * Build the auto-submitting redirect form for Connect IPS. Returns the gateway\n * `action` URL and the `fields` (the transaction params plus the signed\n * `TOKEN`) to render as hidden inputs and POST to the login page.\n *\n * @example\n * const form = await buildForm(params, { privateKeyPem, env: \"prod\" });\n * // render <form action={form.action} method=\"POST\"> … {form.fields} … </form>\n */\nexport async function buildForm(params: TokenParams, opts: BuildFormOptions): Promise<RedirectForm> {\n const token = await signToken(params, opts.privateKeyPem);\n const fields: Record<string, string> = {\n MERCHANTID: String(params.MERCHANTID),\n APPID: String(params.APPID),\n APPNAME: String(params.APPNAME),\n TXNID: String(params.TXNID),\n TXNDATE: String(params.TXNDATE),\n TXNCRNCY: String(params.TXNCRNCY),\n TXNAMT: String(params.TXNAMT),\n REFERENCEID: String(params.REFERENCEID),\n REMARKS: String(params.REMARKS),\n PARTICULARS: String(params.PARTICULARS),\n TOKEN: token,\n };\n return { action: LOGIN_URL[opts.env ?? \"test\"], method: \"POST\", fields };\n}\n\n/* ------------------------------------------------------------------ *\n * Transaction validation (server-to-server)\n * ------------------------------------------------------------------ */\n\nexport interface ValidateParams {\n merchantId: string;\n appId: string;\n referenceId: string;\n txnAmt: string | number;\n}\n\nexport interface ValidateOptions {\n /** Connect IPS creditor API username. */\n user: string;\n /** Connect IPS creditor API password. */\n password: string;\n /** Merchant RSA private key PEM (PKCS#8) used to sign the validation token. */\n privateKeyPem: string;\n /** Which gateway to target. Default \"test\" (UAT). */\n env?: Env;\n /** Inject a `fetch` implementation (defaults to the global `fetch`). */\n fetch?: typeof fetch;\n}\n\n/** Canonical validation message: `MERCHANTID=…,APPID=…,REFERENCEID=…,TXNAMT=…`. */\nfunction validateMessage(p: ValidateParams): string {\n return [\n `MERCHANTID=${p.merchantId}`,\n `APPID=${p.appId}`,\n `REFERENCEID=${p.referenceId}`,\n `TXNAMT=${p.txnAmt}`,\n ].join(\",\");\n}\n\n/**\n * Validate a completed Connect IPS transaction, server-to-server. Signs the\n * canonical validation message with the merchant RSA private key and POSTs the\n * JSON payload (with a Basic-auth header) to the validate-txn API.\n *\n * @example\n * const result = await validateTxn(\n * { merchantId: \"123\", appId: \"APP\", referenceId: \"R1\", txnAmt: 1000 },\n * { user, password, privateKeyPem, env: \"prod\" },\n * );\n */\nexport async function validateTxn(params: ValidateParams, opts: ValidateOptions): Promise<unknown> {\n const token = await signMessage(validateMessage(params), opts.privateKeyPem);\n const doFetch = opts.fetch ?? fetch;\n const basic = bytesToBase64(enc.encode(`${opts.user}:${opts.password}`));\n const res = await doFetch(VALIDATE_URL[opts.env ?? \"test\"], {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Basic ${basic}`,\n },\n body: JSON.stringify({\n merchantId: params.merchantId,\n appId: params.appId,\n referenceId: params.referenceId,\n txnAmt: params.txnAmt,\n token,\n }),\n });\n const text = await res.text();\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Verification helper\n * ------------------------------------------------------------------ */\n\n/**\n * Verify an RSA-SHA256 signature (base64) against a message using an SPKI PEM\n * public key. Useful for tests and for verifying Connect IPS callbacks.\n */\nexport async function verifyToken(\n message: string,\n signatureB64: string,\n publicKeyPem: string,\n): Promise<boolean> {\n const key = await importPublicKey(publicKeyPem);\n const sig = base64ToBytes(signatureB64);\n return crypto.subtle.verify(\n \"RSASSA-PKCS1-v1_5\",\n key,\n sig as unknown as ArrayBuffer,\n enc.encode(message),\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/connectips
|
|
3
|
+
*
|
|
4
|
+
* Connect IPS (Nepal) merchant integration over Web Crypto. Connect IPS is the
|
|
5
|
+
* inter-bank payment gateway operated by Nepal Clearing House (NCHL): the
|
|
6
|
+
* merchant redirects the payer to the Connect IPS login page with a
|
|
7
|
+
* digitally-signed transaction token, and later validates the completed
|
|
8
|
+
* transaction server-to-server.
|
|
9
|
+
*
|
|
10
|
+
* This package handles the awkward parts correctly:
|
|
11
|
+
*
|
|
12
|
+
* 1. Building the canonical `KEY=VALUE,…,TOKEN=TOKEN` message string.
|
|
13
|
+
* 2. Signing it with the merchant's RSA private key using RSA-SHA256
|
|
14
|
+
* (RSASSA-PKCS1-v1_5) — imported straight from a PKCS#8 PEM.
|
|
15
|
+
* 3. Emitting the redirect form and calling the validate-txn API.
|
|
16
|
+
*
|
|
17
|
+
* Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers — all
|
|
18
|
+
* cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.
|
|
19
|
+
*/
|
|
20
|
+
type Env = "test" | "prod";
|
|
21
|
+
/** Connect IPS gateway login (redirect) pages, by environment. */
|
|
22
|
+
declare const LOGIN_URL: Record<Env, string>;
|
|
23
|
+
/** Connect IPS transaction-validation API endpoints, by environment. */
|
|
24
|
+
declare const VALIDATE_URL: Record<Env, string>;
|
|
25
|
+
/** The transaction parameters that go into the redirect token, in order. */
|
|
26
|
+
interface TokenParams {
|
|
27
|
+
MERCHANTID: string;
|
|
28
|
+
APPID: string;
|
|
29
|
+
APPNAME: string;
|
|
30
|
+
TXNID: string;
|
|
31
|
+
TXNDATE: string;
|
|
32
|
+
TXNCRNCY: string;
|
|
33
|
+
TXNAMT: string | number;
|
|
34
|
+
REFERENCEID: string;
|
|
35
|
+
REMARKS: string;
|
|
36
|
+
PARTICULARS: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sign a Connect IPS transaction token with the merchant RSA private key.
|
|
40
|
+
* Builds the canonical `KEY=VALUE,…,TOKEN=TOKEN` message, signs it with
|
|
41
|
+
* RSA-SHA256, and returns the base64 signature to send as the `TOKEN` field.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const token = await signToken(
|
|
45
|
+
* { MERCHANTID: "123", APPID: "APP", APPNAME: "shop", TXNID: "T1",
|
|
46
|
+
* TXNDATE: "05-09-2026", TXNCRNCY: "NPR", TXNAMT: 1000,
|
|
47
|
+
* REFERENCEID: "R1", REMARKS: "order", PARTICULARS: "order" },
|
|
48
|
+
* process.env.CONNECTIPS_PRIVATE_KEY_PEM!,
|
|
49
|
+
* );
|
|
50
|
+
*/
|
|
51
|
+
declare function signToken(params: TokenParams, privateKeyPem: string): Promise<string>;
|
|
52
|
+
interface BuildFormOptions {
|
|
53
|
+
privateKeyPem: string;
|
|
54
|
+
/** Which gateway to target. Default "test" (UAT). */
|
|
55
|
+
env?: Env;
|
|
56
|
+
}
|
|
57
|
+
interface RedirectForm {
|
|
58
|
+
/** The Connect IPS login page URL to POST to. */
|
|
59
|
+
action: string;
|
|
60
|
+
method: "POST";
|
|
61
|
+
/** The transaction params plus the signed `TOKEN`. */
|
|
62
|
+
fields: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the auto-submitting redirect form for Connect IPS. Returns the gateway
|
|
66
|
+
* `action` URL and the `fields` (the transaction params plus the signed
|
|
67
|
+
* `TOKEN`) to render as hidden inputs and POST to the login page.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const form = await buildForm(params, { privateKeyPem, env: "prod" });
|
|
71
|
+
* // render <form action={form.action} method="POST"> … {form.fields} … </form>
|
|
72
|
+
*/
|
|
73
|
+
declare function buildForm(params: TokenParams, opts: BuildFormOptions): Promise<RedirectForm>;
|
|
74
|
+
interface ValidateParams {
|
|
75
|
+
merchantId: string;
|
|
76
|
+
appId: string;
|
|
77
|
+
referenceId: string;
|
|
78
|
+
txnAmt: string | number;
|
|
79
|
+
}
|
|
80
|
+
interface ValidateOptions {
|
|
81
|
+
/** Connect IPS creditor API username. */
|
|
82
|
+
user: string;
|
|
83
|
+
/** Connect IPS creditor API password. */
|
|
84
|
+
password: string;
|
|
85
|
+
/** Merchant RSA private key PEM (PKCS#8) used to sign the validation token. */
|
|
86
|
+
privateKeyPem: string;
|
|
87
|
+
/** Which gateway to target. Default "test" (UAT). */
|
|
88
|
+
env?: Env;
|
|
89
|
+
/** Inject a `fetch` implementation (defaults to the global `fetch`). */
|
|
90
|
+
fetch?: typeof fetch;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate a completed Connect IPS transaction, server-to-server. Signs the
|
|
94
|
+
* canonical validation message with the merchant RSA private key and POSTs the
|
|
95
|
+
* JSON payload (with a Basic-auth header) to the validate-txn API.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* const result = await validateTxn(
|
|
99
|
+
* { merchantId: "123", appId: "APP", referenceId: "R1", txnAmt: 1000 },
|
|
100
|
+
* { user, password, privateKeyPem, env: "prod" },
|
|
101
|
+
* );
|
|
102
|
+
*/
|
|
103
|
+
declare function validateTxn(params: ValidateParams, opts: ValidateOptions): Promise<unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Verify an RSA-SHA256 signature (base64) against a message using an SPKI PEM
|
|
106
|
+
* public key. Useful for tests and for verifying Connect IPS callbacks.
|
|
107
|
+
*/
|
|
108
|
+
declare function verifyToken(message: string, signatureB64: string, publicKeyPem: string): Promise<boolean>;
|
|
109
|
+
|
|
110
|
+
export { type BuildFormOptions, type Env, LOGIN_URL, type RedirectForm, type TokenParams, VALIDATE_URL, type ValidateOptions, type ValidateParams, buildForm, signToken, validateTxn, verifyToken };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/connectips
|
|
3
|
+
*
|
|
4
|
+
* Connect IPS (Nepal) merchant integration over Web Crypto. Connect IPS is the
|
|
5
|
+
* inter-bank payment gateway operated by Nepal Clearing House (NCHL): the
|
|
6
|
+
* merchant redirects the payer to the Connect IPS login page with a
|
|
7
|
+
* digitally-signed transaction token, and later validates the completed
|
|
8
|
+
* transaction server-to-server.
|
|
9
|
+
*
|
|
10
|
+
* This package handles the awkward parts correctly:
|
|
11
|
+
*
|
|
12
|
+
* 1. Building the canonical `KEY=VALUE,…,TOKEN=TOKEN` message string.
|
|
13
|
+
* 2. Signing it with the merchant's RSA private key using RSA-SHA256
|
|
14
|
+
* (RSASSA-PKCS1-v1_5) — imported straight from a PKCS#8 PEM.
|
|
15
|
+
* 3. Emitting the redirect form and calling the validate-txn API.
|
|
16
|
+
*
|
|
17
|
+
* Zero dependencies. Isomorphic: Node 20+, edge runtimes and browsers — all
|
|
18
|
+
* cryptography goes through `globalThis.crypto.subtle`, never hand-rolled.
|
|
19
|
+
*/
|
|
20
|
+
type Env = "test" | "prod";
|
|
21
|
+
/** Connect IPS gateway login (redirect) pages, by environment. */
|
|
22
|
+
declare const LOGIN_URL: Record<Env, string>;
|
|
23
|
+
/** Connect IPS transaction-validation API endpoints, by environment. */
|
|
24
|
+
declare const VALIDATE_URL: Record<Env, string>;
|
|
25
|
+
/** The transaction parameters that go into the redirect token, in order. */
|
|
26
|
+
interface TokenParams {
|
|
27
|
+
MERCHANTID: string;
|
|
28
|
+
APPID: string;
|
|
29
|
+
APPNAME: string;
|
|
30
|
+
TXNID: string;
|
|
31
|
+
TXNDATE: string;
|
|
32
|
+
TXNCRNCY: string;
|
|
33
|
+
TXNAMT: string | number;
|
|
34
|
+
REFERENCEID: string;
|
|
35
|
+
REMARKS: string;
|
|
36
|
+
PARTICULARS: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sign a Connect IPS transaction token with the merchant RSA private key.
|
|
40
|
+
* Builds the canonical `KEY=VALUE,…,TOKEN=TOKEN` message, signs it with
|
|
41
|
+
* RSA-SHA256, and returns the base64 signature to send as the `TOKEN` field.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const token = await signToken(
|
|
45
|
+
* { MERCHANTID: "123", APPID: "APP", APPNAME: "shop", TXNID: "T1",
|
|
46
|
+
* TXNDATE: "05-09-2026", TXNCRNCY: "NPR", TXNAMT: 1000,
|
|
47
|
+
* REFERENCEID: "R1", REMARKS: "order", PARTICULARS: "order" },
|
|
48
|
+
* process.env.CONNECTIPS_PRIVATE_KEY_PEM!,
|
|
49
|
+
* );
|
|
50
|
+
*/
|
|
51
|
+
declare function signToken(params: TokenParams, privateKeyPem: string): Promise<string>;
|
|
52
|
+
interface BuildFormOptions {
|
|
53
|
+
privateKeyPem: string;
|
|
54
|
+
/** Which gateway to target. Default "test" (UAT). */
|
|
55
|
+
env?: Env;
|
|
56
|
+
}
|
|
57
|
+
interface RedirectForm {
|
|
58
|
+
/** The Connect IPS login page URL to POST to. */
|
|
59
|
+
action: string;
|
|
60
|
+
method: "POST";
|
|
61
|
+
/** The transaction params plus the signed `TOKEN`. */
|
|
62
|
+
fields: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the auto-submitting redirect form for Connect IPS. Returns the gateway
|
|
66
|
+
* `action` URL and the `fields` (the transaction params plus the signed
|
|
67
|
+
* `TOKEN`) to render as hidden inputs and POST to the login page.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const form = await buildForm(params, { privateKeyPem, env: "prod" });
|
|
71
|
+
* // render <form action={form.action} method="POST"> … {form.fields} … </form>
|
|
72
|
+
*/
|
|
73
|
+
declare function buildForm(params: TokenParams, opts: BuildFormOptions): Promise<RedirectForm>;
|
|
74
|
+
interface ValidateParams {
|
|
75
|
+
merchantId: string;
|
|
76
|
+
appId: string;
|
|
77
|
+
referenceId: string;
|
|
78
|
+
txnAmt: string | number;
|
|
79
|
+
}
|
|
80
|
+
interface ValidateOptions {
|
|
81
|
+
/** Connect IPS creditor API username. */
|
|
82
|
+
user: string;
|
|
83
|
+
/** Connect IPS creditor API password. */
|
|
84
|
+
password: string;
|
|
85
|
+
/** Merchant RSA private key PEM (PKCS#8) used to sign the validation token. */
|
|
86
|
+
privateKeyPem: string;
|
|
87
|
+
/** Which gateway to target. Default "test" (UAT). */
|
|
88
|
+
env?: Env;
|
|
89
|
+
/** Inject a `fetch` implementation (defaults to the global `fetch`). */
|
|
90
|
+
fetch?: typeof fetch;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate a completed Connect IPS transaction, server-to-server. Signs the
|
|
94
|
+
* canonical validation message with the merchant RSA private key and POSTs the
|
|
95
|
+
* JSON payload (with a Basic-auth header) to the validate-txn API.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* const result = await validateTxn(
|
|
99
|
+
* { merchantId: "123", appId: "APP", referenceId: "R1", txnAmt: 1000 },
|
|
100
|
+
* { user, password, privateKeyPem, env: "prod" },
|
|
101
|
+
* );
|
|
102
|
+
*/
|
|
103
|
+
declare function validateTxn(params: ValidateParams, opts: ValidateOptions): Promise<unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Verify an RSA-SHA256 signature (base64) against a message using an SPKI PEM
|
|
106
|
+
* public key. Useful for tests and for verifying Connect IPS callbacks.
|
|
107
|
+
*/
|
|
108
|
+
declare function verifyToken(message: string, signatureB64: string, publicKeyPem: string): Promise<boolean>;
|
|
109
|
+
|
|
110
|
+
export { type BuildFormOptions, type Env, LOGIN_URL, type RedirectForm, type TokenParams, VALIDATE_URL, type ValidateOptions, type ValidateParams, buildForm, signToken, validateTxn, verifyToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var LOGIN_URL = {
|
|
3
|
+
test: "https://uat.connectips.com/connectipswebgw/loginpage",
|
|
4
|
+
prod: "https://login.connectips.com/connectipswebgw/loginpage"
|
|
5
|
+
};
|
|
6
|
+
var VALIDATE_URL = {
|
|
7
|
+
test: "https://uat.connectips.com/connectipswebws/api/creditor/validatetxn",
|
|
8
|
+
prod: "https://login.connectips.com/connectipswebws/api/creditor/validatetxn"
|
|
9
|
+
};
|
|
10
|
+
var enc = new TextEncoder();
|
|
11
|
+
function bytesToBase64(bytes) {
|
|
12
|
+
let bin = "";
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
14
|
+
return btoa(bin);
|
|
15
|
+
}
|
|
16
|
+
function base64ToBytes(b64) {
|
|
17
|
+
const bin = atob(b64);
|
|
18
|
+
const out = new Uint8Array(bin.length);
|
|
19
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function pemToBytes(pem, label) {
|
|
23
|
+
const body = pem.replace(`-----BEGIN ${label}-----`, "").replace(`-----END ${label}-----`, "").replace(/\s+/g, "");
|
|
24
|
+
return base64ToBytes(body);
|
|
25
|
+
}
|
|
26
|
+
async function importPrivateKey(pem) {
|
|
27
|
+
const der = pemToBytes(pem, "PRIVATE KEY");
|
|
28
|
+
return crypto.subtle.importKey(
|
|
29
|
+
"pkcs8",
|
|
30
|
+
der,
|
|
31
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
32
|
+
false,
|
|
33
|
+
["sign"]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
async function importPublicKey(pem) {
|
|
37
|
+
const der = pemToBytes(pem, "PUBLIC KEY");
|
|
38
|
+
return crypto.subtle.importKey(
|
|
39
|
+
"spki",
|
|
40
|
+
der,
|
|
41
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
42
|
+
false,
|
|
43
|
+
["verify"]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
async function signMessage(message, privateKeyPem) {
|
|
47
|
+
const key = await importPrivateKey(privateKeyPem);
|
|
48
|
+
const sig = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, enc.encode(message));
|
|
49
|
+
return bytesToBase64(new Uint8Array(sig));
|
|
50
|
+
}
|
|
51
|
+
function tokenMessage(p) {
|
|
52
|
+
return [
|
|
53
|
+
`MERCHANTID=${p.MERCHANTID}`,
|
|
54
|
+
`APPID=${p.APPID}`,
|
|
55
|
+
`APPNAME=${p.APPNAME}`,
|
|
56
|
+
`TXNID=${p.TXNID}`,
|
|
57
|
+
`TXNDATE=${p.TXNDATE}`,
|
|
58
|
+
`TXNCRNCY=${p.TXNCRNCY}`,
|
|
59
|
+
`TXNAMT=${p.TXNAMT}`,
|
|
60
|
+
`REFERENCEID=${p.REFERENCEID}`,
|
|
61
|
+
`REMARKS=${p.REMARKS}`,
|
|
62
|
+
`PARTICULARS=${p.PARTICULARS}`,
|
|
63
|
+
`TOKEN=TOKEN`
|
|
64
|
+
].join(",");
|
|
65
|
+
}
|
|
66
|
+
async function signToken(params, privateKeyPem) {
|
|
67
|
+
return signMessage(tokenMessage(params), privateKeyPem);
|
|
68
|
+
}
|
|
69
|
+
async function buildForm(params, opts) {
|
|
70
|
+
const token = await signToken(params, opts.privateKeyPem);
|
|
71
|
+
const fields = {
|
|
72
|
+
MERCHANTID: String(params.MERCHANTID),
|
|
73
|
+
APPID: String(params.APPID),
|
|
74
|
+
APPNAME: String(params.APPNAME),
|
|
75
|
+
TXNID: String(params.TXNID),
|
|
76
|
+
TXNDATE: String(params.TXNDATE),
|
|
77
|
+
TXNCRNCY: String(params.TXNCRNCY),
|
|
78
|
+
TXNAMT: String(params.TXNAMT),
|
|
79
|
+
REFERENCEID: String(params.REFERENCEID),
|
|
80
|
+
REMARKS: String(params.REMARKS),
|
|
81
|
+
PARTICULARS: String(params.PARTICULARS),
|
|
82
|
+
TOKEN: token
|
|
83
|
+
};
|
|
84
|
+
return { action: LOGIN_URL[opts.env ?? "test"], method: "POST", fields };
|
|
85
|
+
}
|
|
86
|
+
function validateMessage(p) {
|
|
87
|
+
return [
|
|
88
|
+
`MERCHANTID=${p.merchantId}`,
|
|
89
|
+
`APPID=${p.appId}`,
|
|
90
|
+
`REFERENCEID=${p.referenceId}`,
|
|
91
|
+
`TXNAMT=${p.txnAmt}`
|
|
92
|
+
].join(",");
|
|
93
|
+
}
|
|
94
|
+
async function validateTxn(params, opts) {
|
|
95
|
+
const token = await signMessage(validateMessage(params), opts.privateKeyPem);
|
|
96
|
+
const doFetch = opts.fetch ?? fetch;
|
|
97
|
+
const basic = bytesToBase64(enc.encode(`${opts.user}:${opts.password}`));
|
|
98
|
+
const res = await doFetch(VALIDATE_URL[opts.env ?? "test"], {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Authorization: `Basic ${basic}`
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
merchantId: params.merchantId,
|
|
106
|
+
appId: params.appId,
|
|
107
|
+
referenceId: params.referenceId,
|
|
108
|
+
txnAmt: params.txnAmt,
|
|
109
|
+
token
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
} catch {
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function verifyToken(message, signatureB64, publicKeyPem) {
|
|
120
|
+
const key = await importPublicKey(publicKeyPem);
|
|
121
|
+
const sig = base64ToBytes(signatureB64);
|
|
122
|
+
return crypto.subtle.verify(
|
|
123
|
+
"RSASSA-PKCS1-v1_5",
|
|
124
|
+
key,
|
|
125
|
+
sig,
|
|
126
|
+
enc.encode(message)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { LOGIN_URL, VALIDATE_URL, buildForm, signToken, validateTxn, verifyToken };
|
|
131
|
+
//# sourceMappingURL=index.js.map
|
|
132
|
+
//# 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/connectips\n *\n * Connect IPS (Nepal) merchant integration over Web Crypto. Connect IPS is the\n * inter-bank payment gateway operated by Nepal Clearing House (NCHL): the\n * merchant redirects the payer to the Connect IPS login page with a\n * digitally-signed transaction token, and later validates the completed\n * transaction server-to-server.\n *\n * This package handles the awkward parts correctly:\n *\n * 1. Building the canonical `KEY=VALUE,…,TOKEN=TOKEN` message string.\n * 2. Signing it with the merchant's RSA private key using RSA-SHA256\n * (RSASSA-PKCS1-v1_5) — imported straight from a PKCS#8 PEM.\n * 3. Emitting the redirect form and calling the validate-txn API.\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/** Connect IPS gateway login (redirect) pages, by environment. */\nexport const LOGIN_URL: Record<Env, string> = {\n test: \"https://uat.connectips.com/connectipswebgw/loginpage\",\n prod: \"https://login.connectips.com/connectipswebgw/loginpage\",\n};\n\n/** Connect IPS transaction-validation API endpoints, by environment. */\nexport const VALIDATE_URL: Record<Env, string> = {\n test: \"https://uat.connectips.com/connectipswebws/api/creditor/validatetxn\",\n prod: \"https://login.connectips.com/connectipswebws/api/creditor/validatetxn\",\n};\n\n/* ------------------------------------------------------------------ *\n * Base64 + PEM helpers (isomorphic, zero-dep)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let bin = \"\";\n for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);\n return btoa(bin);\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Strip the PEM armour + whitespace and base64-decode the DER body. */\nfunction pemToBytes(pem: string, label: string): Uint8Array {\n const body = pem\n .replace(`-----BEGIN ${label}-----`, \"\")\n .replace(`-----END ${label}-----`, \"\")\n .replace(/\\s+/g, \"\");\n return base64ToBytes(body);\n}\n\n/** Import a PKCS#8 PEM RSA private key for RSASSA-PKCS1-v1_5 / SHA-256 signing. */\nasync function importPrivateKey(pem: string): Promise<CryptoKey> {\n const der = pemToBytes(pem, \"PRIVATE KEY\");\n return crypto.subtle.importKey(\n \"pkcs8\",\n der as unknown as ArrayBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n}\n\n/** Import an SPKI PEM RSA public key for RSASSA-PKCS1-v1_5 / SHA-256 verification. */\nasync function importPublicKey(pem: string): Promise<CryptoKey> {\n const der = pemToBytes(pem, \"PUBLIC KEY\");\n return crypto.subtle.importKey(\n \"spki\",\n der as unknown as ArrayBuffer,\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"verify\"],\n );\n}\n\n/** Sign a message with the merchant RSA private key → base64 signature. */\nasync function signMessage(message: string, privateKeyPem: string): Promise<string> {\n const key = await importPrivateKey(privateKeyPem);\n const sig = await crypto.subtle.sign(\"RSASSA-PKCS1-v1_5\", key, enc.encode(message));\n return bytesToBase64(new Uint8Array(sig));\n}\n\n/* ------------------------------------------------------------------ *\n * Token signing\n * ------------------------------------------------------------------ */\n\n/** The transaction parameters that go into the redirect token, in order. */\nexport interface TokenParams {\n MERCHANTID: string;\n APPID: string;\n APPNAME: string;\n TXNID: string;\n TXNDATE: string;\n TXNCRNCY: string;\n TXNAMT: string | number;\n REFERENCEID: string;\n REMARKS: string;\n PARTICULARS: string;\n}\n\n/** Build the canonical token message: `KEY=VALUE,…,TOKEN=TOKEN`. */\nfunction tokenMessage(p: TokenParams): string {\n return [\n `MERCHANTID=${p.MERCHANTID}`,\n `APPID=${p.APPID}`,\n `APPNAME=${p.APPNAME}`,\n `TXNID=${p.TXNID}`,\n `TXNDATE=${p.TXNDATE}`,\n `TXNCRNCY=${p.TXNCRNCY}`,\n `TXNAMT=${p.TXNAMT}`,\n `REFERENCEID=${p.REFERENCEID}`,\n `REMARKS=${p.REMARKS}`,\n `PARTICULARS=${p.PARTICULARS}`,\n `TOKEN=TOKEN`,\n ].join(\",\");\n}\n\n/**\n * Sign a Connect IPS transaction token with the merchant RSA private key.\n * Builds the canonical `KEY=VALUE,…,TOKEN=TOKEN` message, signs it with\n * RSA-SHA256, and returns the base64 signature to send as the `TOKEN` field.\n *\n * @example\n * const token = await signToken(\n * { MERCHANTID: \"123\", APPID: \"APP\", APPNAME: \"shop\", TXNID: \"T1\",\n * TXNDATE: \"05-09-2026\", TXNCRNCY: \"NPR\", TXNAMT: 1000,\n * REFERENCEID: \"R1\", REMARKS: \"order\", PARTICULARS: \"order\" },\n * process.env.CONNECTIPS_PRIVATE_KEY_PEM!,\n * );\n */\nexport async function signToken(params: TokenParams, privateKeyPem: string): Promise<string> {\n return signMessage(tokenMessage(params), privateKeyPem);\n}\n\n/* ------------------------------------------------------------------ *\n * Redirect form\n * ------------------------------------------------------------------ */\n\nexport interface BuildFormOptions {\n privateKeyPem: string;\n /** Which gateway to target. Default \"test\" (UAT). */\n env?: Env;\n}\n\nexport interface RedirectForm {\n /** The Connect IPS login page URL to POST to. */\n action: string;\n method: \"POST\";\n /** The transaction params plus the signed `TOKEN`. */\n fields: Record<string, string>;\n}\n\n/**\n * Build the auto-submitting redirect form for Connect IPS. Returns the gateway\n * `action` URL and the `fields` (the transaction params plus the signed\n * `TOKEN`) to render as hidden inputs and POST to the login page.\n *\n * @example\n * const form = await buildForm(params, { privateKeyPem, env: \"prod\" });\n * // render <form action={form.action} method=\"POST\"> … {form.fields} … </form>\n */\nexport async function buildForm(params: TokenParams, opts: BuildFormOptions): Promise<RedirectForm> {\n const token = await signToken(params, opts.privateKeyPem);\n const fields: Record<string, string> = {\n MERCHANTID: String(params.MERCHANTID),\n APPID: String(params.APPID),\n APPNAME: String(params.APPNAME),\n TXNID: String(params.TXNID),\n TXNDATE: String(params.TXNDATE),\n TXNCRNCY: String(params.TXNCRNCY),\n TXNAMT: String(params.TXNAMT),\n REFERENCEID: String(params.REFERENCEID),\n REMARKS: String(params.REMARKS),\n PARTICULARS: String(params.PARTICULARS),\n TOKEN: token,\n };\n return { action: LOGIN_URL[opts.env ?? \"test\"], method: \"POST\", fields };\n}\n\n/* ------------------------------------------------------------------ *\n * Transaction validation (server-to-server)\n * ------------------------------------------------------------------ */\n\nexport interface ValidateParams {\n merchantId: string;\n appId: string;\n referenceId: string;\n txnAmt: string | number;\n}\n\nexport interface ValidateOptions {\n /** Connect IPS creditor API username. */\n user: string;\n /** Connect IPS creditor API password. */\n password: string;\n /** Merchant RSA private key PEM (PKCS#8) used to sign the validation token. */\n privateKeyPem: string;\n /** Which gateway to target. Default \"test\" (UAT). */\n env?: Env;\n /** Inject a `fetch` implementation (defaults to the global `fetch`). */\n fetch?: typeof fetch;\n}\n\n/** Canonical validation message: `MERCHANTID=…,APPID=…,REFERENCEID=…,TXNAMT=…`. */\nfunction validateMessage(p: ValidateParams): string {\n return [\n `MERCHANTID=${p.merchantId}`,\n `APPID=${p.appId}`,\n `REFERENCEID=${p.referenceId}`,\n `TXNAMT=${p.txnAmt}`,\n ].join(\",\");\n}\n\n/**\n * Validate a completed Connect IPS transaction, server-to-server. Signs the\n * canonical validation message with the merchant RSA private key and POSTs the\n * JSON payload (with a Basic-auth header) to the validate-txn API.\n *\n * @example\n * const result = await validateTxn(\n * { merchantId: \"123\", appId: \"APP\", referenceId: \"R1\", txnAmt: 1000 },\n * { user, password, privateKeyPem, env: \"prod\" },\n * );\n */\nexport async function validateTxn(params: ValidateParams, opts: ValidateOptions): Promise<unknown> {\n const token = await signMessage(validateMessage(params), opts.privateKeyPem);\n const doFetch = opts.fetch ?? fetch;\n const basic = bytesToBase64(enc.encode(`${opts.user}:${opts.password}`));\n const res = await doFetch(VALIDATE_URL[opts.env ?? \"test\"], {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Basic ${basic}`,\n },\n body: JSON.stringify({\n merchantId: params.merchantId,\n appId: params.appId,\n referenceId: params.referenceId,\n txnAmt: params.txnAmt,\n token,\n }),\n });\n const text = await res.text();\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Verification helper\n * ------------------------------------------------------------------ */\n\n/**\n * Verify an RSA-SHA256 signature (base64) against a message using an SPKI PEM\n * public key. Useful for tests and for verifying Connect IPS callbacks.\n */\nexport async function verifyToken(\n message: string,\n signatureB64: string,\n publicKeyPem: string,\n): Promise<boolean> {\n const key = await importPublicKey(publicKeyPem);\n const sig = base64ToBytes(signatureB64);\n return crypto.subtle.verify(\n \"RSASSA-PKCS1-v1_5\",\n key,\n sig as unknown as ArrayBuffer,\n enc.encode(message),\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/connectips",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Connect IPS (Nepal) merchant integration over Web Crypto — sign the redirect transaction token with your RSA private key and validate transactions server-to-server. 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
|
+
"connectips",
|
|
31
|
+
"connect-ips",
|
|
32
|
+
"nepal",
|
|
33
|
+
"payment",
|
|
34
|
+
"payment-gateway",
|
|
35
|
+
"nchl",
|
|
36
|
+
"rsa",
|
|
37
|
+
"rsa-sha256",
|
|
38
|
+
"web-crypto",
|
|
39
|
+
"isomorphic",
|
|
40
|
+
"edge",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
44
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
45
|
+
"homepage": "https://developer.lacspace.com/packages/connectips",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
49
|
+
"directory": "connectips"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|