@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/aya-pay/aya-pay.d.ts +5 -2
  3. package/dist/cjs/aya-pay/aya-pay.js +5 -1
  4. package/dist/cjs/core/amount.d.ts +4 -3
  5. package/dist/cjs/core/amount.js +13 -4
  6. package/dist/cjs/core/callback.d.ts +7 -2
  7. package/dist/cjs/core/callback.js +14 -7
  8. package/dist/cjs/core/crypto.d.ts +6 -1
  9. package/dist/cjs/core/crypto.js +17 -3
  10. package/dist/cjs/core/env.d.ts +7 -0
  11. package/dist/cjs/core/env.js +14 -2
  12. package/dist/cjs/core/json.d.ts +2 -2
  13. package/dist/cjs/core/json.js +3 -3
  14. package/dist/cjs/core/values.d.ts +2 -0
  15. package/dist/cjs/core/values.js +5 -0
  16. package/dist/cjs/cyber-source/cyber-source.d.ts +5 -2
  17. package/dist/cjs/cyber-source/cyber-source.js +1 -1
  18. package/dist/cjs/kbz-pay/kbz-pay.d.ts +6 -2
  19. package/dist/cjs/kbz-pay/kbz-pay.js +1 -1
  20. package/dist/cjs/kbz-pay/signer.d.ts +4 -1
  21. package/dist/cjs/kbz-pay/signer.js +7 -2
  22. package/dist/cjs/wave-money/wave-money.d.ts +6 -3
  23. package/dist/cjs/wave-money/wave-money.js +5 -3
  24. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +5 -2
  25. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +8 -5
  26. package/dist/esm/aya-pay/aya-pay.d.ts +5 -2
  27. package/dist/esm/aya-pay/aya-pay.js +7 -3
  28. package/dist/esm/core/amount.d.ts +4 -3
  29. package/dist/esm/core/amount.js +13 -4
  30. package/dist/esm/core/callback.d.ts +7 -2
  31. package/dist/esm/core/callback.js +14 -7
  32. package/dist/esm/core/crypto.d.ts +6 -1
  33. package/dist/esm/core/crypto.js +17 -3
  34. package/dist/esm/core/env.d.ts +7 -0
  35. package/dist/esm/core/env.js +13 -2
  36. package/dist/esm/core/json.d.ts +2 -2
  37. package/dist/esm/core/json.js +3 -3
  38. package/dist/esm/core/values.d.ts +2 -0
  39. package/dist/esm/core/values.js +4 -0
  40. package/dist/esm/cyber-source/cyber-source.d.ts +5 -2
  41. package/dist/esm/cyber-source/cyber-source.js +2 -2
  42. package/dist/esm/kbz-pay/kbz-pay.d.ts +6 -2
  43. package/dist/esm/kbz-pay/kbz-pay.js +2 -2
  44. package/dist/esm/kbz-pay/signer.d.ts +4 -1
  45. package/dist/esm/kbz-pay/signer.js +8 -3
  46. package/dist/esm/wave-money/wave-money.d.ts +6 -3
  47. package/dist/esm/wave-money/wave-money.js +7 -5
  48. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +5 -2
  49. package/dist/esm/yoma-mmqr/yoma-mmqr.js +10 -7
  50. package/package.json +1 -1
  51. 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
- /** Use the UAT environment (default `true`). */
13
- sandbox?: boolean | undefined;
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 ?? true;
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, ignoring trailing fractional zeros
47
- * (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
48
- * that was not sent never matches.
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
  }
@@ -88,20 +88,29 @@ class Amount {
88
88
  return !this.isZero();
89
89
  }
90
90
  /**
91
- * Whether both amounts have the same value, ignoring trailing fractional zeros
92
- * (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
93
- * that was not sent never matches.
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 raw request body, decoded as UTF-8. */
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
- /** The body decoded as JSON or as a urlencoded form. */
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 raw request body, decoded as UTF-8. */
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.body = bodyText(init.body);
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
- /** The body decoded as JSON or as a urlencoded form. */
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 bodyText(body) {
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
- return new TextDecoder().decode(body);
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
- /** Decodes strict standard base64 (with padding), or `undefined`. @internal */
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;
@@ -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
- /** Decodes strict standard base64 (with padding), or `undefined`. @internal */
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 (value === '' || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
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
  }
@@ -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 */
@@ -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
- const value = env[key]?.trim().toLowerCase() ?? '';
29
- return !['false', '0', 'f', 'no', 'off'].includes(value);
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) {
@@ -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 (numbers become `number`), for the `raw`
36
- * fields of results.
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
  */
@@ -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 (numbers become `number`), for the `raw`
60
- * fields of results.
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 Number(value.text);
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;
@@ -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
- /** Use the test environment (default `true`). */
14
- sandbox?: boolean | undefined;
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 ?? true;
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
- /** Use the UAT endpoints (default `true`). UAT and production issue separate credentials. */
16
- sandbox?: boolean | undefined;
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 ?? true;
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
- /** Whether `fields.sign` matches, compared in constant time. */
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
- /** Whether `fields.sign` matches, compared in constant time. */
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' && (0, crypto_js_1.safeEqual)(this.sign(fields), sign.toUpperCase());
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
- /** Use the test environment (default `true`). */
17
- sandbox?: boolean | undefined;
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 ?? true;
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' || !(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
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
- /** Use the UAT payment hub (default `true`). */
20
- sandbox?: boolean | undefined;
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 ?? true;
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 === '' || !(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'hashValue').toLowerCase())) {
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
- const expiresIn = Number.parseInt((0, values_js_1.get)(body, 'expires_in'), 10);
214
- const ttl = Math.max(60, (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) - 60);
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 `node-myanmar-payments.yoma-mmqr.token.${(0, crypto_js_1.sha256Hex)(`${this.config.baseUrl}|${this.config.clientId}`)}`;
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
- /** Use the UAT environment (default `true`). */
13
- sandbox?: boolean | undefined;
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 ?? true;
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, ignoring trailing fractional zeros
47
- * (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
48
- * that was not sent never matches.
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
  }
@@ -85,19 +85,28 @@ export class Amount {
85
85
  return !this.isZero();
86
86
  }
87
87
  /**
88
- * Whether both amounts have the same value, ignoring trailing fractional zeros
89
- * (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
90
- * that was not sent never matches.
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
  }