@laranex/myanmar-payments 4.0.0-dev.3 → 4.0.0-dev.4
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/CHANGELOG.md +7 -0
- package/dist/cjs/aya-pay/aya-pay.d.ts +5 -2
- package/dist/cjs/aya-pay/aya-pay.js +5 -1
- package/dist/cjs/core/amount.d.ts +4 -3
- package/dist/cjs/core/amount.js +13 -4
- package/dist/cjs/core/callback.d.ts +7 -2
- package/dist/cjs/core/callback.js +14 -7
- package/dist/cjs/core/crypto.d.ts +6 -1
- package/dist/cjs/core/crypto.js +17 -3
- package/dist/cjs/core/env.d.ts +7 -0
- package/dist/cjs/core/env.js +14 -2
- package/dist/cjs/core/json.d.ts +2 -2
- package/dist/cjs/core/json.js +3 -3
- package/dist/cjs/core/values.d.ts +2 -0
- package/dist/cjs/core/values.js +5 -0
- package/dist/cjs/cyber-source/cyber-source.d.ts +5 -2
- package/dist/cjs/cyber-source/cyber-source.js +1 -1
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +6 -2
- package/dist/cjs/kbz-pay/kbz-pay.js +1 -1
- package/dist/cjs/kbz-pay/signer.d.ts +4 -1
- package/dist/cjs/kbz-pay/signer.js +7 -2
- package/dist/cjs/wave-money/wave-money.d.ts +6 -3
- package/dist/cjs/wave-money/wave-money.js +5 -3
- package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +5 -2
- package/dist/cjs/yoma-mmqr/yoma-mmqr.js +8 -5
- package/dist/esm/aya-pay/aya-pay.d.ts +5 -2
- package/dist/esm/aya-pay/aya-pay.js +7 -3
- package/dist/esm/core/amount.d.ts +4 -3
- package/dist/esm/core/amount.js +13 -4
- package/dist/esm/core/callback.d.ts +7 -2
- package/dist/esm/core/callback.js +14 -7
- package/dist/esm/core/crypto.d.ts +6 -1
- package/dist/esm/core/crypto.js +17 -3
- package/dist/esm/core/env.d.ts +7 -0
- package/dist/esm/core/env.js +13 -2
- package/dist/esm/core/json.d.ts +2 -2
- package/dist/esm/core/json.js +3 -3
- package/dist/esm/core/values.d.ts +2 -0
- package/dist/esm/core/values.js +4 -0
- package/dist/esm/cyber-source/cyber-source.d.ts +5 -2
- package/dist/esm/cyber-source/cyber-source.js +2 -2
- package/dist/esm/kbz-pay/kbz-pay.d.ts +6 -2
- package/dist/esm/kbz-pay/kbz-pay.js +2 -2
- package/dist/esm/kbz-pay/signer.d.ts +4 -1
- package/dist/esm/kbz-pay/signer.js +8 -3
- package/dist/esm/wave-money/wave-money.d.ts +6 -3
- package/dist/esm/wave-money/wave-money.js +7 -5
- package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +5 -2
- package/dist/esm/yoma-mmqr/yoma-mmqr.js +10 -7
- package/package.json +1 -1
- package/skills/node-myanmar-payments/SKILL.md +21 -20
package/CHANGELOG.md
CHANGED
|
@@ -24,3 +24,10 @@ Initial release. The version number matches the other Laranex Myanmar payments p
|
|
|
24
24
|
|
|
25
25
|
### Changed since the pre-releases
|
|
26
26
|
- `PaymentStatus.Cancelled` is renamed to `PaymentStatus.Canceled` and its value from `'cancelled'` to `'canceled'` (American English), with no alias. Code or stored statuses from the `v4.0.0-dev` pre-releases need the new name; gateway status literals such as Wave Money's `PAYMENT_REQUEST_CANCELLED` are unchanged.
|
|
27
|
+
- Callback values that are JSON numbers are kept as their exact text, as strings, in `raw`, `parsedBody()`, `input()`, `queryInput()` and the `raw` of errors (`1000.50` is `'1000.50'`, not `1000.5`), like the PHP SDK.
|
|
28
|
+
- A nested value (object or array) in a signed or hashed callback field now fails verification: KBZ Pay (any callback field), Wave Money (the hashed fields), AYA Pay (the signed payload fields) and Yoma MMQR (`status`). Before, KBZ Pay skipped it, Wave Money hashed it as `null` and AYA Pay signed it as empty.
|
|
29
|
+
- AYA Pay accepts a base64 `payload` without padding as well as with full padding; partial padding, the URL-safe alphabet, line breaks and payloads that are not UTF-8 are rejected.
|
|
30
|
+
- Yoma MMQR caches its access token under `myanmar-payments.yoma-mmqr.token.<sha256 of base URL and client id>` (was `node-myanmar-payments.…`), the same key as the PHP, Go and Python SDKs, and reads `expires_in` from its leading digits.
|
|
31
|
+
- `Amount.equals()` compares by value, ignoring leading zeros too (`'01000'` equals `Amount.kyat(1000)`); text that is not plain digits with an optional fraction is never equal.
|
|
32
|
+
- `CallbackRequest.rawBody` holds the exact bytes received (a string body is encoded as UTF-8), next to `body` as text, like Python's `raw_body` and Go's `Body`.
|
|
33
|
+
- A config's `sandbox` option also takes a string, read like the `*_SANDBOX` variable (`false`, `0`, `f`, `no` or `off` select production).
|
|
@@ -9,8 +9,11 @@ export interface AyaPayConfigOptions {
|
|
|
9
9
|
appKey: string;
|
|
10
10
|
/** The secret that signs requests and verifies callbacks. */
|
|
11
11
|
appSecret: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Use the UAT environment (default `true`).
|
|
14
|
+
* Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
|
|
15
|
+
*/
|
|
16
|
+
sandbox?: boolean | string | undefined;
|
|
14
17
|
/** Overrides the gateway base URL. */
|
|
15
18
|
baseUrl?: string | undefined;
|
|
16
19
|
}
|
|
@@ -26,7 +26,7 @@ class AyaPayConfig {
|
|
|
26
26
|
constructor(options) {
|
|
27
27
|
this.appKey = (0, env_js_1.requireSetting)('aya_pay', 'app_key', options.appKey);
|
|
28
28
|
this.appSecret = (0, env_js_1.requireSetting)('aya_pay', 'app_secret', options.appSecret);
|
|
29
|
-
this.sandbox = options.sandbox
|
|
29
|
+
this.sandbox = (0, env_js_1.parseSandbox)(options.sandbox);
|
|
30
30
|
this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
|
|
31
31
|
(this.sandbox ? AyaPayConfig.SANDBOX_URL : AyaPayConfig.PRODUCTION_URL));
|
|
32
32
|
}
|
|
@@ -256,6 +256,10 @@ class AyaPay {
|
|
|
256
256
|
for (const field of PAYLOAD_FIELDS) {
|
|
257
257
|
const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
|
|
258
258
|
if (Object.prototype.hasOwnProperty.call(payload, key)) {
|
|
259
|
+
// A nested value is never signed, so a payload carrying one is rejected.
|
|
260
|
+
if ((0, values_js_1.isNested)(payload[key])) {
|
|
261
|
+
throw fail();
|
|
262
|
+
}
|
|
259
263
|
parts.push((0, values_js_1.scalarString)(payload[key]) ?? '');
|
|
260
264
|
}
|
|
261
265
|
}
|
|
@@ -43,9 +43,10 @@ export declare class Amount {
|
|
|
43
43
|
/** Whether the amount is greater than zero. */
|
|
44
44
|
isPositive(): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Whether both amounts have the same value
|
|
47
|
-
* (`1000`, `
|
|
48
|
-
*
|
|
46
|
+
* Whether both amounts have the same value. Leading zeros of the whole part and trailing zeros of
|
|
47
|
+
* the fraction are ignored (`1000`, `01000` and `1000.00` are equal). Text must be plain digits
|
|
48
|
+
* with an optional fraction, so `1,000` or ` 1000` is never equal, and neither is `null` or
|
|
49
|
+
* `undefined` (a gateway amount that was not sent never matches).
|
|
49
50
|
*/
|
|
50
51
|
equals(other: Amount | string | null | undefined): boolean;
|
|
51
52
|
}
|
package/dist/cjs/core/amount.js
CHANGED
|
@@ -88,20 +88,29 @@ class Amount {
|
|
|
88
88
|
return !this.isZero();
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* Whether both amounts have the same value
|
|
92
|
-
* (`1000`, `
|
|
93
|
-
*
|
|
91
|
+
* Whether both amounts have the same value. Leading zeros of the whole part and trailing zeros of
|
|
92
|
+
* the fraction are ignored (`1000`, `01000` and `1000.00` are equal). Text must be plain digits
|
|
93
|
+
* with an optional fraction, so `1,000` or ` 1000` is never equal, and neither is `null` or
|
|
94
|
+
* `undefined` (a gateway amount that was not sent never matches).
|
|
94
95
|
*/
|
|
95
96
|
equals(other) {
|
|
96
97
|
if (other === undefined || other === null) {
|
|
97
98
|
return false;
|
|
98
99
|
}
|
|
99
|
-
const normalize = (value) => value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value;
|
|
100
100
|
const text = other instanceof Amount ? other.value : other;
|
|
101
|
+
if (typeof text !== 'string' || !PATTERN.test(text)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
101
104
|
return normalize(this.value) === normalize(text);
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
exports.Amount = Amount;
|
|
108
|
+
function normalize(value) {
|
|
109
|
+
const [whole = '', fraction = ''] = value.split('.');
|
|
110
|
+
const trimmedWhole = whole.replace(/^0+/, '') || '0';
|
|
111
|
+
const trimmedFraction = fraction.replace(/0+$/, '');
|
|
112
|
+
return trimmedFraction === '' ? trimmedWhole : `${trimmedWhole}.${trimmedFraction}`;
|
|
113
|
+
}
|
|
105
114
|
function invalid(message) {
|
|
106
115
|
return new errors_js_1.InvalidPaymentDataError({ amount: message });
|
|
107
116
|
}
|
|
@@ -42,8 +42,10 @@ export interface WebRequestLike {
|
|
|
42
42
|
* {@link CallbackRequest.fromNodeRequest} or {@link CallbackRequest.fromWebRequest}.
|
|
43
43
|
*/
|
|
44
44
|
export declare class CallbackRequest {
|
|
45
|
-
/** The
|
|
45
|
+
/** The request body as text: a string body as given, bytes decoded as UTF-8. */
|
|
46
46
|
readonly body: string;
|
|
47
|
+
/** The exact bytes received. A string body is encoded as UTF-8. */
|
|
48
|
+
readonly rawBody: Uint8Array;
|
|
47
49
|
/** The request headers, with lowercase names. Repeated headers are joined with `, `. */
|
|
48
50
|
readonly headers: Readonly<Record<string, string>>;
|
|
49
51
|
/** The query string parameters (the first value of each). */
|
|
@@ -71,7 +73,10 @@ export declare class CallbackRequest {
|
|
|
71
73
|
static fromWebRequest(request: WebRequestLike): Promise<CallbackRequest>;
|
|
72
74
|
/** The named header, matched case-insensitively, or `undefined`. */
|
|
73
75
|
header(name: string): string | undefined;
|
|
74
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* The body decoded as JSON or as a urlencoded form. JSON numbers keep their exact text, as strings
|
|
78
|
+
* (`1000.50` stays `"1000.50"`).
|
|
79
|
+
*/
|
|
75
80
|
parsedBody(): Record<string, unknown>;
|
|
76
81
|
/** The parsed body merged over the query string. */
|
|
77
82
|
input(): Record<string, unknown>;
|
|
@@ -11,14 +11,17 @@ const json_js_1 = require("./json.js");
|
|
|
11
11
|
* {@link CallbackRequest.fromNodeRequest} or {@link CallbackRequest.fromWebRequest}.
|
|
12
12
|
*/
|
|
13
13
|
class CallbackRequest {
|
|
14
|
-
/** The
|
|
14
|
+
/** The request body as text: a string body as given, bytes decoded as UTF-8. */
|
|
15
15
|
body;
|
|
16
|
+
/** The exact bytes received. A string body is encoded as UTF-8. */
|
|
17
|
+
rawBody;
|
|
16
18
|
/** The request headers, with lowercase names. Repeated headers are joined with `, `. */
|
|
17
19
|
headers;
|
|
18
20
|
/** The query string parameters (the first value of each). */
|
|
19
21
|
query;
|
|
20
22
|
constructor(init = {}) {
|
|
21
|
-
this.
|
|
23
|
+
this.rawBody = bodyBytes(init.body);
|
|
24
|
+
this.body = typeof init.body === 'string' ? init.body : new TextDecoder().decode(this.rawBody);
|
|
22
25
|
this.headers = Object.freeze(normalizeHeaders(init.headers));
|
|
23
26
|
this.query = Object.freeze(normalizeQuery(init.query));
|
|
24
27
|
}
|
|
@@ -74,7 +77,10 @@ class CallbackRequest {
|
|
|
74
77
|
header(name) {
|
|
75
78
|
return this.headers[name.toLowerCase()];
|
|
76
79
|
}
|
|
77
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* The body decoded as JSON or as a urlencoded form. JSON numbers keep their exact text, as strings
|
|
82
|
+
* (`1000.50` stays `"1000.50"`).
|
|
83
|
+
*/
|
|
78
84
|
parsedBody() {
|
|
79
85
|
return (0, json_js_1.toPlainObject)(losslessBody(this));
|
|
80
86
|
}
|
|
@@ -217,14 +223,15 @@ class PaymentStatusResult {
|
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
exports.PaymentStatusResult = PaymentStatusResult;
|
|
220
|
-
function
|
|
226
|
+
function bodyBytes(body) {
|
|
221
227
|
if (body === undefined || body === null) {
|
|
222
|
-
return
|
|
228
|
+
return new Uint8Array();
|
|
223
229
|
}
|
|
224
230
|
if (typeof body === 'string') {
|
|
225
|
-
return body;
|
|
231
|
+
return new TextEncoder().encode(body);
|
|
226
232
|
}
|
|
227
|
-
|
|
233
|
+
// A copy, so later changes to the caller's buffer cannot alter what was verified.
|
|
234
|
+
return body instanceof ArrayBuffer ? new Uint8Array(body.slice(0)) : Uint8Array.from(body);
|
|
228
235
|
}
|
|
229
236
|
function rawBody(value) {
|
|
230
237
|
if (typeof value === 'string' || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
@@ -15,5 +15,10 @@ export declare function randomHex(bytes?: number): string;
|
|
|
15
15
|
* @internal
|
|
16
16
|
*/
|
|
17
17
|
export declare function queryEscape(value: string): string;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Decodes standard base64 (`A-Z a-z 0-9 + /`), either correctly padded or without any padding, to
|
|
20
|
+
* UTF-8 text. Partial padding, other characters and bytes that are not UTF-8 return `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
19
24
|
export declare function decodeBase64(value: string): string | undefined;
|
package/dist/cjs/core/crypto.js
CHANGED
|
@@ -41,10 +41,24 @@ function queryEscape(value) {
|
|
|
41
41
|
.replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
42
42
|
.replace(/%20/g, '+');
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
const UTF8 = new TextDecoder('utf-8', { fatal: true });
|
|
45
|
+
/**
|
|
46
|
+
* Decodes standard base64 (`A-Z a-z 0-9 + /`), either correctly padded or without any padding, to
|
|
47
|
+
* UTF-8 text. Partial padding, other characters and bytes that are not UTF-8 return `undefined`.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
45
51
|
function decodeBase64(value) {
|
|
46
|
-
if (
|
|
52
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (value.includes('=') ? value.length % 4 !== 0 : value.length % 4 === 1) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return UTF8.decode(Buffer.from(value, 'base64'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
47
62
|
return undefined;
|
|
48
63
|
}
|
|
49
|
-
return Buffer.from(value, 'base64').toString('utf8');
|
|
50
64
|
}
|
package/dist/cjs/core/env.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare function envFirst(env: EnvSource, ...keys: string[]): string;
|
|
|
9
9
|
* @internal
|
|
10
10
|
*/
|
|
11
11
|
export declare function envSandbox(env: EnvSource, key: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* A `sandbox` setting: booleans as is, text parsed like a `*_SANDBOX` variable (`false`, `0`, `f`,
|
|
14
|
+
* `no` and `off` in any case select production; anything else means sandbox), unset means sandbox.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseSandbox(value: boolean | string | undefined | null): boolean;
|
|
12
19
|
/** An integer variable, or `undefined` when unset or not an integer. @internal */
|
|
13
20
|
export declare function envInt(env: EnvSource, key: string): number | undefined;
|
|
14
21
|
/** The trimmed value, or throws a {@link ConfigurationError} naming `key`. @internal */
|
package/dist/cjs/core/env.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.envFirst = envFirst;
|
|
4
4
|
exports.envSandbox = envSandbox;
|
|
5
|
+
exports.parseSandbox = parseSandbox;
|
|
5
6
|
exports.envInt = envInt;
|
|
6
7
|
exports.requireSetting = requireSetting;
|
|
7
8
|
exports.optionalSetting = optionalSetting;
|
|
@@ -25,8 +26,19 @@ function envFirst(env, ...keys) {
|
|
|
25
26
|
* @internal
|
|
26
27
|
*/
|
|
27
28
|
function envSandbox(env, key) {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
return parseSandbox(env[key] ?? '');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A `sandbox` setting: booleans as is, text parsed like a `*_SANDBOX` variable (`false`, `0`, `f`,
|
|
33
|
+
* `no` and `off` in any case select production; anything else means sandbox), unset means sandbox.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
function parseSandbox(value) {
|
|
38
|
+
if (typeof value === 'boolean') {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return !['false', '0', 'f', 'no', 'off'].includes((value ?? '').trim().toLowerCase());
|
|
30
42
|
}
|
|
31
43
|
/** An integer variable, or `undefined` when unset or not an integer. @internal */
|
|
32
44
|
function envInt(env, key) {
|
package/dist/cjs/core/json.d.ts
CHANGED
|
@@ -32,8 +32,8 @@ export declare function parseJsonObject(text: string): LosslessObject | undefine
|
|
|
32
32
|
/** @internal */
|
|
33
33
|
export declare function isLosslessObject(value: unknown): value is LosslessObject;
|
|
34
34
|
/**
|
|
35
|
-
* Converts a lossless value to plain JavaScript values
|
|
36
|
-
*
|
|
35
|
+
* Converts a lossless value to plain JavaScript values for the `raw` fields of results. Numbers
|
|
36
|
+
* become their exact text as a string (`1000.50` stays `"1000.50"`), so no precision is lost.
|
|
37
37
|
*
|
|
38
38
|
* @internal
|
|
39
39
|
*/
|
package/dist/cjs/core/json.js
CHANGED
|
@@ -56,14 +56,14 @@ function isLosslessObject(value) {
|
|
|
56
56
|
!(value instanceof JsonNumber));
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Converts a lossless value to plain JavaScript values
|
|
60
|
-
*
|
|
59
|
+
* Converts a lossless value to plain JavaScript values for the `raw` fields of results. Numbers
|
|
60
|
+
* become their exact text as a string (`1000.50` stays `"1000.50"`), so no precision is lost.
|
|
61
61
|
*
|
|
62
62
|
* @internal
|
|
63
63
|
*/
|
|
64
64
|
function toPlain(value) {
|
|
65
65
|
if (value instanceof JsonNumber) {
|
|
66
|
-
return
|
|
66
|
+
return value.text;
|
|
67
67
|
}
|
|
68
68
|
if (Array.isArray(value)) {
|
|
69
69
|
return value.map(toPlain);
|
|
@@ -14,3 +14,5 @@ export declare function optional(source: Record<string, unknown>, key: string):
|
|
|
14
14
|
export declare function trimmed(source: Record<string, unknown>, key: string): string;
|
|
15
15
|
/** The value at `key` when it is an object (not an array). @internal */
|
|
16
16
|
export declare function object(source: Record<string, unknown>, key: string): LosslessObject | undefined;
|
|
17
|
+
/** Whether `value` is an object or array, which a gateway never signs. @internal */
|
|
18
|
+
export declare function isNested(value: unknown): boolean;
|
package/dist/cjs/core/values.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.get = get;
|
|
|
5
5
|
exports.optional = optional;
|
|
6
6
|
exports.trimmed = trimmed;
|
|
7
7
|
exports.object = object;
|
|
8
|
+
exports.isNested = isNested;
|
|
8
9
|
const json_js_1 = require("./json.js");
|
|
9
10
|
/**
|
|
10
11
|
* Returns a scalar as the text a gateway signs: strings as is, numbers exactly as sent, booleans as
|
|
@@ -43,3 +44,7 @@ function object(source, key) {
|
|
|
43
44
|
const value = source[key];
|
|
44
45
|
return (0, json_js_1.isLosslessObject)(value) ? value : undefined;
|
|
45
46
|
}
|
|
47
|
+
/** Whether `value` is an object or array, which a gateway never signs. @internal */
|
|
48
|
+
function isNested(value) {
|
|
49
|
+
return typeof value === 'object' && value !== null && !(value instanceof json_js_1.JsonNumber);
|
|
50
|
+
}
|
|
@@ -10,8 +10,11 @@ export interface CyberSourceConfigOptions {
|
|
|
10
10
|
accessKey: string;
|
|
11
11
|
/** The profile's secret key, which signs the fields. */
|
|
12
12
|
secretKey: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Use the test environment (default `true`).
|
|
15
|
+
* Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
|
|
16
|
+
*/
|
|
17
|
+
sandbox?: boolean | string | undefined;
|
|
15
18
|
/** Overrides the Secure Acceptance base URL. */
|
|
16
19
|
baseUrl?: string | undefined;
|
|
17
20
|
}
|
|
@@ -26,7 +26,7 @@ class CyberSourceConfig {
|
|
|
26
26
|
this.profileId = (0, env_js_1.requireSetting)('cyber_source', 'profile_id', options.profileId);
|
|
27
27
|
this.accessKey = (0, env_js_1.requireSetting)('cyber_source', 'access_key', options.accessKey);
|
|
28
28
|
this.secretKey = (0, env_js_1.requireSetting)('cyber_source', 'secret_key', options.secretKey);
|
|
29
|
-
this.sandbox = options.sandbox
|
|
29
|
+
this.sandbox = (0, env_js_1.parseSandbox)(options.sandbox);
|
|
30
30
|
this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
|
|
31
31
|
(this.sandbox ? CyberSourceConfig.SANDBOX_URL : CyberSourceConfig.PRODUCTION_URL));
|
|
32
32
|
}
|
|
@@ -12,8 +12,12 @@ export interface KbzPayConfigOptions {
|
|
|
12
12
|
appKey: string;
|
|
13
13
|
/** The `merch_code` KBZ issued. */
|
|
14
14
|
merchantCode: string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Use the UAT endpoints (default `true`). UAT and production issue separate
|
|
17
|
+
* credentials. Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off`
|
|
18
|
+
* select production.
|
|
19
|
+
*/
|
|
20
|
+
sandbox?: boolean | string | undefined;
|
|
17
21
|
/** Overrides the API base URL, e.g. to go through a proxy. */
|
|
18
22
|
apiUrl?: string | undefined;
|
|
19
23
|
/** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
|
|
@@ -32,7 +32,7 @@ class KbzPayConfig {
|
|
|
32
32
|
this.appId = (0, env_js_1.requireSetting)('kbz_pay', 'app_id', options.appId);
|
|
33
33
|
this.appKey = (0, env_js_1.requireSetting)('kbz_pay', 'app_key', options.appKey);
|
|
34
34
|
this.merchantCode = (0, env_js_1.requireSetting)('kbz_pay', 'merchant_code', options.merchantCode);
|
|
35
|
-
this.sandbox = options.sandbox
|
|
35
|
+
this.sandbox = (0, env_js_1.parseSandbox)(options.sandbox);
|
|
36
36
|
this.apiUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.apiUrl) ??
|
|
37
37
|
(this.sandbox ? KbzPayConfig.SANDBOX_API_URL : KbzPayConfig.PRODUCTION_API_URL));
|
|
38
38
|
this.pwaUrl = `${(0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.pwaUrl) ??
|
|
@@ -10,6 +10,9 @@ export declare class KbzPaySigner {
|
|
|
10
10
|
signString(fields: Readonly<Record<string, unknown>>): string;
|
|
11
11
|
/** The uppercase SHA-256 signature of `fields`. */
|
|
12
12
|
sign(fields: Readonly<Record<string, unknown>>): string;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
|
|
15
|
+
* fields holding an object or array fail rather than being partly trusted.
|
|
16
|
+
*/
|
|
14
17
|
verify(fields: Readonly<Record<string, unknown>>): boolean;
|
|
15
18
|
}
|
|
@@ -27,10 +27,15 @@ class KbzPaySigner {
|
|
|
27
27
|
sign(fields) {
|
|
28
28
|
return (0, crypto_js_1.sha256Hex)(`${this.signString(fields)}&key=${this.appKey}`).toUpperCase();
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
|
|
32
|
+
* fields holding an object or array fail rather than being partly trusted.
|
|
33
|
+
*/
|
|
31
34
|
verify(fields) {
|
|
32
35
|
const sign = fields.sign;
|
|
33
|
-
return typeof sign === 'string' &&
|
|
36
|
+
return (typeof sign === 'string' &&
|
|
37
|
+
!Object.values(fields).some(values_js_1.isNested) &&
|
|
38
|
+
(0, crypto_js_1.safeEqual)(this.sign(fields), sign.toUpperCase()));
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
exports.KbzPaySigner = KbzPaySigner;
|
|
@@ -13,8 +13,11 @@ export interface WaveMoneyConfigOptions {
|
|
|
13
13
|
merchantName: string;
|
|
14
14
|
/** How long the customer has to pay, in seconds (default 300). */
|
|
15
15
|
timeToLiveSeconds?: number | undefined;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Use the test environment (default `true`).
|
|
18
|
+
* Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
|
|
19
|
+
*/
|
|
20
|
+
sandbox?: boolean | string | undefined;
|
|
18
21
|
/** Overrides the API base URL. */
|
|
19
22
|
baseUrl?: string | undefined;
|
|
20
23
|
/** Overrides the host the customer is redirected to. Wave serves it without the API port. */
|
|
@@ -104,7 +107,7 @@ export declare class WaveMoney {
|
|
|
104
107
|
/**
|
|
105
108
|
* Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
|
|
106
109
|
* back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
|
|
107
|
-
* optional.
|
|
110
|
+
* optional. A hashed field holding an object or array fails verification.
|
|
108
111
|
*/
|
|
109
112
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
110
113
|
private hash;
|
|
@@ -40,7 +40,7 @@ class WaveMoneyConfig {
|
|
|
40
40
|
ttl !== undefined && Number.isInteger(ttl) && ttl > 0
|
|
41
41
|
? ttl
|
|
42
42
|
: WaveMoneyConfig.DEFAULT_TIME_TO_LIVE_SECONDS;
|
|
43
|
-
this.sandbox = options.sandbox
|
|
43
|
+
this.sandbox = (0, env_js_1.parseSandbox)(options.sandbox);
|
|
44
44
|
this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
|
|
45
45
|
(this.sandbox ? WaveMoneyConfig.SANDBOX_URL : WaveMoneyConfig.PRODUCTION_URL));
|
|
46
46
|
this.authenticateUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.authenticateUrl) ??
|
|
@@ -209,13 +209,15 @@ class WaveMoney {
|
|
|
209
209
|
/**
|
|
210
210
|
* Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
|
|
211
211
|
* back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
|
|
212
|
-
* optional.
|
|
212
|
+
* optional. A hashed field holding an object or array fails verification.
|
|
213
213
|
*/
|
|
214
214
|
handleCallback(request) {
|
|
215
215
|
const payload = (0, callback_js_1.losslessBody)(request);
|
|
216
216
|
const expected = this.hash(CALLBACK_FIELDS.map((field) => (0, values_js_1.scalarString)(payload[field]) ?? 'null'));
|
|
217
217
|
const hashValue = payload.hashValue;
|
|
218
|
-
if (typeof hashValue !== 'string' ||
|
|
218
|
+
if (typeof hashValue !== 'string' ||
|
|
219
|
+
CALLBACK_FIELDS.some((field) => (0, values_js_1.isNested)(payload[field])) ||
|
|
220
|
+
!(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
|
|
219
221
|
throw new errors_js_1.SignatureVerificationError('Wave Money callback hash verification failed.', (0, json_js_1.toPlainObject)(payload));
|
|
220
222
|
}
|
|
221
223
|
const gatewayStatus = (0, values_js_1.trimmed)(payload, 'status');
|
|
@@ -16,8 +16,11 @@ export interface YomaMmqrConfigOptions {
|
|
|
16
16
|
webhookHashKey: string;
|
|
17
17
|
/** The secret you shared with Yoma; when set, callbacks must carry it in `X-Webhook-Secret`. */
|
|
18
18
|
webhookSecret?: string | undefined;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Use the UAT payment hub (default `true`).
|
|
21
|
+
* Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
|
|
22
|
+
*/
|
|
23
|
+
sandbox?: boolean | string | undefined;
|
|
21
24
|
/** Overrides the API base URL. */
|
|
22
25
|
baseUrl?: string | undefined;
|
|
23
26
|
/** The `{version}` segment of the API paths (default `v1rc`). */
|
|
@@ -35,7 +35,7 @@ class YomaMmqrConfig {
|
|
|
35
35
|
this.clientSecret = (0, env_js_1.requireSetting)('yoma_mmqr', 'client_secret', options.clientSecret);
|
|
36
36
|
this.webhookHashKey = (0, env_js_1.requireSetting)('yoma_mmqr', 'webhook_hashkey', options.webhookHashKey);
|
|
37
37
|
this.webhookSecret = (0, env_js_1.optionalSetting)(options.webhookSecret);
|
|
38
|
-
this.sandbox = options.sandbox
|
|
38
|
+
this.sandbox = (0, env_js_1.parseSandbox)(options.sandbox);
|
|
39
39
|
this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
|
|
40
40
|
(this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
|
|
41
41
|
this.apiVersion = (0, env_js_1.optionalSetting)(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
|
|
@@ -161,7 +161,9 @@ class YomaMmqr {
|
|
|
161
161
|
const orderNumber = (0, values_js_1.get)(payload, 'orderNumber');
|
|
162
162
|
const status = (0, values_js_1.trimmed)(payload, 'status');
|
|
163
163
|
const expected = (0, crypto_js_1.hmacSha256Hex)(orderNumber + this.config.webhookHashKey, `orderNumber=${orderNumber}&status=${status}`);
|
|
164
|
-
if (orderNumber === '' ||
|
|
164
|
+
if (orderNumber === '' ||
|
|
165
|
+
(0, values_js_1.isNested)(payload.status) ||
|
|
166
|
+
!(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'hashValue').toLowerCase())) {
|
|
165
167
|
throw new errors_js_1.SignatureVerificationError('Yoma MMQR callback hash verification failed.', (0, json_js_1.toPlainObject)(payload));
|
|
166
168
|
}
|
|
167
169
|
return new callback_js_1.PaymentCallback({
|
|
@@ -210,13 +212,14 @@ class YomaMmqr {
|
|
|
210
212
|
if (!response.successful() || token === '') {
|
|
211
213
|
throw apiError('token', response.status, body, (0, values_js_1.get)(body, 'error'));
|
|
212
214
|
}
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
+
// The leading digits of `expires_in`; missing, non-numeric or 0 means an hour.
|
|
216
|
+
const expiresIn = Number(/^\d+/.exec((0, values_js_1.get)(body, 'expires_in'))?.[0] ?? 0);
|
|
217
|
+
const ttl = Math.max(60, (expiresIn > 0 ? expiresIn : 3600) - 60);
|
|
215
218
|
await this.cache.set(this.tokenCacheKey(), token, ttl);
|
|
216
219
|
return token;
|
|
217
220
|
}
|
|
218
221
|
tokenCacheKey() {
|
|
219
|
-
return `
|
|
222
|
+
return `myanmar-payments.yoma-mmqr.token.${(0, crypto_js_1.sha256Hex)(`${this.config.baseUrl}|${this.config.clientId}`)}`;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
exports.YomaMmqr = YomaMmqr;
|
|
@@ -9,8 +9,11 @@ export interface AyaPayConfigOptions {
|
|
|
9
9
|
appKey: string;
|
|
10
10
|
/** The secret that signs requests and verifies callbacks. */
|
|
11
11
|
appSecret: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Use the UAT environment (default `true`).
|
|
14
|
+
* Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
|
|
15
|
+
*/
|
|
16
|
+
sandbox?: boolean | string | undefined;
|
|
14
17
|
/** Overrides the gateway base URL. */
|
|
15
18
|
baseUrl?: string | undefined;
|
|
16
19
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {} from '../core/amount.js';
|
|
2
2
|
import { CallbackRequest, losslessInput, losslessQueryInput, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
|
|
3
3
|
import { decodeBase64, hmacSha256Hex, safeEqual } from '../core/crypto.js';
|
|
4
|
-
import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
|
|
4
|
+
import { defaultEnv, envFirst, envSandbox, parseSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
|
|
5
5
|
import { ApiError, SignatureVerificationError } from '../core/errors.js';
|
|
6
6
|
import { httpClientFrom, Transport, } from '../core/http.js';
|
|
7
7
|
import { isLosslessObject, parseJsonObject, toPlainObject, } from '../core/json.js';
|
|
8
8
|
import { FormPayment } from '../core/results.js';
|
|
9
9
|
import { resolveStatus } from '../core/status.js';
|
|
10
10
|
import { toAmount, Validator } from '../core/validate.js';
|
|
11
|
-
import { get, object, optional, scalarString, trimmed } from '../core/values.js';
|
|
11
|
+
import { get, isNested, object, optional, scalarString, trimmed } from '../core/values.js';
|
|
12
12
|
/**
|
|
13
13
|
* AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
|
|
14
14
|
* `ConfigurationError`.
|
|
@@ -24,7 +24,7 @@ export class AyaPayConfig {
|
|
|
24
24
|
constructor(options) {
|
|
25
25
|
this.appKey = requireSetting('aya_pay', 'app_key', options.appKey);
|
|
26
26
|
this.appSecret = requireSetting('aya_pay', 'app_secret', options.appSecret);
|
|
27
|
-
this.sandbox = options.sandbox
|
|
27
|
+
this.sandbox = parseSandbox(options.sandbox);
|
|
28
28
|
this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
|
|
29
29
|
(this.sandbox ? AyaPayConfig.SANDBOX_URL : AyaPayConfig.PRODUCTION_URL));
|
|
30
30
|
}
|
|
@@ -252,6 +252,10 @@ export class AyaPay {
|
|
|
252
252
|
for (const field of PAYLOAD_FIELDS) {
|
|
253
253
|
const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
|
|
254
254
|
if (Object.prototype.hasOwnProperty.call(payload, key)) {
|
|
255
|
+
// A nested value is never signed, so a payload carrying one is rejected.
|
|
256
|
+
if (isNested(payload[key])) {
|
|
257
|
+
throw fail();
|
|
258
|
+
}
|
|
255
259
|
parts.push(scalarString(payload[key]) ?? '');
|
|
256
260
|
}
|
|
257
261
|
}
|
|
@@ -43,9 +43,10 @@ export declare class Amount {
|
|
|
43
43
|
/** Whether the amount is greater than zero. */
|
|
44
44
|
isPositive(): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Whether both amounts have the same value
|
|
47
|
-
* (`1000`, `
|
|
48
|
-
*
|
|
46
|
+
* Whether both amounts have the same value. Leading zeros of the whole part and trailing zeros of
|
|
47
|
+
* the fraction are ignored (`1000`, `01000` and `1000.00` are equal). Text must be plain digits
|
|
48
|
+
* with an optional fraction, so `1,000` or ` 1000` is never equal, and neither is `null` or
|
|
49
|
+
* `undefined` (a gateway amount that was not sent never matches).
|
|
49
50
|
*/
|
|
50
51
|
equals(other: Amount | string | null | undefined): boolean;
|
|
51
52
|
}
|
package/dist/esm/core/amount.js
CHANGED
|
@@ -85,19 +85,28 @@ export class Amount {
|
|
|
85
85
|
return !this.isZero();
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* Whether both amounts have the same value
|
|
89
|
-
* (`1000`, `
|
|
90
|
-
*
|
|
88
|
+
* Whether both amounts have the same value. Leading zeros of the whole part and trailing zeros of
|
|
89
|
+
* the fraction are ignored (`1000`, `01000` and `1000.00` are equal). Text must be plain digits
|
|
90
|
+
* with an optional fraction, so `1,000` or ` 1000` is never equal, and neither is `null` or
|
|
91
|
+
* `undefined` (a gateway amount that was not sent never matches).
|
|
91
92
|
*/
|
|
92
93
|
equals(other) {
|
|
93
94
|
if (other === undefined || other === null) {
|
|
94
95
|
return false;
|
|
95
96
|
}
|
|
96
|
-
const normalize = (value) => value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value;
|
|
97
97
|
const text = other instanceof Amount ? other.value : other;
|
|
98
|
+
if (typeof text !== 'string' || !PATTERN.test(text)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
98
101
|
return normalize(this.value) === normalize(text);
|
|
99
102
|
}
|
|
100
103
|
}
|
|
104
|
+
function normalize(value) {
|
|
105
|
+
const [whole = '', fraction = ''] = value.split('.');
|
|
106
|
+
const trimmedWhole = whole.replace(/^0+/, '') || '0';
|
|
107
|
+
const trimmedFraction = fraction.replace(/0+$/, '');
|
|
108
|
+
return trimmedFraction === '' ? trimmedWhole : `${trimmedWhole}.${trimmedFraction}`;
|
|
109
|
+
}
|
|
101
110
|
function invalid(message) {
|
|
102
111
|
return new InvalidPaymentDataError({ amount: message });
|
|
103
112
|
}
|