@laranex/myanmar-payments 4.0.0-dev.2 → 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 +13 -1
- package/README.md +1 -1
- package/dist/cjs/aya-pay/aya-pay.d.ts +5 -2
- package/dist/cjs/aya-pay/aya-pay.js +7 -2
- package/dist/cjs/core/amount.d.ts +4 -3
- package/dist/cjs/core/amount.js +13 -4
- package/dist/cjs/core/callback.d.ts +14 -8
- package/dist/cjs/core/callback.js +19 -11
- 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/status.d.ts +3 -3
- package/dist/cjs/core/status.js +2 -2
- 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 +7 -3
- package/dist/cjs/cyber-source/cyber-source.js +27 -13
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +6 -2
- package/dist/cjs/kbz-pay/kbz-pay.js +2 -2
- 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 +6 -4
- 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 +9 -4
- package/dist/esm/core/amount.d.ts +4 -3
- package/dist/esm/core/amount.js +13 -4
- package/dist/esm/core/callback.d.ts +14 -8
- package/dist/esm/core/callback.js +19 -11
- 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/status.d.ts +3 -3
- package/dist/esm/core/status.js +2 -2
- 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 +7 -3
- package/dist/esm/cyber-source/cyber-source.js +29 -15
- package/dist/esm/kbz-pay/kbz-pay.d.ts +6 -2
- package/dist/esm/kbz-pay/kbz-pay.js +3 -3
- 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 +8 -6
- 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 +22 -20
package/CHANGELOG.md
CHANGED
|
@@ -12,10 +12,22 @@ Initial release. The version number matches the other Laranex Myanmar payments p
|
|
|
12
12
|
- Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole-number `number` or `bigint` and rejects fractions. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
|
|
13
13
|
- A config class per gateway with `fromEnv(process.env)`, reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel and Go packages; a missing credential throws `ConfigurationError`. `MyanmarPayments` builds every gateway lazily from one config object or the environment.
|
|
14
14
|
- Result classes: `RedirectPayment`, `FormPayment` (with `toHtml()`), `QrPayment` (with `qrImageDataUri()`) and `AppPayment`; `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, sent with `send(res)` or `toResponse()`) and `PaymentStatusResult`.
|
|
15
|
-
- `CallbackRequest.fromNodeRequest()` (node:http, Express
|
|
15
|
+
- `CallbackRequest.fromNodeRequest()` (node:http, Express), `CallbackRequest.fromWebRequest()` (Fetch API servers such as Next.js, Hono and Bun), `CallbackRequest.from()` and `CallbackRequest.fromJson()`. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text.
|
|
16
16
|
- Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause`), `SignatureVerificationError` and `ConfigurationError`.
|
|
17
17
|
- No runtime dependencies: the global `fetch` (or an injected `fetch` / `HttpClient`, 30 second default timeout, `AbortSignal` per call) and `node:crypto`; an injectable `TokenCache` with an in-memory default for Yoma's access token.
|
|
18
|
+
- CyberSource callbacks trust only the fields listed in `signed_field_names`: `decision` and `req_reference_number` must be signed, and unsigned fields are left out of the result and `raw`.
|
|
19
|
+
- AYA Pay reads a `payload` whose `+` signs became spaces in an unencoded return query string.
|
|
18
20
|
- Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
|
|
19
21
|
- Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
|
|
20
22
|
- Agent skill in `skills/node-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/node-myanmar-payments`.
|
|
21
23
|
- Requires Node.js 20 or higher.
|
|
24
|
+
|
|
25
|
+
### Changed since the pre-releases
|
|
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).
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Node Myanmar Payments
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@laranex/myanmar-payments)
|
|
4
|
-
[](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml)
|
|
5
5
|
[](LICENSE.md)
|
|
6
6
|
|
|
7
7
|
Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed TypeScript API, ESM and CommonJS, no runtime dependencies. Built for humans and AI agents.
|
|
@@ -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
|
}
|
|
@@ -246,7 +246,8 @@ class AyaPay {
|
|
|
246
246
|
/** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
|
|
247
247
|
verifiedPayload(input, context) {
|
|
248
248
|
const fail = () => new errors_js_1.SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, (0, json_js_1.toPlainObject)(input));
|
|
249
|
-
|
|
249
|
+
// Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
|
|
250
|
+
const json = (0, crypto_js_1.decodeBase64)((0, values_js_1.get)(input, 'payload').replace(/ /g, '+'));
|
|
250
251
|
const payload = json === undefined ? undefined : (0, json_js_1.parseJsonObject)(json);
|
|
251
252
|
if (payload === undefined) {
|
|
252
253
|
throw fail();
|
|
@@ -255,6 +256,10 @@ class AyaPay {
|
|
|
255
256
|
for (const field of PAYLOAD_FIELDS) {
|
|
256
257
|
const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
|
|
257
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
|
+
}
|
|
258
263
|
parts.push((0, values_js_1.scalarString)(payload[key]) ?? '');
|
|
259
264
|
}
|
|
260
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
|
}
|
|
@@ -16,8 +16,8 @@ export interface CallbackRequestInit {
|
|
|
16
16
|
query?: QueryInput | null | undefined;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* The parts of a Node `http.IncomingMessage` this package reads, so Express
|
|
20
|
-
*
|
|
19
|
+
* The parts of a Node `http.IncomingMessage` this package reads, so Express requests work too (and
|
|
20
|
+
* Koa's `ctx.req` when no body parser consumed it).
|
|
21
21
|
*/
|
|
22
22
|
export interface NodeRequestLike extends AsyncIterable<unknown> {
|
|
23
23
|
headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
@@ -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). */
|
|
@@ -57,10 +59,11 @@ export declare class CallbackRequest {
|
|
|
57
59
|
*/
|
|
58
60
|
static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
|
|
59
61
|
/**
|
|
60
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
61
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
62
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
63
|
-
* again as JSON or a form.
|
|
62
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
63
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
64
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
65
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
66
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
64
67
|
*/
|
|
65
68
|
static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
|
|
66
69
|
/**
|
|
@@ -70,7 +73,10 @@ export declare class CallbackRequest {
|
|
|
70
73
|
static fromWebRequest(request: WebRequestLike): Promise<CallbackRequest>;
|
|
71
74
|
/** The named header, matched case-insensitively, or `undefined`. */
|
|
72
75
|
header(name: string): string | undefined;
|
|
73
|
-
/**
|
|
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
|
+
*/
|
|
74
80
|
parsedBody(): Record<string, unknown>;
|
|
75
81
|
/** The parsed body merged over the query string. */
|
|
76
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
|
}
|
|
@@ -37,10 +40,11 @@ class CallbackRequest {
|
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
40
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
41
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
42
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
43
|
-
* again as JSON or a form.
|
|
43
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
44
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
45
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
46
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
47
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
44
48
|
*/
|
|
45
49
|
static async fromNodeRequest(request) {
|
|
46
50
|
const headers = normalizeHeaders(request.headers);
|
|
@@ -73,7 +77,10 @@ class CallbackRequest {
|
|
|
73
77
|
header(name) {
|
|
74
78
|
return this.headers[name.toLowerCase()];
|
|
75
79
|
}
|
|
76
|
-
/**
|
|
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
|
+
*/
|
|
77
84
|
parsedBody() {
|
|
78
85
|
return (0, json_js_1.toPlainObject)(losslessBody(this));
|
|
79
86
|
}
|
|
@@ -216,14 +223,15 @@ class PaymentStatusResult {
|
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
exports.PaymentStatusResult = PaymentStatusResult;
|
|
219
|
-
function
|
|
226
|
+
function bodyBytes(body) {
|
|
220
227
|
if (body === undefined || body === null) {
|
|
221
|
-
return
|
|
228
|
+
return new Uint8Array();
|
|
222
229
|
}
|
|
223
230
|
if (typeof body === 'string') {
|
|
224
|
-
return body;
|
|
231
|
+
return new TextEncoder().encode(body);
|
|
225
232
|
}
|
|
226
|
-
|
|
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);
|
|
227
235
|
}
|
|
228
236
|
function rawBody(value) {
|
|
229
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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A gateway-independent payment status. Every gateway's own status values are mapped onto these.
|
|
3
3
|
*/
|
|
4
|
-
export type PaymentStatus = 'successful' | 'pending' | 'failed' | '
|
|
4
|
+
export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'canceled' | 'expired' | 'unknown';
|
|
5
5
|
/**
|
|
6
6
|
* The payment statuses, plus helpers.
|
|
7
7
|
*
|
|
@@ -18,13 +18,13 @@ export declare const PaymentStatus: Readonly<{
|
|
|
18
18
|
/** The payment was attempted and failed or was rejected. */
|
|
19
19
|
Failed: "failed";
|
|
20
20
|
/** The payment or order was canceled or closed before completing. */
|
|
21
|
-
|
|
21
|
+
Canceled: "canceled";
|
|
22
22
|
/** The payment window ran out before the customer paid. */
|
|
23
23
|
Expired: "expired";
|
|
24
24
|
/** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
|
|
25
25
|
Unknown: "unknown";
|
|
26
26
|
/** Every status, in the order above. */
|
|
27
|
-
values: readonly ["successful", "pending", "failed", "
|
|
27
|
+
values: readonly ["successful", "pending", "failed", "canceled", "expired", "unknown"];
|
|
28
28
|
/** Whether the status will not change any more: `false` for `pending` and `unknown`. */
|
|
29
29
|
isFinal(status: PaymentStatus): boolean;
|
|
30
30
|
}>;
|
package/dist/cjs/core/status.js
CHANGED
|
@@ -18,7 +18,7 @@ exports.PaymentStatus = Object.freeze({
|
|
|
18
18
|
/** The payment was attempted and failed or was rejected. */
|
|
19
19
|
Failed: 'failed',
|
|
20
20
|
/** The payment or order was canceled or closed before completing. */
|
|
21
|
-
|
|
21
|
+
Canceled: 'canceled',
|
|
22
22
|
/** The payment window ran out before the customer paid. */
|
|
23
23
|
Expired: 'expired',
|
|
24
24
|
/** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
|
|
@@ -28,7 +28,7 @@ exports.PaymentStatus = Object.freeze({
|
|
|
28
28
|
'successful',
|
|
29
29
|
'pending',
|
|
30
30
|
'failed',
|
|
31
|
-
'
|
|
31
|
+
'canceled',
|
|
32
32
|
'expired',
|
|
33
33
|
'unknown',
|
|
34
34
|
]),
|
|
@@ -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
|
}
|
|
@@ -87,7 +90,8 @@ export declare class CyberSource {
|
|
|
87
90
|
initiate(data: CyberSourcePaymentData): FormPayment;
|
|
88
91
|
/**
|
|
89
92
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
90
|
-
* page.
|
|
93
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
94
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
91
95
|
*/
|
|
92
96
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
93
97
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
@@ -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
|
}
|
|
@@ -77,7 +77,7 @@ const STATUSES = {
|
|
|
77
77
|
REVIEW: 'pending',
|
|
78
78
|
DECLINE: 'failed',
|
|
79
79
|
ERROR: 'failed',
|
|
80
|
-
CANCEL: '
|
|
80
|
+
CANCEL: 'canceled',
|
|
81
81
|
};
|
|
82
82
|
/**
|
|
83
83
|
* CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
|
|
@@ -149,31 +149,39 @@ class CyberSource {
|
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
151
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
152
|
-
* page.
|
|
152
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
153
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
153
154
|
*/
|
|
154
155
|
handleCallback(request) {
|
|
155
156
|
const payload = (0, callback_js_1.losslessInput)(request);
|
|
156
157
|
const expected = this.sign(payload);
|
|
157
|
-
|
|
158
|
+
const names = signedNames(payload);
|
|
159
|
+
if (expected === undefined ||
|
|
160
|
+
!(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'signature')) ||
|
|
161
|
+
!names.includes('decision') ||
|
|
162
|
+
!names.includes('req_reference_number')) {
|
|
158
163
|
throw new errors_js_1.SignatureVerificationError('CyberSource callback signature verification failed.', (0, json_js_1.toPlainObject)(payload));
|
|
159
164
|
}
|
|
160
|
-
|
|
165
|
+
// A signature only covers the listed fields, so anything else in the post could have been
|
|
166
|
+
// added by the sender (e.g. a `decision` next to the signed request fields).
|
|
167
|
+
const signed = {};
|
|
168
|
+
for (const name of [...names, 'signature']) {
|
|
169
|
+
(0, json_js_1.setKey)(signed, name, payload[name]);
|
|
170
|
+
}
|
|
171
|
+
const decision = (0, values_js_1.trimmed)(signed, 'decision').toUpperCase();
|
|
161
172
|
return new callback_js_1.PaymentCallback({
|
|
162
|
-
orderId: (0, values_js_1.get)(
|
|
173
|
+
orderId: (0, values_js_1.get)(signed, 'req_reference_number'),
|
|
163
174
|
status: (0, status_js_1.resolveStatus)(STATUSES, decision),
|
|
164
175
|
gatewayStatus: decision,
|
|
165
|
-
gatewayReference: (0, values_js_1.optional)(
|
|
166
|
-
amount: (0, values_js_1.get)(
|
|
167
|
-
raw: (0, json_js_1.toPlainObject)(
|
|
176
|
+
gatewayReference: (0, values_js_1.optional)(signed, 'transaction_id'),
|
|
177
|
+
amount: (0, values_js_1.get)(signed, 'auth_amount') || (0, values_js_1.optional)(signed, 'req_amount'),
|
|
178
|
+
raw: (0, json_js_1.toPlainObject)(signed),
|
|
168
179
|
});
|
|
169
180
|
}
|
|
170
181
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
171
182
|
sign(fields) {
|
|
172
183
|
const pairs = [];
|
|
173
|
-
for (const name of (
|
|
174
|
-
if (name === '') {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
184
|
+
for (const name of signedNames(fields)) {
|
|
177
185
|
const value = (0, values_js_1.scalarString)(fields[name]);
|
|
178
186
|
if (value === undefined) {
|
|
179
187
|
return undefined;
|
|
@@ -186,3 +194,9 @@ class CyberSource {
|
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
exports.CyberSource = CyberSource;
|
|
197
|
+
/** The names listed in `signed_field_names`, in order. */
|
|
198
|
+
function signedNames(fields) {
|
|
199
|
+
return (0, values_js_1.get)(fields, 'signed_field_names')
|
|
200
|
+
.split(',')
|
|
201
|
+
.filter((name) => name !== '');
|
|
202
|
+
}
|
|
@@ -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) ??
|
|
@@ -60,7 +60,7 @@ const STATUSES = {
|
|
|
60
60
|
WAIT_PAY: 'pending',
|
|
61
61
|
PAYING: 'pending',
|
|
62
62
|
PAY_FAILED: 'failed',
|
|
63
|
-
ORDER_CLOSED: '
|
|
63
|
+
ORDER_CLOSED: 'canceled',
|
|
64
64
|
ORDER_EXPIRED: 'expired',
|
|
65
65
|
};
|
|
66
66
|
/**
|
|
@@ -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;
|