konthaina-khqr 0.1.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 +21 -0
- package/README.md +94 -0
- package/dist/index.cjs +281 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +244 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# konthaina-khqr (npm)
|
|
2
|
+
|
|
3
|
+
KHQR / EMVCo merchant-presented QR payload generator for Bakong (Cambodia), inspired by `konthaina/khqr-php`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i konthaina-khqr
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick usage
|
|
12
|
+
|
|
13
|
+
### Individual (Tag 29)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { KHQRGenerator } from "konthaina-khqr";
|
|
17
|
+
|
|
18
|
+
const { qr, md5 } = new KHQRGenerator("individual")
|
|
19
|
+
.setBakongAccountId("john_smith@devb")
|
|
20
|
+
.setMerchantName("John Smith")
|
|
21
|
+
.setCurrency("USD")
|
|
22
|
+
.setAmount("1.00")
|
|
23
|
+
.setMerchantCity("Phnom Penh")
|
|
24
|
+
.generate();
|
|
25
|
+
|
|
26
|
+
console.log(qr);
|
|
27
|
+
console.log(md5);
|
|
28
|
+
console.log(KHQRGenerator.verify(qr)); // true/false
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Merchant (Tag 30)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { KHQRGenerator } from "konthaina-khqr";
|
|
35
|
+
|
|
36
|
+
const { qr } = new KHQRGenerator("merchant")
|
|
37
|
+
.setBakongAccountId("dev_merchant@devb")
|
|
38
|
+
.setMerchantId("YOUR_MERCHANT_ID")
|
|
39
|
+
.setAcquiringBank("Dev Bank")
|
|
40
|
+
.setMerchantName("Dev Store")
|
|
41
|
+
.setCurrency("KHR")
|
|
42
|
+
.setAmount("1000")
|
|
43
|
+
.generate();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
- `new KHQRGenerator("individual" | "merchant")`
|
|
49
|
+
- `.setStatic(boolean)` (static QR omits timestamp)
|
|
50
|
+
- `.setBakongAccountId(string)`
|
|
51
|
+
- `.setMerchantName(string)`
|
|
52
|
+
- `.setMerchantCity(string)`
|
|
53
|
+
- `.setCurrency("KHR" | "USD")`
|
|
54
|
+
- `.setAmount(number|string)`
|
|
55
|
+
- `.setBillNumber(string)`
|
|
56
|
+
- `.setMobileNumber(string)`
|
|
57
|
+
- `.setStoreLabel(string)`
|
|
58
|
+
- `.setTerminalLabel(string)`
|
|
59
|
+
- `.setPurposeOfTransaction(string)`
|
|
60
|
+
- `.setMerchantAlternateLanguagePreference(string)`
|
|
61
|
+
- `.setMerchantNameAlternateLanguage(string)`
|
|
62
|
+
- `.setMerchantCityAlternateLanguage(string)`
|
|
63
|
+
- merchant-only:
|
|
64
|
+
- `.setMerchantId(string)`
|
|
65
|
+
- `.setAcquiringBank(string)`
|
|
66
|
+
- individual optional:
|
|
67
|
+
- `.setAccountInformation(string)`
|
|
68
|
+
- `.setAcquiringBank(string)`
|
|
69
|
+
|
|
70
|
+
Helpers:
|
|
71
|
+
- `KHQRGenerator.verify(qr: string): boolean`
|
|
72
|
+
- `verifyCrc(qr: string): boolean`
|
|
73
|
+
- `decodeTlv(qr: string): Record<string,string>`
|
|
74
|
+
|
|
75
|
+
## Dev
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm i
|
|
79
|
+
npm run build
|
|
80
|
+
npm test
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Publish
|
|
84
|
+
|
|
85
|
+
1) Update `package.json` name/version.
|
|
86
|
+
2) Build:
|
|
87
|
+
```bash
|
|
88
|
+
npm run build
|
|
89
|
+
```
|
|
90
|
+
3) Publish:
|
|
91
|
+
```bash
|
|
92
|
+
npm login
|
|
93
|
+
npm publish --access public
|
|
94
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
KHQRGenerator: () => KHQRGenerator,
|
|
34
|
+
decodeTlv: () => decodeTlv,
|
|
35
|
+
verifyCrc: () => verifyCrc
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
39
|
+
var KHQRGenerator = class {
|
|
40
|
+
constructor(merchantType) {
|
|
41
|
+
// individual optional
|
|
42
|
+
this.merchantCity = "Phnom Penh";
|
|
43
|
+
this.currency = "KHR";
|
|
44
|
+
this.isStatic = false;
|
|
45
|
+
this.merchantType = merchantType;
|
|
46
|
+
}
|
|
47
|
+
setStatic(v) {
|
|
48
|
+
this.isStatic = !!v;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
setBakongAccountId(v) {
|
|
52
|
+
this.bakongAccountId = v;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
setMerchantName(v) {
|
|
56
|
+
this.merchantName = v;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
setMerchantId(v) {
|
|
60
|
+
this.merchantId = v;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
setAcquiringBank(v) {
|
|
64
|
+
this.acquiringBank = v;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
setAccountInformation(v) {
|
|
68
|
+
this.accountInformation = v;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
setMerchantCity(v) {
|
|
72
|
+
this.merchantCity = v;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
setCurrency(v) {
|
|
76
|
+
const up = v.toUpperCase();
|
|
77
|
+
if (up !== "KHR" && up !== "USD") throw new Error("currency must be KHR or USD");
|
|
78
|
+
this.currency = up;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
setAmount(v) {
|
|
82
|
+
this.amount = normalizeAmount(v, this.currency);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
setBillNumber(v) {
|
|
86
|
+
this.billNumber = v;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
setMobileNumber(v) {
|
|
90
|
+
this.mobileNumber = v;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
setStoreLabel(v) {
|
|
94
|
+
this.storeLabel = v;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
setTerminalLabel(v) {
|
|
98
|
+
this.terminalLabel = v;
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
setPurposeOfTransaction(v) {
|
|
102
|
+
this.purposeOfTransaction = v;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
setUpiAccountInformation(v) {
|
|
106
|
+
this.upiAccountInformation = v;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
setMerchantAlternateLanguagePreference(v) {
|
|
110
|
+
this.merchantAlternateLanguagePreference = v;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
setMerchantNameAlternateLanguage(v) {
|
|
114
|
+
this.merchantNameAlternateLanguage = v;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
setMerchantCityAlternateLanguage(v) {
|
|
118
|
+
this.merchantCityAlternateLanguage = v;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
generate() {
|
|
122
|
+
if (!this.bakongAccountId) throw new Error("BakongAccountID is required");
|
|
123
|
+
if (!this.merchantName) throw new Error("MerchantName is required");
|
|
124
|
+
if (this.merchantType === "merchant") {
|
|
125
|
+
if (!this.merchantId) throw new Error("MerchantID is required for merchant type");
|
|
126
|
+
if (!this.acquiringBank) throw new Error("AcquiringBank is required for merchant type");
|
|
127
|
+
}
|
|
128
|
+
const bakongAccountId = truncUtf8(this.bakongAccountId, 32);
|
|
129
|
+
const merchantName = truncUtf8(this.merchantName, 25);
|
|
130
|
+
const merchantCity = truncUtf8(this.merchantCity || "Phnom Penh", 15);
|
|
131
|
+
const merchantId = this.merchantId ? truncUtf8(this.merchantId, 32) : void 0;
|
|
132
|
+
const acquiringBank = this.acquiringBank ? truncUtf8(this.acquiringBank, 32) : void 0;
|
|
133
|
+
const accountInformation = this.accountInformation ? truncUtf8(this.accountInformation, 32) : void 0;
|
|
134
|
+
const billNumber = this.billNumber ? truncUtf8(this.billNumber, 25) : void 0;
|
|
135
|
+
const mobileNumber = this.mobileNumber ? truncUtf8(this.mobileNumber, 25) : void 0;
|
|
136
|
+
const storeLabel = this.storeLabel ? truncUtf8(this.storeLabel, 25) : void 0;
|
|
137
|
+
const terminalLabel = this.terminalLabel ? truncUtf8(this.terminalLabel, 25) : void 0;
|
|
138
|
+
const purpose = this.purposeOfTransaction ? truncUtf8(this.purposeOfTransaction, 25) : void 0;
|
|
139
|
+
const upi = this.upiAccountInformation ? truncUtf8(this.upiAccountInformation, 31) : void 0;
|
|
140
|
+
const langPref = this.merchantAlternateLanguagePreference ? truncUtf8(this.merchantAlternateLanguagePreference, 2) : void 0;
|
|
141
|
+
const nameAlt = this.merchantNameAlternateLanguage ? truncUtf8(this.merchantNameAlternateLanguage, 25) : void 0;
|
|
142
|
+
const cityAlt = this.merchantCityAlternateLanguage ? truncUtf8(this.merchantCityAlternateLanguage, 15) : void 0;
|
|
143
|
+
const poi = this.isStatic ? "11" : "12";
|
|
144
|
+
const currencyNumeric = this.currency === "KHR" ? "116" : "840";
|
|
145
|
+
const merchantCategoryCode = "5999";
|
|
146
|
+
const countryCode = "KH";
|
|
147
|
+
let payload = "";
|
|
148
|
+
payload += tlv("00", "01");
|
|
149
|
+
payload += tlv("01", poi);
|
|
150
|
+
if (upi) payload += tlv("15", upi);
|
|
151
|
+
if (this.merchantType === "individual") {
|
|
152
|
+
let t = "";
|
|
153
|
+
t += tlv("00", bakongAccountId);
|
|
154
|
+
if (accountInformation) t += tlv("01", accountInformation);
|
|
155
|
+
if (acquiringBank) t += tlv("02", acquiringBank);
|
|
156
|
+
payload += tlv("29", t);
|
|
157
|
+
} else {
|
|
158
|
+
let t = "";
|
|
159
|
+
t += tlv("00", bakongAccountId);
|
|
160
|
+
t += tlv("01", merchantId);
|
|
161
|
+
t += tlv("02", acquiringBank);
|
|
162
|
+
payload += tlv("30", t);
|
|
163
|
+
}
|
|
164
|
+
payload += tlv("52", merchantCategoryCode);
|
|
165
|
+
payload += tlv("53", currencyNumeric);
|
|
166
|
+
if (this.amount != null) payload += tlv("54", this.amount);
|
|
167
|
+
payload += tlv("58", countryCode);
|
|
168
|
+
payload += tlv("59", merchantName);
|
|
169
|
+
payload += tlv("60", merchantCity);
|
|
170
|
+
{
|
|
171
|
+
let t = "";
|
|
172
|
+
if (billNumber) t += tlv("01", billNumber);
|
|
173
|
+
if (mobileNumber) t += tlv("02", mobileNumber);
|
|
174
|
+
if (storeLabel) t += tlv("03", storeLabel);
|
|
175
|
+
if (terminalLabel) t += tlv("07", terminalLabel);
|
|
176
|
+
if (purpose) t += tlv("08", purpose);
|
|
177
|
+
if (t) payload += tlv("62", t);
|
|
178
|
+
}
|
|
179
|
+
{
|
|
180
|
+
let t = "";
|
|
181
|
+
if (langPref) t += tlv("00", langPref);
|
|
182
|
+
if (nameAlt) t += tlv("01", nameAlt);
|
|
183
|
+
if (cityAlt) t += tlv("02", cityAlt);
|
|
184
|
+
if (t) payload += tlv("64", t);
|
|
185
|
+
}
|
|
186
|
+
const timestamp = this.isStatic ? null : String(Date.now());
|
|
187
|
+
if (timestamp) payload += tlv("99", timestamp);
|
|
188
|
+
const crcInput = payload + "6304";
|
|
189
|
+
const crc = crc16CcittFalseHex(crcInput);
|
|
190
|
+
const qr = crcInput + crc;
|
|
191
|
+
const md5 = import_node_crypto.default.createHash("md5").update(qr, "utf8").digest("hex");
|
|
192
|
+
return { qr, timestamp, type: this.merchantType, md5 };
|
|
193
|
+
}
|
|
194
|
+
static verify(qr) {
|
|
195
|
+
return verifyCrc(qr);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
KHQRGenerator.MERCHANT_TYPE_INDIVIDUAL = "individual";
|
|
199
|
+
KHQRGenerator.MERCHANT_TYPE_MERCHANT = "merchant";
|
|
200
|
+
function verifyCrc(qr) {
|
|
201
|
+
const i = qr.lastIndexOf("6304");
|
|
202
|
+
if (i < 0) return false;
|
|
203
|
+
const provided = qr.slice(i + 4, i + 8);
|
|
204
|
+
if (provided.length !== 4) return false;
|
|
205
|
+
const crcInput = qr.slice(0, i + 4);
|
|
206
|
+
const expected = crc16CcittFalseHex(crcInput);
|
|
207
|
+
return provided.toUpperCase() === expected.toUpperCase();
|
|
208
|
+
}
|
|
209
|
+
function decodeTlv(qr) {
|
|
210
|
+
const out = {};
|
|
211
|
+
let i = 0;
|
|
212
|
+
while (i + 4 <= qr.length) {
|
|
213
|
+
const tag = qr.slice(i, i + 2);
|
|
214
|
+
const lenStr = qr.slice(i + 2, i + 4);
|
|
215
|
+
if (!/^\d{2}$/.test(lenStr)) break;
|
|
216
|
+
const len = Number(lenStr);
|
|
217
|
+
const vStart = i + 4;
|
|
218
|
+
const vEnd = vStart + len;
|
|
219
|
+
const value = qr.slice(vStart, vEnd);
|
|
220
|
+
out[tag] = value;
|
|
221
|
+
i = vEnd;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function tlv(tag, value) {
|
|
226
|
+
if (!/^\d{2}$/.test(tag)) throw new Error(`Invalid tag: ${tag}`);
|
|
227
|
+
const len = utf8ByteLength(value);
|
|
228
|
+
if (len > 99) throw new Error(`TLV value too long for tag ${tag}: ${len}`);
|
|
229
|
+
return tag + String(len).padStart(2, "0") + value;
|
|
230
|
+
}
|
|
231
|
+
function utf8ByteLength(s) {
|
|
232
|
+
return Buffer.byteLength(s, "utf8");
|
|
233
|
+
}
|
|
234
|
+
function truncUtf8(s, maxBytes) {
|
|
235
|
+
if (utf8ByteLength(s) <= maxBytes) return s;
|
|
236
|
+
let out = "";
|
|
237
|
+
for (const ch of s) {
|
|
238
|
+
const next = out + ch;
|
|
239
|
+
if (utf8ByteLength(next) > maxBytes) break;
|
|
240
|
+
out = next;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function crc16CcittFalseHex(input) {
|
|
245
|
+
const bytes = Buffer.from(input, "utf8");
|
|
246
|
+
let crc = 65535;
|
|
247
|
+
for (const b of bytes) {
|
|
248
|
+
crc ^= (b & 255) << 8;
|
|
249
|
+
for (let i = 0; i < 8; i++) {
|
|
250
|
+
crc = crc & 32768 ? crc << 1 ^ 4129 : crc << 1;
|
|
251
|
+
crc &= 65535;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return crc.toString(16).toUpperCase().padStart(4, "0");
|
|
255
|
+
}
|
|
256
|
+
function normalizeAmount(v, currency) {
|
|
257
|
+
if (typeof v === "string") {
|
|
258
|
+
const s = v.trim();
|
|
259
|
+
if (!/^\d+(\.\d+)?$/.test(s)) throw new Error("Amount must be numeric");
|
|
260
|
+
return normalizeAmountString(s, currency);
|
|
261
|
+
}
|
|
262
|
+
if (!Number.isFinite(v)) throw new Error("Amount must be finite");
|
|
263
|
+
if (v < 0) throw new Error("Amount must be >= 0");
|
|
264
|
+
return normalizeAmountString(String(v), currency);
|
|
265
|
+
}
|
|
266
|
+
function normalizeAmountString(s, currency) {
|
|
267
|
+
if (currency === "USD") {
|
|
268
|
+
const n = Number(s);
|
|
269
|
+
return n.toFixed(2);
|
|
270
|
+
}
|
|
271
|
+
if (!s.includes(".")) return s;
|
|
272
|
+
s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
273
|
+
return s.length ? s : "0";
|
|
274
|
+
}
|
|
275
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
276
|
+
0 && (module.exports = {
|
|
277
|
+
KHQRGenerator,
|
|
278
|
+
decodeTlv,
|
|
279
|
+
verifyCrc
|
|
280
|
+
});
|
|
281
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nexport type MerchantType = \"individual\" | \"merchant\";\n\nexport interface GenerateResult {\n qr: string;\n timestamp: string | null;\n type: MerchantType;\n md5: string;\n}\n\n/**\n * KHQR / EMVCo merchant-presented payload generator for Bakong Cambodia.\n *\n * - Individual: Tag 29\n * - Merchant: Tag 30\n * - Additional: Tag 62\n * - Alt Lang: Tag 64\n * - Timestamp: Tag 99 (dynamic only)\n * - CRC: Tag 63 (CRC-16/CCITT-FALSE)\n */\nexport class KHQRGenerator {\n static readonly MERCHANT_TYPE_INDIVIDUAL: MerchantType = \"individual\";\n static readonly MERCHANT_TYPE_MERCHANT: MerchantType = \"merchant\";\n\n private merchantType: MerchantType;\n\n private bakongAccountId?: string;\n private merchantName?: string;\n\n private merchantId?: string; // merchant type\n private acquiringBank?: string; // merchant required, individual optional\n private accountInformation?: string; // individual optional\n\n private merchantCity = \"Phnom Penh\";\n private currency: \"KHR\" | \"USD\" = \"KHR\";\n private amount?: string;\n\n // Tag 62 (Additional data)\n private billNumber?: string;\n private mobileNumber?: string;\n private storeLabel?: string;\n private terminalLabel?: string;\n private purposeOfTransaction?: string;\n\n // Tag 15 UPI (optional)\n private upiAccountInformation?: string;\n\n // Tag 64 (Alternate language)\n private merchantAlternateLanguagePreference?: string;\n private merchantNameAlternateLanguage?: string;\n private merchantCityAlternateLanguage?: string;\n\n private isStatic = false;\n\n constructor(merchantType: MerchantType) {\n this.merchantType = merchantType;\n }\n\n setStatic(v: boolean) {\n this.isStatic = !!v;\n return this;\n }\n\n setBakongAccountId(v: string) {\n this.bakongAccountId = v;\n return this;\n }\n\n setMerchantName(v: string) {\n this.merchantName = v;\n return this;\n }\n\n setMerchantId(v: string) {\n this.merchantId = v;\n return this;\n }\n\n setAcquiringBank(v: string) {\n this.acquiringBank = v;\n return this;\n }\n\n setAccountInformation(v: string) {\n this.accountInformation = v;\n return this;\n }\n\n setMerchantCity(v: string) {\n this.merchantCity = v;\n return this;\n }\n\n setCurrency(v: string) {\n const up = v.toUpperCase();\n if (up !== \"KHR\" && up !== \"USD\") throw new Error(\"currency must be KHR or USD\");\n this.currency = up as \"KHR\" | \"USD\";\n return this;\n }\n\n setAmount(v: number | string) {\n this.amount = normalizeAmount(v, this.currency);\n return this;\n }\n\n setBillNumber(v: string) {\n this.billNumber = v;\n return this;\n }\n\n setMobileNumber(v: string) {\n this.mobileNumber = v;\n return this;\n }\n\n setStoreLabel(v: string) {\n this.storeLabel = v;\n return this;\n }\n\n setTerminalLabel(v: string) {\n this.terminalLabel = v;\n return this;\n }\n\n setPurposeOfTransaction(v: string) {\n this.purposeOfTransaction = v;\n return this;\n }\n\n setUpiAccountInformation(v: string) {\n this.upiAccountInformation = v;\n return this;\n }\n\n setMerchantAlternateLanguagePreference(v: string) {\n this.merchantAlternateLanguagePreference = v;\n return this;\n }\n\n setMerchantNameAlternateLanguage(v: string) {\n this.merchantNameAlternateLanguage = v;\n return this;\n }\n\n setMerchantCityAlternateLanguage(v: string) {\n this.merchantCityAlternateLanguage = v;\n return this;\n }\n\n generate(): GenerateResult {\n if (!this.bakongAccountId) throw new Error(\"BakongAccountID is required\");\n if (!this.merchantName) throw new Error(\"MerchantName is required\");\n\n if (this.merchantType === \"merchant\") {\n if (!this.merchantId) throw new Error(\"MerchantID is required for merchant type\");\n if (!this.acquiringBank) throw new Error(\"AcquiringBank is required for merchant type\");\n }\n\n // Truncation limits commonly used in KHQR implementations\n const bakongAccountId = truncUtf8(this.bakongAccountId, 32);\n const merchantName = truncUtf8(this.merchantName, 25);\n const merchantCity = truncUtf8(this.merchantCity || \"Phnom Penh\", 15);\n\n const merchantId = this.merchantId ? truncUtf8(this.merchantId, 32) : undefined;\n const acquiringBank = this.acquiringBank ? truncUtf8(this.acquiringBank, 32) : undefined;\n const accountInformation = this.accountInformation ? truncUtf8(this.accountInformation, 32) : undefined;\n\n const billNumber = this.billNumber ? truncUtf8(this.billNumber, 25) : undefined;\n const mobileNumber = this.mobileNumber ? truncUtf8(this.mobileNumber, 25) : undefined;\n const storeLabel = this.storeLabel ? truncUtf8(this.storeLabel, 25) : undefined;\n const terminalLabel = this.terminalLabel ? truncUtf8(this.terminalLabel, 25) : undefined;\n const purpose = this.purposeOfTransaction ? truncUtf8(this.purposeOfTransaction, 25) : undefined;\n\n const upi = this.upiAccountInformation ? truncUtf8(this.upiAccountInformation, 31) : undefined;\n\n const langPref = this.merchantAlternateLanguagePreference\n ? truncUtf8(this.merchantAlternateLanguagePreference, 2)\n : undefined;\n const nameAlt = this.merchantNameAlternateLanguage\n ? truncUtf8(this.merchantNameAlternateLanguage, 25)\n : undefined;\n const cityAlt = this.merchantCityAlternateLanguage\n ? truncUtf8(this.merchantCityAlternateLanguage, 15)\n : undefined;\n\n const poi = this.isStatic ? \"11\" : \"12\";\n const currencyNumeric = this.currency === \"KHR\" ? \"116\" : \"840\";\n\n // Common defaults in KHQR samples\n const merchantCategoryCode = \"5999\";\n const countryCode = \"KH\";\n\n let payload = \"\";\n payload += tlv(\"00\", \"01\");\n payload += tlv(\"01\", poi);\n\n if (upi) payload += tlv(\"15\", upi);\n\n if (this.merchantType === \"individual\") {\n let t = \"\";\n t += tlv(\"00\", bakongAccountId);\n if (accountInformation) t += tlv(\"01\", accountInformation);\n if (acquiringBank) t += tlv(\"02\", acquiringBank);\n payload += tlv(\"29\", t);\n } else {\n let t = \"\";\n t += tlv(\"00\", bakongAccountId);\n t += tlv(\"01\", merchantId!);\n t += tlv(\"02\", acquiringBank!);\n payload += tlv(\"30\", t);\n }\n\n payload += tlv(\"52\", merchantCategoryCode);\n payload += tlv(\"53\", currencyNumeric);\n if (this.amount != null) payload += tlv(\"54\", this.amount);\n payload += tlv(\"58\", countryCode);\n payload += tlv(\"59\", merchantName);\n payload += tlv(\"60\", merchantCity);\n\n // Tag 62: Additional data (optional)\n {\n let t = \"\";\n if (billNumber) t += tlv(\"01\", billNumber);\n if (mobileNumber) t += tlv(\"02\", mobileNumber);\n if (storeLabel) t += tlv(\"03\", storeLabel);\n if (terminalLabel) t += tlv(\"07\", terminalLabel);\n if (purpose) t += tlv(\"08\", purpose);\n if (t) payload += tlv(\"62\", t);\n }\n\n // Tag 64: Alternate language (optional)\n {\n let t = \"\";\n if (langPref) t += tlv(\"00\", langPref);\n if (nameAlt) t += tlv(\"01\", nameAlt);\n if (cityAlt) t += tlv(\"02\", cityAlt);\n if (t) payload += tlv(\"64\", t);\n }\n\n // Tag 99: Timestamp (dynamic only)\n const timestamp = this.isStatic ? null : String(Date.now());\n if (timestamp) payload += tlv(\"99\", timestamp);\n\n // CRC: Tag 63 (length 04). Compute over payload + \"6304\"\n const crcInput = payload + \"6304\";\n const crc = crc16CcittFalseHex(crcInput);\n const qr = crcInput + crc;\n\n const md5 = crypto.createHash(\"md5\").update(qr, \"utf8\").digest(\"hex\");\n\n return { qr, timestamp, type: this.merchantType, md5 };\n }\n\n static verify(qr: string): boolean {\n return verifyCrc(qr);\n }\n}\n\nexport function verifyCrc(qr: string): boolean {\n const i = qr.lastIndexOf(\"6304\");\n if (i < 0) return false;\n const provided = qr.slice(i + 4, i + 8);\n if (provided.length !== 4) return false;\n\n const crcInput = qr.slice(0, i + 4); // include \"6304\"\n const expected = crc16CcittFalseHex(crcInput);\n return provided.toUpperCase() === expected.toUpperCase();\n}\n\n/** Top-level TLV decoder (does not recursively decode nested templates). */\nexport function decodeTlv(qr: string): Record<string, string> {\n const out: Record<string, string> = {};\n let i = 0;\n while (i + 4 <= qr.length) {\n const tag = qr.slice(i, i + 2);\n const lenStr = qr.slice(i + 2, i + 4);\n if (!/^\\d{2}$/.test(lenStr)) break;\n const len = Number(lenStr);\n const vStart = i + 4;\n const vEnd = vStart + len;\n const value = qr.slice(vStart, vEnd);\n out[tag] = value;\n i = vEnd;\n }\n return out;\n}\n\n// ---------------- helpers ----------------\n\nfunction tlv(tag: string, value: string): string {\n if (!/^\\d{2}$/.test(tag)) throw new Error(`Invalid tag: ${tag}`);\n const len = utf8ByteLength(value);\n if (len > 99) throw new Error(`TLV value too long for tag ${tag}: ${len}`);\n return tag + String(len).padStart(2, \"0\") + value;\n}\n\nfunction utf8ByteLength(s: string): number {\n return Buffer.byteLength(s, \"utf8\");\n}\n\nfunction truncUtf8(s: string, maxBytes: number): string {\n if (utf8ByteLength(s) <= maxBytes) return s;\n let out = \"\";\n for (const ch of s) {\n const next = out + ch;\n if (utf8ByteLength(next) > maxBytes) break;\n out = next;\n }\n return out;\n}\n\n/** CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF) */\nfunction crc16CcittFalseHex(input: string): string {\n const bytes = Buffer.from(input, \"utf8\");\n let crc = 0xffff;\n for (const b of bytes) {\n crc ^= (b & 0xff) << 8;\n for (let i = 0; i < 8; i++) {\n crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);\n crc &= 0xffff;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\nfunction normalizeAmount(v: number | string, currency: \"KHR\" | \"USD\"): string {\n if (typeof v === \"string\") {\n const s = v.trim();\n if (!/^\\d+(\\.\\d+)?$/.test(s)) throw new Error(\"Amount must be numeric\");\n return normalizeAmountString(s, currency);\n }\n\n if (!Number.isFinite(v)) throw new Error(\"Amount must be finite\");\n if (v < 0) throw new Error(\"Amount must be >= 0\");\n return normalizeAmountString(String(v), currency);\n}\n\nfunction normalizeAmountString(s: string, currency: \"KHR\" | \"USD\"): string {\n if (currency === \"USD\") {\n const n = Number(s);\n return n.toFixed(2);\n }\n\n if (!s.includes(\".\")) return s;\n s = s.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n return s.length ? s : \"0\";\n}\n"],"mappings":"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","names":["crypto"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type MerchantType = "individual" | "merchant";
|
|
2
|
+
interface GenerateResult {
|
|
3
|
+
qr: string;
|
|
4
|
+
timestamp: string | null;
|
|
5
|
+
type: MerchantType;
|
|
6
|
+
md5: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* KHQR / EMVCo merchant-presented payload generator for Bakong Cambodia.
|
|
10
|
+
*
|
|
11
|
+
* - Individual: Tag 29
|
|
12
|
+
* - Merchant: Tag 30
|
|
13
|
+
* - Additional: Tag 62
|
|
14
|
+
* - Alt Lang: Tag 64
|
|
15
|
+
* - Timestamp: Tag 99 (dynamic only)
|
|
16
|
+
* - CRC: Tag 63 (CRC-16/CCITT-FALSE)
|
|
17
|
+
*/
|
|
18
|
+
declare class KHQRGenerator {
|
|
19
|
+
static readonly MERCHANT_TYPE_INDIVIDUAL: MerchantType;
|
|
20
|
+
static readonly MERCHANT_TYPE_MERCHANT: MerchantType;
|
|
21
|
+
private merchantType;
|
|
22
|
+
private bakongAccountId?;
|
|
23
|
+
private merchantName?;
|
|
24
|
+
private merchantId?;
|
|
25
|
+
private acquiringBank?;
|
|
26
|
+
private accountInformation?;
|
|
27
|
+
private merchantCity;
|
|
28
|
+
private currency;
|
|
29
|
+
private amount?;
|
|
30
|
+
private billNumber?;
|
|
31
|
+
private mobileNumber?;
|
|
32
|
+
private storeLabel?;
|
|
33
|
+
private terminalLabel?;
|
|
34
|
+
private purposeOfTransaction?;
|
|
35
|
+
private upiAccountInformation?;
|
|
36
|
+
private merchantAlternateLanguagePreference?;
|
|
37
|
+
private merchantNameAlternateLanguage?;
|
|
38
|
+
private merchantCityAlternateLanguage?;
|
|
39
|
+
private isStatic;
|
|
40
|
+
constructor(merchantType: MerchantType);
|
|
41
|
+
setStatic(v: boolean): this;
|
|
42
|
+
setBakongAccountId(v: string): this;
|
|
43
|
+
setMerchantName(v: string): this;
|
|
44
|
+
setMerchantId(v: string): this;
|
|
45
|
+
setAcquiringBank(v: string): this;
|
|
46
|
+
setAccountInformation(v: string): this;
|
|
47
|
+
setMerchantCity(v: string): this;
|
|
48
|
+
setCurrency(v: string): this;
|
|
49
|
+
setAmount(v: number | string): this;
|
|
50
|
+
setBillNumber(v: string): this;
|
|
51
|
+
setMobileNumber(v: string): this;
|
|
52
|
+
setStoreLabel(v: string): this;
|
|
53
|
+
setTerminalLabel(v: string): this;
|
|
54
|
+
setPurposeOfTransaction(v: string): this;
|
|
55
|
+
setUpiAccountInformation(v: string): this;
|
|
56
|
+
setMerchantAlternateLanguagePreference(v: string): this;
|
|
57
|
+
setMerchantNameAlternateLanguage(v: string): this;
|
|
58
|
+
setMerchantCityAlternateLanguage(v: string): this;
|
|
59
|
+
generate(): GenerateResult;
|
|
60
|
+
static verify(qr: string): boolean;
|
|
61
|
+
}
|
|
62
|
+
declare function verifyCrc(qr: string): boolean;
|
|
63
|
+
/** Top-level TLV decoder (does not recursively decode nested templates). */
|
|
64
|
+
declare function decodeTlv(qr: string): Record<string, string>;
|
|
65
|
+
|
|
66
|
+
export { type GenerateResult, KHQRGenerator, type MerchantType, decodeTlv, verifyCrc };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type MerchantType = "individual" | "merchant";
|
|
2
|
+
interface GenerateResult {
|
|
3
|
+
qr: string;
|
|
4
|
+
timestamp: string | null;
|
|
5
|
+
type: MerchantType;
|
|
6
|
+
md5: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* KHQR / EMVCo merchant-presented payload generator for Bakong Cambodia.
|
|
10
|
+
*
|
|
11
|
+
* - Individual: Tag 29
|
|
12
|
+
* - Merchant: Tag 30
|
|
13
|
+
* - Additional: Tag 62
|
|
14
|
+
* - Alt Lang: Tag 64
|
|
15
|
+
* - Timestamp: Tag 99 (dynamic only)
|
|
16
|
+
* - CRC: Tag 63 (CRC-16/CCITT-FALSE)
|
|
17
|
+
*/
|
|
18
|
+
declare class KHQRGenerator {
|
|
19
|
+
static readonly MERCHANT_TYPE_INDIVIDUAL: MerchantType;
|
|
20
|
+
static readonly MERCHANT_TYPE_MERCHANT: MerchantType;
|
|
21
|
+
private merchantType;
|
|
22
|
+
private bakongAccountId?;
|
|
23
|
+
private merchantName?;
|
|
24
|
+
private merchantId?;
|
|
25
|
+
private acquiringBank?;
|
|
26
|
+
private accountInformation?;
|
|
27
|
+
private merchantCity;
|
|
28
|
+
private currency;
|
|
29
|
+
private amount?;
|
|
30
|
+
private billNumber?;
|
|
31
|
+
private mobileNumber?;
|
|
32
|
+
private storeLabel?;
|
|
33
|
+
private terminalLabel?;
|
|
34
|
+
private purposeOfTransaction?;
|
|
35
|
+
private upiAccountInformation?;
|
|
36
|
+
private merchantAlternateLanguagePreference?;
|
|
37
|
+
private merchantNameAlternateLanguage?;
|
|
38
|
+
private merchantCityAlternateLanguage?;
|
|
39
|
+
private isStatic;
|
|
40
|
+
constructor(merchantType: MerchantType);
|
|
41
|
+
setStatic(v: boolean): this;
|
|
42
|
+
setBakongAccountId(v: string): this;
|
|
43
|
+
setMerchantName(v: string): this;
|
|
44
|
+
setMerchantId(v: string): this;
|
|
45
|
+
setAcquiringBank(v: string): this;
|
|
46
|
+
setAccountInformation(v: string): this;
|
|
47
|
+
setMerchantCity(v: string): this;
|
|
48
|
+
setCurrency(v: string): this;
|
|
49
|
+
setAmount(v: number | string): this;
|
|
50
|
+
setBillNumber(v: string): this;
|
|
51
|
+
setMobileNumber(v: string): this;
|
|
52
|
+
setStoreLabel(v: string): this;
|
|
53
|
+
setTerminalLabel(v: string): this;
|
|
54
|
+
setPurposeOfTransaction(v: string): this;
|
|
55
|
+
setUpiAccountInformation(v: string): this;
|
|
56
|
+
setMerchantAlternateLanguagePreference(v: string): this;
|
|
57
|
+
setMerchantNameAlternateLanguage(v: string): this;
|
|
58
|
+
setMerchantCityAlternateLanguage(v: string): this;
|
|
59
|
+
generate(): GenerateResult;
|
|
60
|
+
static verify(qr: string): boolean;
|
|
61
|
+
}
|
|
62
|
+
declare function verifyCrc(qr: string): boolean;
|
|
63
|
+
/** Top-level TLV decoder (does not recursively decode nested templates). */
|
|
64
|
+
declare function decodeTlv(qr: string): Record<string, string>;
|
|
65
|
+
|
|
66
|
+
export { type GenerateResult, KHQRGenerator, type MerchantType, decodeTlv, verifyCrc };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
var KHQRGenerator = class {
|
|
4
|
+
constructor(merchantType) {
|
|
5
|
+
// individual optional
|
|
6
|
+
this.merchantCity = "Phnom Penh";
|
|
7
|
+
this.currency = "KHR";
|
|
8
|
+
this.isStatic = false;
|
|
9
|
+
this.merchantType = merchantType;
|
|
10
|
+
}
|
|
11
|
+
setStatic(v) {
|
|
12
|
+
this.isStatic = !!v;
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
setBakongAccountId(v) {
|
|
16
|
+
this.bakongAccountId = v;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
setMerchantName(v) {
|
|
20
|
+
this.merchantName = v;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
setMerchantId(v) {
|
|
24
|
+
this.merchantId = v;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
setAcquiringBank(v) {
|
|
28
|
+
this.acquiringBank = v;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
setAccountInformation(v) {
|
|
32
|
+
this.accountInformation = v;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
setMerchantCity(v) {
|
|
36
|
+
this.merchantCity = v;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
setCurrency(v) {
|
|
40
|
+
const up = v.toUpperCase();
|
|
41
|
+
if (up !== "KHR" && up !== "USD") throw new Error("currency must be KHR or USD");
|
|
42
|
+
this.currency = up;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
setAmount(v) {
|
|
46
|
+
this.amount = normalizeAmount(v, this.currency);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
setBillNumber(v) {
|
|
50
|
+
this.billNumber = v;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
setMobileNumber(v) {
|
|
54
|
+
this.mobileNumber = v;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
setStoreLabel(v) {
|
|
58
|
+
this.storeLabel = v;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
setTerminalLabel(v) {
|
|
62
|
+
this.terminalLabel = v;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
setPurposeOfTransaction(v) {
|
|
66
|
+
this.purposeOfTransaction = v;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
setUpiAccountInformation(v) {
|
|
70
|
+
this.upiAccountInformation = v;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
setMerchantAlternateLanguagePreference(v) {
|
|
74
|
+
this.merchantAlternateLanguagePreference = v;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
setMerchantNameAlternateLanguage(v) {
|
|
78
|
+
this.merchantNameAlternateLanguage = v;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
setMerchantCityAlternateLanguage(v) {
|
|
82
|
+
this.merchantCityAlternateLanguage = v;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
generate() {
|
|
86
|
+
if (!this.bakongAccountId) throw new Error("BakongAccountID is required");
|
|
87
|
+
if (!this.merchantName) throw new Error("MerchantName is required");
|
|
88
|
+
if (this.merchantType === "merchant") {
|
|
89
|
+
if (!this.merchantId) throw new Error("MerchantID is required for merchant type");
|
|
90
|
+
if (!this.acquiringBank) throw new Error("AcquiringBank is required for merchant type");
|
|
91
|
+
}
|
|
92
|
+
const bakongAccountId = truncUtf8(this.bakongAccountId, 32);
|
|
93
|
+
const merchantName = truncUtf8(this.merchantName, 25);
|
|
94
|
+
const merchantCity = truncUtf8(this.merchantCity || "Phnom Penh", 15);
|
|
95
|
+
const merchantId = this.merchantId ? truncUtf8(this.merchantId, 32) : void 0;
|
|
96
|
+
const acquiringBank = this.acquiringBank ? truncUtf8(this.acquiringBank, 32) : void 0;
|
|
97
|
+
const accountInformation = this.accountInformation ? truncUtf8(this.accountInformation, 32) : void 0;
|
|
98
|
+
const billNumber = this.billNumber ? truncUtf8(this.billNumber, 25) : void 0;
|
|
99
|
+
const mobileNumber = this.mobileNumber ? truncUtf8(this.mobileNumber, 25) : void 0;
|
|
100
|
+
const storeLabel = this.storeLabel ? truncUtf8(this.storeLabel, 25) : void 0;
|
|
101
|
+
const terminalLabel = this.terminalLabel ? truncUtf8(this.terminalLabel, 25) : void 0;
|
|
102
|
+
const purpose = this.purposeOfTransaction ? truncUtf8(this.purposeOfTransaction, 25) : void 0;
|
|
103
|
+
const upi = this.upiAccountInformation ? truncUtf8(this.upiAccountInformation, 31) : void 0;
|
|
104
|
+
const langPref = this.merchantAlternateLanguagePreference ? truncUtf8(this.merchantAlternateLanguagePreference, 2) : void 0;
|
|
105
|
+
const nameAlt = this.merchantNameAlternateLanguage ? truncUtf8(this.merchantNameAlternateLanguage, 25) : void 0;
|
|
106
|
+
const cityAlt = this.merchantCityAlternateLanguage ? truncUtf8(this.merchantCityAlternateLanguage, 15) : void 0;
|
|
107
|
+
const poi = this.isStatic ? "11" : "12";
|
|
108
|
+
const currencyNumeric = this.currency === "KHR" ? "116" : "840";
|
|
109
|
+
const merchantCategoryCode = "5999";
|
|
110
|
+
const countryCode = "KH";
|
|
111
|
+
let payload = "";
|
|
112
|
+
payload += tlv("00", "01");
|
|
113
|
+
payload += tlv("01", poi);
|
|
114
|
+
if (upi) payload += tlv("15", upi);
|
|
115
|
+
if (this.merchantType === "individual") {
|
|
116
|
+
let t = "";
|
|
117
|
+
t += tlv("00", bakongAccountId);
|
|
118
|
+
if (accountInformation) t += tlv("01", accountInformation);
|
|
119
|
+
if (acquiringBank) t += tlv("02", acquiringBank);
|
|
120
|
+
payload += tlv("29", t);
|
|
121
|
+
} else {
|
|
122
|
+
let t = "";
|
|
123
|
+
t += tlv("00", bakongAccountId);
|
|
124
|
+
t += tlv("01", merchantId);
|
|
125
|
+
t += tlv("02", acquiringBank);
|
|
126
|
+
payload += tlv("30", t);
|
|
127
|
+
}
|
|
128
|
+
payload += tlv("52", merchantCategoryCode);
|
|
129
|
+
payload += tlv("53", currencyNumeric);
|
|
130
|
+
if (this.amount != null) payload += tlv("54", this.amount);
|
|
131
|
+
payload += tlv("58", countryCode);
|
|
132
|
+
payload += tlv("59", merchantName);
|
|
133
|
+
payload += tlv("60", merchantCity);
|
|
134
|
+
{
|
|
135
|
+
let t = "";
|
|
136
|
+
if (billNumber) t += tlv("01", billNumber);
|
|
137
|
+
if (mobileNumber) t += tlv("02", mobileNumber);
|
|
138
|
+
if (storeLabel) t += tlv("03", storeLabel);
|
|
139
|
+
if (terminalLabel) t += tlv("07", terminalLabel);
|
|
140
|
+
if (purpose) t += tlv("08", purpose);
|
|
141
|
+
if (t) payload += tlv("62", t);
|
|
142
|
+
}
|
|
143
|
+
{
|
|
144
|
+
let t = "";
|
|
145
|
+
if (langPref) t += tlv("00", langPref);
|
|
146
|
+
if (nameAlt) t += tlv("01", nameAlt);
|
|
147
|
+
if (cityAlt) t += tlv("02", cityAlt);
|
|
148
|
+
if (t) payload += tlv("64", t);
|
|
149
|
+
}
|
|
150
|
+
const timestamp = this.isStatic ? null : String(Date.now());
|
|
151
|
+
if (timestamp) payload += tlv("99", timestamp);
|
|
152
|
+
const crcInput = payload + "6304";
|
|
153
|
+
const crc = crc16CcittFalseHex(crcInput);
|
|
154
|
+
const qr = crcInput + crc;
|
|
155
|
+
const md5 = crypto.createHash("md5").update(qr, "utf8").digest("hex");
|
|
156
|
+
return { qr, timestamp, type: this.merchantType, md5 };
|
|
157
|
+
}
|
|
158
|
+
static verify(qr) {
|
|
159
|
+
return verifyCrc(qr);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
KHQRGenerator.MERCHANT_TYPE_INDIVIDUAL = "individual";
|
|
163
|
+
KHQRGenerator.MERCHANT_TYPE_MERCHANT = "merchant";
|
|
164
|
+
function verifyCrc(qr) {
|
|
165
|
+
const i = qr.lastIndexOf("6304");
|
|
166
|
+
if (i < 0) return false;
|
|
167
|
+
const provided = qr.slice(i + 4, i + 8);
|
|
168
|
+
if (provided.length !== 4) return false;
|
|
169
|
+
const crcInput = qr.slice(0, i + 4);
|
|
170
|
+
const expected = crc16CcittFalseHex(crcInput);
|
|
171
|
+
return provided.toUpperCase() === expected.toUpperCase();
|
|
172
|
+
}
|
|
173
|
+
function decodeTlv(qr) {
|
|
174
|
+
const out = {};
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i + 4 <= qr.length) {
|
|
177
|
+
const tag = qr.slice(i, i + 2);
|
|
178
|
+
const lenStr = qr.slice(i + 2, i + 4);
|
|
179
|
+
if (!/^\d{2}$/.test(lenStr)) break;
|
|
180
|
+
const len = Number(lenStr);
|
|
181
|
+
const vStart = i + 4;
|
|
182
|
+
const vEnd = vStart + len;
|
|
183
|
+
const value = qr.slice(vStart, vEnd);
|
|
184
|
+
out[tag] = value;
|
|
185
|
+
i = vEnd;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function tlv(tag, value) {
|
|
190
|
+
if (!/^\d{2}$/.test(tag)) throw new Error(`Invalid tag: ${tag}`);
|
|
191
|
+
const len = utf8ByteLength(value);
|
|
192
|
+
if (len > 99) throw new Error(`TLV value too long for tag ${tag}: ${len}`);
|
|
193
|
+
return tag + String(len).padStart(2, "0") + value;
|
|
194
|
+
}
|
|
195
|
+
function utf8ByteLength(s) {
|
|
196
|
+
return Buffer.byteLength(s, "utf8");
|
|
197
|
+
}
|
|
198
|
+
function truncUtf8(s, maxBytes) {
|
|
199
|
+
if (utf8ByteLength(s) <= maxBytes) return s;
|
|
200
|
+
let out = "";
|
|
201
|
+
for (const ch of s) {
|
|
202
|
+
const next = out + ch;
|
|
203
|
+
if (utf8ByteLength(next) > maxBytes) break;
|
|
204
|
+
out = next;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function crc16CcittFalseHex(input) {
|
|
209
|
+
const bytes = Buffer.from(input, "utf8");
|
|
210
|
+
let crc = 65535;
|
|
211
|
+
for (const b of bytes) {
|
|
212
|
+
crc ^= (b & 255) << 8;
|
|
213
|
+
for (let i = 0; i < 8; i++) {
|
|
214
|
+
crc = crc & 32768 ? crc << 1 ^ 4129 : crc << 1;
|
|
215
|
+
crc &= 65535;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return crc.toString(16).toUpperCase().padStart(4, "0");
|
|
219
|
+
}
|
|
220
|
+
function normalizeAmount(v, currency) {
|
|
221
|
+
if (typeof v === "string") {
|
|
222
|
+
const s = v.trim();
|
|
223
|
+
if (!/^\d+(\.\d+)?$/.test(s)) throw new Error("Amount must be numeric");
|
|
224
|
+
return normalizeAmountString(s, currency);
|
|
225
|
+
}
|
|
226
|
+
if (!Number.isFinite(v)) throw new Error("Amount must be finite");
|
|
227
|
+
if (v < 0) throw new Error("Amount must be >= 0");
|
|
228
|
+
return normalizeAmountString(String(v), currency);
|
|
229
|
+
}
|
|
230
|
+
function normalizeAmountString(s, currency) {
|
|
231
|
+
if (currency === "USD") {
|
|
232
|
+
const n = Number(s);
|
|
233
|
+
return n.toFixed(2);
|
|
234
|
+
}
|
|
235
|
+
if (!s.includes(".")) return s;
|
|
236
|
+
s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
237
|
+
return s.length ? s : "0";
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
KHQRGenerator,
|
|
241
|
+
decodeTlv,
|
|
242
|
+
verifyCrc
|
|
243
|
+
};
|
|
244
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nexport type MerchantType = \"individual\" | \"merchant\";\n\nexport interface GenerateResult {\n qr: string;\n timestamp: string | null;\n type: MerchantType;\n md5: string;\n}\n\n/**\n * KHQR / EMVCo merchant-presented payload generator for Bakong Cambodia.\n *\n * - Individual: Tag 29\n * - Merchant: Tag 30\n * - Additional: Tag 62\n * - Alt Lang: Tag 64\n * - Timestamp: Tag 99 (dynamic only)\n * - CRC: Tag 63 (CRC-16/CCITT-FALSE)\n */\nexport class KHQRGenerator {\n static readonly MERCHANT_TYPE_INDIVIDUAL: MerchantType = \"individual\";\n static readonly MERCHANT_TYPE_MERCHANT: MerchantType = \"merchant\";\n\n private merchantType: MerchantType;\n\n private bakongAccountId?: string;\n private merchantName?: string;\n\n private merchantId?: string; // merchant type\n private acquiringBank?: string; // merchant required, individual optional\n private accountInformation?: string; // individual optional\n\n private merchantCity = \"Phnom Penh\";\n private currency: \"KHR\" | \"USD\" = \"KHR\";\n private amount?: string;\n\n // Tag 62 (Additional data)\n private billNumber?: string;\n private mobileNumber?: string;\n private storeLabel?: string;\n private terminalLabel?: string;\n private purposeOfTransaction?: string;\n\n // Tag 15 UPI (optional)\n private upiAccountInformation?: string;\n\n // Tag 64 (Alternate language)\n private merchantAlternateLanguagePreference?: string;\n private merchantNameAlternateLanguage?: string;\n private merchantCityAlternateLanguage?: string;\n\n private isStatic = false;\n\n constructor(merchantType: MerchantType) {\n this.merchantType = merchantType;\n }\n\n setStatic(v: boolean) {\n this.isStatic = !!v;\n return this;\n }\n\n setBakongAccountId(v: string) {\n this.bakongAccountId = v;\n return this;\n }\n\n setMerchantName(v: string) {\n this.merchantName = v;\n return this;\n }\n\n setMerchantId(v: string) {\n this.merchantId = v;\n return this;\n }\n\n setAcquiringBank(v: string) {\n this.acquiringBank = v;\n return this;\n }\n\n setAccountInformation(v: string) {\n this.accountInformation = v;\n return this;\n }\n\n setMerchantCity(v: string) {\n this.merchantCity = v;\n return this;\n }\n\n setCurrency(v: string) {\n const up = v.toUpperCase();\n if (up !== \"KHR\" && up !== \"USD\") throw new Error(\"currency must be KHR or USD\");\n this.currency = up as \"KHR\" | \"USD\";\n return this;\n }\n\n setAmount(v: number | string) {\n this.amount = normalizeAmount(v, this.currency);\n return this;\n }\n\n setBillNumber(v: string) {\n this.billNumber = v;\n return this;\n }\n\n setMobileNumber(v: string) {\n this.mobileNumber = v;\n return this;\n }\n\n setStoreLabel(v: string) {\n this.storeLabel = v;\n return this;\n }\n\n setTerminalLabel(v: string) {\n this.terminalLabel = v;\n return this;\n }\n\n setPurposeOfTransaction(v: string) {\n this.purposeOfTransaction = v;\n return this;\n }\n\n setUpiAccountInformation(v: string) {\n this.upiAccountInformation = v;\n return this;\n }\n\n setMerchantAlternateLanguagePreference(v: string) {\n this.merchantAlternateLanguagePreference = v;\n return this;\n }\n\n setMerchantNameAlternateLanguage(v: string) {\n this.merchantNameAlternateLanguage = v;\n return this;\n }\n\n setMerchantCityAlternateLanguage(v: string) {\n this.merchantCityAlternateLanguage = v;\n return this;\n }\n\n generate(): GenerateResult {\n if (!this.bakongAccountId) throw new Error(\"BakongAccountID is required\");\n if (!this.merchantName) throw new Error(\"MerchantName is required\");\n\n if (this.merchantType === \"merchant\") {\n if (!this.merchantId) throw new Error(\"MerchantID is required for merchant type\");\n if (!this.acquiringBank) throw new Error(\"AcquiringBank is required for merchant type\");\n }\n\n // Truncation limits commonly used in KHQR implementations\n const bakongAccountId = truncUtf8(this.bakongAccountId, 32);\n const merchantName = truncUtf8(this.merchantName, 25);\n const merchantCity = truncUtf8(this.merchantCity || \"Phnom Penh\", 15);\n\n const merchantId = this.merchantId ? truncUtf8(this.merchantId, 32) : undefined;\n const acquiringBank = this.acquiringBank ? truncUtf8(this.acquiringBank, 32) : undefined;\n const accountInformation = this.accountInformation ? truncUtf8(this.accountInformation, 32) : undefined;\n\n const billNumber = this.billNumber ? truncUtf8(this.billNumber, 25) : undefined;\n const mobileNumber = this.mobileNumber ? truncUtf8(this.mobileNumber, 25) : undefined;\n const storeLabel = this.storeLabel ? truncUtf8(this.storeLabel, 25) : undefined;\n const terminalLabel = this.terminalLabel ? truncUtf8(this.terminalLabel, 25) : undefined;\n const purpose = this.purposeOfTransaction ? truncUtf8(this.purposeOfTransaction, 25) : undefined;\n\n const upi = this.upiAccountInformation ? truncUtf8(this.upiAccountInformation, 31) : undefined;\n\n const langPref = this.merchantAlternateLanguagePreference\n ? truncUtf8(this.merchantAlternateLanguagePreference, 2)\n : undefined;\n const nameAlt = this.merchantNameAlternateLanguage\n ? truncUtf8(this.merchantNameAlternateLanguage, 25)\n : undefined;\n const cityAlt = this.merchantCityAlternateLanguage\n ? truncUtf8(this.merchantCityAlternateLanguage, 15)\n : undefined;\n\n const poi = this.isStatic ? \"11\" : \"12\";\n const currencyNumeric = this.currency === \"KHR\" ? \"116\" : \"840\";\n\n // Common defaults in KHQR samples\n const merchantCategoryCode = \"5999\";\n const countryCode = \"KH\";\n\n let payload = \"\";\n payload += tlv(\"00\", \"01\");\n payload += tlv(\"01\", poi);\n\n if (upi) payload += tlv(\"15\", upi);\n\n if (this.merchantType === \"individual\") {\n let t = \"\";\n t += tlv(\"00\", bakongAccountId);\n if (accountInformation) t += tlv(\"01\", accountInformation);\n if (acquiringBank) t += tlv(\"02\", acquiringBank);\n payload += tlv(\"29\", t);\n } else {\n let t = \"\";\n t += tlv(\"00\", bakongAccountId);\n t += tlv(\"01\", merchantId!);\n t += tlv(\"02\", acquiringBank!);\n payload += tlv(\"30\", t);\n }\n\n payload += tlv(\"52\", merchantCategoryCode);\n payload += tlv(\"53\", currencyNumeric);\n if (this.amount != null) payload += tlv(\"54\", this.amount);\n payload += tlv(\"58\", countryCode);\n payload += tlv(\"59\", merchantName);\n payload += tlv(\"60\", merchantCity);\n\n // Tag 62: Additional data (optional)\n {\n let t = \"\";\n if (billNumber) t += tlv(\"01\", billNumber);\n if (mobileNumber) t += tlv(\"02\", mobileNumber);\n if (storeLabel) t += tlv(\"03\", storeLabel);\n if (terminalLabel) t += tlv(\"07\", terminalLabel);\n if (purpose) t += tlv(\"08\", purpose);\n if (t) payload += tlv(\"62\", t);\n }\n\n // Tag 64: Alternate language (optional)\n {\n let t = \"\";\n if (langPref) t += tlv(\"00\", langPref);\n if (nameAlt) t += tlv(\"01\", nameAlt);\n if (cityAlt) t += tlv(\"02\", cityAlt);\n if (t) payload += tlv(\"64\", t);\n }\n\n // Tag 99: Timestamp (dynamic only)\n const timestamp = this.isStatic ? null : String(Date.now());\n if (timestamp) payload += tlv(\"99\", timestamp);\n\n // CRC: Tag 63 (length 04). Compute over payload + \"6304\"\n const crcInput = payload + \"6304\";\n const crc = crc16CcittFalseHex(crcInput);\n const qr = crcInput + crc;\n\n const md5 = crypto.createHash(\"md5\").update(qr, \"utf8\").digest(\"hex\");\n\n return { qr, timestamp, type: this.merchantType, md5 };\n }\n\n static verify(qr: string): boolean {\n return verifyCrc(qr);\n }\n}\n\nexport function verifyCrc(qr: string): boolean {\n const i = qr.lastIndexOf(\"6304\");\n if (i < 0) return false;\n const provided = qr.slice(i + 4, i + 8);\n if (provided.length !== 4) return false;\n\n const crcInput = qr.slice(0, i + 4); // include \"6304\"\n const expected = crc16CcittFalseHex(crcInput);\n return provided.toUpperCase() === expected.toUpperCase();\n}\n\n/** Top-level TLV decoder (does not recursively decode nested templates). */\nexport function decodeTlv(qr: string): Record<string, string> {\n const out: Record<string, string> = {};\n let i = 0;\n while (i + 4 <= qr.length) {\n const tag = qr.slice(i, i + 2);\n const lenStr = qr.slice(i + 2, i + 4);\n if (!/^\\d{2}$/.test(lenStr)) break;\n const len = Number(lenStr);\n const vStart = i + 4;\n const vEnd = vStart + len;\n const value = qr.slice(vStart, vEnd);\n out[tag] = value;\n i = vEnd;\n }\n return out;\n}\n\n// ---------------- helpers ----------------\n\nfunction tlv(tag: string, value: string): string {\n if (!/^\\d{2}$/.test(tag)) throw new Error(`Invalid tag: ${tag}`);\n const len = utf8ByteLength(value);\n if (len > 99) throw new Error(`TLV value too long for tag ${tag}: ${len}`);\n return tag + String(len).padStart(2, \"0\") + value;\n}\n\nfunction utf8ByteLength(s: string): number {\n return Buffer.byteLength(s, \"utf8\");\n}\n\nfunction truncUtf8(s: string, maxBytes: number): string {\n if (utf8ByteLength(s) <= maxBytes) return s;\n let out = \"\";\n for (const ch of s) {\n const next = out + ch;\n if (utf8ByteLength(next) > maxBytes) break;\n out = next;\n }\n return out;\n}\n\n/** CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF) */\nfunction crc16CcittFalseHex(input: string): string {\n const bytes = Buffer.from(input, \"utf8\");\n let crc = 0xffff;\n for (const b of bytes) {\n crc ^= (b & 0xff) << 8;\n for (let i = 0; i < 8; i++) {\n crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);\n crc &= 0xffff;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\nfunction normalizeAmount(v: number | string, currency: \"KHR\" | \"USD\"): string {\n if (typeof v === \"string\") {\n const s = v.trim();\n if (!/^\\d+(\\.\\d+)?$/.test(s)) throw new Error(\"Amount must be numeric\");\n return normalizeAmountString(s, currency);\n }\n\n if (!Number.isFinite(v)) throw new Error(\"Amount must be finite\");\n if (v < 0) throw new Error(\"Amount must be >= 0\");\n return normalizeAmountString(String(v), currency);\n}\n\nfunction normalizeAmountString(s: string, currency: \"KHR\" | \"USD\"): string {\n if (currency === \"USD\") {\n const n = Number(s);\n return n.toFixed(2);\n }\n\n if (!s.includes(\".\")) return s;\n s = s.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n return s.length ? s : \"0\";\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "konthaina-khqr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "KHQR / EMVCo merchant-presented payload generator (Bakong Cambodia) with CRC verification.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"KHQR",
|
|
7
|
+
"Bakong",
|
|
8
|
+
"EMVCo",
|
|
9
|
+
"QR",
|
|
10
|
+
"Cambodia",
|
|
11
|
+
"CRC16",
|
|
12
|
+
"TLV"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"lint": "eslint .",
|
|
39
|
+
"format": "prettier -w ."
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.10.1",
|
|
43
|
+
"eslint": "^9.8.0",
|
|
44
|
+
"eslint-config-prettier": "^9.1.0",
|
|
45
|
+
"prettier": "^3.3.3",
|
|
46
|
+
"tsup": "^8.2.4",
|
|
47
|
+
"typescript": "^5.6.3",
|
|
48
|
+
"vitest": "4.0.18"
|
|
49
|
+
}
|
|
50
|
+
}
|