@laranex/myanmar-payments 4.0.0-dev.0 → 4.0.0-dev.10

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 (68) hide show
  1. package/CHANGELOG.md +19 -4
  2. package/README.md +3 -3
  3. package/dist/cjs/aya-pay/aya-pay.d.ts +13 -9
  4. package/dist/cjs/aya-pay/aya-pay.js +16 -12
  5. package/dist/cjs/core/amount.d.ts +4 -3
  6. package/dist/cjs/core/amount.js +13 -4
  7. package/dist/cjs/core/callback.d.ts +14 -8
  8. package/dist/cjs/core/callback.js +19 -11
  9. package/dist/cjs/core/crypto.d.ts +6 -1
  10. package/dist/cjs/core/crypto.js +17 -3
  11. package/dist/cjs/core/env.d.ts +14 -5
  12. package/dist/cjs/core/env.js +35 -11
  13. package/dist/cjs/core/errors.d.ts +9 -3
  14. package/dist/cjs/core/errors.js +12 -4
  15. package/dist/cjs/core/http.d.ts +10 -11
  16. package/dist/cjs/core/http.js +7 -9
  17. package/dist/cjs/core/index.d.ts +2 -2
  18. package/dist/cjs/core/index.js +3 -2
  19. package/dist/cjs/core/json.d.ts +2 -2
  20. package/dist/cjs/core/json.js +3 -3
  21. package/dist/cjs/core/status.d.ts +3 -3
  22. package/dist/cjs/core/status.js +2 -2
  23. package/dist/cjs/core/values.d.ts +2 -0
  24. package/dist/cjs/core/values.js +5 -0
  25. package/dist/cjs/cyber-source/cyber-source.d.ts +12 -14
  26. package/dist/cjs/cyber-source/cyber-source.js +39 -26
  27. package/dist/cjs/kbz-pay/kbz-pay.d.ts +27 -11
  28. package/dist/cjs/kbz-pay/kbz-pay.js +43 -20
  29. package/dist/cjs/kbz-pay/signer.d.ts +4 -1
  30. package/dist/cjs/kbz-pay/signer.js +7 -2
  31. package/dist/cjs/wave-money/wave-money.d.ts +26 -13
  32. package/dist/cjs/wave-money/wave-money.js +29 -29
  33. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +17 -13
  34. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +21 -19
  35. package/dist/esm/aya-pay/aya-pay.d.ts +13 -9
  36. package/dist/esm/aya-pay/aya-pay.js +18 -14
  37. package/dist/esm/core/amount.d.ts +4 -3
  38. package/dist/esm/core/amount.js +13 -4
  39. package/dist/esm/core/callback.d.ts +14 -8
  40. package/dist/esm/core/callback.js +19 -11
  41. package/dist/esm/core/crypto.d.ts +6 -1
  42. package/dist/esm/core/crypto.js +17 -3
  43. package/dist/esm/core/env.d.ts +14 -5
  44. package/dist/esm/core/env.js +32 -9
  45. package/dist/esm/core/errors.d.ts +9 -3
  46. package/dist/esm/core/errors.js +12 -4
  47. package/dist/esm/core/http.d.ts +10 -11
  48. package/dist/esm/core/http.js +6 -8
  49. package/dist/esm/core/index.d.ts +2 -2
  50. package/dist/esm/core/index.js +2 -1
  51. package/dist/esm/core/json.d.ts +2 -2
  52. package/dist/esm/core/json.js +3 -3
  53. package/dist/esm/core/status.d.ts +3 -3
  54. package/dist/esm/core/status.js +2 -2
  55. package/dist/esm/core/values.d.ts +2 -0
  56. package/dist/esm/core/values.js +4 -0
  57. package/dist/esm/cyber-source/cyber-source.d.ts +12 -14
  58. package/dist/esm/cyber-source/cyber-source.js +41 -28
  59. package/dist/esm/kbz-pay/kbz-pay.d.ts +27 -11
  60. package/dist/esm/kbz-pay/kbz-pay.js +45 -22
  61. package/dist/esm/kbz-pay/signer.d.ts +4 -1
  62. package/dist/esm/kbz-pay/signer.js +8 -3
  63. package/dist/esm/wave-money/wave-money.d.ts +26 -13
  64. package/dist/esm/wave-money/wave-money.js +31 -31
  65. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +17 -13
  66. package/dist/esm/yoma-mmqr/yoma-mmqr.js +23 -21
  67. package/package.json +2 -2
  68. package/skills/node-myanmar-payments/SKILL.md +26 -22
@@ -1,43 +1,39 @@
1
1
  import {} from '../core/amount.js';
2
2
  import { CallbackRequest, losslessInput, PaymentCallback } from '../core/callback.js';
3
3
  import { hmacSha256Base64, randomHex, safeEqual } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
4
+ import { defaultEnv, envFirst, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { SignatureVerificationError } from '../core/errors.js';
6
- import { toPlainObject } from '../core/json.js';
6
+ import { setKey, toPlainObject } from '../core/json.js';
7
7
  import { FormPayment } from '../core/results.js';
8
8
  import { resolveStatus } from '../core/status.js';
9
9
  import { toAmount, Validator } from '../core/validate.js';
10
10
  import { get, optional, scalarString, trimmed } from '../core/values.js';
11
11
  /**
12
- * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`.
12
+ * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`. The
13
+ * URL defaults to production.
13
14
  */
14
15
  export class CyberSourceConfig {
15
- static SANDBOX_URL = 'https://testsecureacceptance.cybersource.com';
16
16
  static PRODUCTION_URL = 'https://secureacceptance.cybersource.com';
17
17
  profileId;
18
18
  accessKey;
19
19
  secretKey;
20
- sandbox;
21
20
  /** The base URL in use. */
22
21
  baseUrl;
23
22
  constructor(options) {
24
23
  this.profileId = requireSetting('cyber_source', 'profile_id', options.profileId);
25
24
  this.accessKey = requireSetting('cyber_source', 'access_key', options.accessKey);
26
25
  this.secretKey = requireSetting('cyber_source', 'secret_key', options.secretKey);
27
- this.sandbox = options.sandbox ?? true;
28
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
29
- (this.sandbox ? CyberSourceConfig.SANDBOX_URL : CyberSourceConfig.PRODUCTION_URL));
26
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? CyberSourceConfig.PRODUCTION_URL);
30
27
  }
31
28
  /**
32
29
  * Reads `CYBER_SOURCE_PROFILE_ID`, `CYBER_SOURCE_ACCESS_KEY`, `CYBER_SOURCE_SECRET_KEY`,
33
- * `CYBER_SOURCE_SANDBOX` and `CYBER_SOURCE_BASE_URL`.
30
+ * and `CYBER_SOURCE_BASE_URL`.
34
31
  */
35
32
  static fromEnv(env = defaultEnv()) {
36
33
  return new CyberSourceConfig({
37
34
  profileId: envFirst(env, 'CYBER_SOURCE_PROFILE_ID'),
38
35
  accessKey: envFirst(env, 'CYBER_SOURCE_ACCESS_KEY'),
39
36
  secretKey: envFirst(env, 'CYBER_SOURCE_SECRET_KEY'),
40
- sandbox: envSandbox(env, 'CYBER_SOURCE_SANDBOX'),
41
37
  baseUrl: envFirst(env, 'CYBER_SOURCE_BASE_URL'),
42
38
  });
43
39
  }
@@ -74,7 +70,7 @@ const STATUSES = {
74
70
  REVIEW: 'pending',
75
71
  DECLINE: 'failed',
76
72
  ERROR: 'failed',
77
- CANCEL: 'cancelled',
73
+ CANCEL: 'canceled',
78
74
  };
79
75
  /**
80
76
  * CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
@@ -110,9 +106,12 @@ export class CyberSource {
110
106
  .string('cancelUrl', data.cancelUrl)
111
107
  .url('cancelUrl', data.cancelUrl)
112
108
  .max('cancelUrl', data.cancelUrl, 255)
113
- .pattern('currency', String(data.currency || 'MMK'), /^[A-Z]{3}$/, 'a three letter ISO 4217 code')
114
- .pattern('locale', String(data.locale || 'en-us'), /^[a-z]{2}-[a-z]{2}$/, 'a locale code such as en-us')
115
- .when(!TRANSACTION_TYPES.includes(String(data.transactionType || 'sale')), 'transactionType', 'The transactionType field is not a supported transaction type.')
109
+ .required('currency', data.currency)
110
+ .pattern('currency', data.currency, /^[A-Z]{3}$/, 'a three letter ISO 4217 code')
111
+ .required('transactionType', data.transactionType)
112
+ .when(!TRANSACTION_TYPES.includes(String(data.transactionType)), 'transactionType', 'The transactionType field is not a supported transaction type.')
113
+ .required('locale', data.locale)
114
+ .pattern('locale', data.locale, /^[a-z]{2}-[a-z]{2}$/, 'a locale code such as en-us')
116
115
  .validate();
117
116
  }
118
117
  /**
@@ -127,11 +126,11 @@ export class CyberSource {
127
126
  transaction_uuid: randomHex(),
128
127
  signed_field_names: SIGNED_FIELDS.join(','),
129
128
  signed_date_time: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
130
- locale: data.locale || 'en-us',
131
- transaction_type: data.transactionType || 'sale',
129
+ locale: data.locale,
130
+ transaction_type: data.transactionType,
132
131
  reference_number: data.orderId,
133
132
  amount: String(toAmount(data.amount)),
134
- currency: data.currency || 'MMK',
133
+ currency: data.currency,
135
134
  override_custom_receipt_page: data.returnUrl ?? '',
136
135
  override_backoffice_post_url: data.callbackUrl,
137
136
  override_custom_cancel_page: data.cancelUrl ?? '',
@@ -146,31 +145,39 @@ export class CyberSource {
146
145
  }
147
146
  /**
148
147
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
149
- * page.
148
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
149
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
150
150
  */
151
151
  handleCallback(request) {
152
152
  const payload = losslessInput(request);
153
153
  const expected = this.sign(payload);
154
- if (expected === undefined || !safeEqual(expected, get(payload, 'signature'))) {
154
+ const names = signedNames(payload);
155
+ if (expected === undefined ||
156
+ !safeEqual(expected, get(payload, 'signature')) ||
157
+ !names.includes('decision') ||
158
+ !names.includes('req_reference_number')) {
155
159
  throw new SignatureVerificationError('CyberSource callback signature verification failed.', toPlainObject(payload));
156
160
  }
157
- const decision = trimmed(payload, 'decision').toUpperCase();
161
+ // A signature only covers the listed fields, so anything else in the post could have been
162
+ // added by the sender (e.g. a `decision` next to the signed request fields).
163
+ const signed = {};
164
+ for (const name of [...names, 'signature']) {
165
+ setKey(signed, name, payload[name]);
166
+ }
167
+ const decision = trimmed(signed, 'decision').toUpperCase();
158
168
  return new PaymentCallback({
159
- orderId: get(payload, 'req_reference_number'),
169
+ orderId: get(signed, 'req_reference_number'),
160
170
  status: resolveStatus(STATUSES, decision),
161
171
  gatewayStatus: decision,
162
- gatewayReference: optional(payload, 'transaction_id'),
163
- amount: get(payload, 'auth_amount') || optional(payload, 'req_amount'),
164
- raw: toPlainObject(payload),
172
+ gatewayReference: optional(signed, 'transaction_id'),
173
+ amount: get(signed, 'auth_amount') || optional(signed, 'req_amount'),
174
+ raw: toPlainObject(signed),
165
175
  });
166
176
  }
167
177
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
168
178
  sign(fields) {
169
179
  const pairs = [];
170
- for (const name of get(fields, 'signed_field_names').split(',')) {
171
- if (name === '') {
172
- continue;
173
- }
180
+ for (const name of signedNames(fields)) {
174
181
  const value = scalarString(fields[name]);
175
182
  if (value === undefined) {
176
183
  return undefined;
@@ -182,3 +189,9 @@ export class CyberSource {
182
189
  : hmacSha256Base64(this.config.secretKey, pairs.join(','));
183
190
  }
184
191
  }
192
+ /** The names listed in `signed_field_names`, in order. */
193
+ function signedNames(fields) {
194
+ return get(fields, 'signed_field_names')
195
+ .split(',')
196
+ .filter((name) => name !== '');
197
+ }
@@ -12,33 +12,45 @@ 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;
17
- /** Overrides the API base URL, e.g. to go through a proxy. */
15
+ /**
16
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
17
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
18
+ * client you pass keeps its own timeout.
19
+ */
20
+ timeoutSeconds?: number | string | null | undefined;
21
+ /** Overrides the API base URL, e.g. to point at UAT or go through a proxy. */
18
22
  apiUrl?: string | undefined;
19
23
  /** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
20
24
  pwaUrl?: string | undefined;
25
+ /**
26
+ * Seconds the customer has to pay each order, a multiple of 60 from 60 to 7200 (or its text).
27
+ * A payment's own `timeToLiveSeconds` overrides it; unset or blank leaves it to KBZ (7200).
28
+ */
29
+ timeToLiveSeconds?: number | string | null | undefined;
21
30
  }
22
31
  /**
23
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
32
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
33
+ * default to production; UAT and production issue separate credentials.
24
34
  */
25
35
  export declare class KbzPayConfig {
26
- static readonly SANDBOX_API_URL = "http://api-uat.kbzpay.com/payment/gateway/uat";
27
36
  static readonly PRODUCTION_API_URL = "https://api.kbzpay.com/payment/gateway";
28
- static readonly SANDBOX_PWA_URL = "https://static.kbzpay.com/pgw/uat/pwa/#/";
29
37
  static readonly PRODUCTION_PWA_URL = "https://wap.kbzpay.com/pgw/pwa/#/";
30
38
  readonly appId: string;
31
39
  readonly appKey: string;
32
40
  readonly merchantCode: string;
33
- readonly sandbox: boolean;
41
+ /** Seconds before the default HTTP client gives up on a request. */
42
+ readonly timeoutSeconds: number;
34
43
  /** The API base URL in use. */
35
44
  readonly apiUrl: string;
36
45
  /** The PWA checkout URL in use, always ending in `/`. */
37
46
  readonly pwaUrl: string;
47
+ /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
48
+ readonly timeToLiveSeconds: number | undefined;
38
49
  constructor(options: KbzPayConfigOptions);
39
50
  /**
40
- * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`, `KBZ_PAY_SANDBOX`,
41
- * `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
51
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
52
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
53
+ * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
42
54
  */
43
55
  static fromEnv(env?: EnvSource): KbzPayConfig;
44
56
  }
@@ -54,8 +66,12 @@ export interface KbzPayPaymentData {
54
66
  callbackUrl: string;
55
67
  /** The product name shown to the customer. */
56
68
  title?: string | undefined;
57
- /** How long the order stays payable, 1 to 120 minutes. KBZ defaults to 120. */
58
- timeoutMinutes?: number | undefined;
69
+ /**
70
+ * Seconds the customer has to pay before the order expires, 60 to 7200 in whole minutes (a multiple
71
+ * of 60). KBZ counts minutes, so 1800 is sent as `30m`. Overrides the config's `timeToLiveSeconds`;
72
+ * when both are unset KBZ allows 7200 (120 minutes).
73
+ */
74
+ timeToLiveSeconds?: number | undefined;
59
75
  /** Free text KBZ sends back unchanged in the callback, at most 512 characters once URL-encoded. */
60
76
  callbackInfo?: string | undefined;
61
77
  }
@@ -1,8 +1,8 @@
1
1
  import {} from '../core/amount.js';
2
2
  import { Acknowledgement, CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
3
3
  import { queryEscape, randomHex } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
- import { ApiError, SignatureVerificationError } from '../core/errors.js';
4
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSetting, resolveTimeout, trimUrl, } from '../core/env.js';
5
+ import { ApiError, ConfigurationError, SignatureVerificationError } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { toPlainObject } from '../core/json.js';
8
8
  import { AppPayment, QrPayment, RedirectPayment } from '../core/results.js';
@@ -10,44 +10,65 @@ import { resolveStatus } from '../core/status.js';
10
10
  import { toAmount, Validator } from '../core/validate.js';
11
11
  import { get, object, optional, trimmed } from '../core/values.js';
12
12
  import { KbzPaySigner } from './signer.js';
13
+ /** What a valid KBZ Pay `time_to_live_in_seconds` is: KBZ counts whole minutes, up to 120. */
14
+ const TIME_TO_LIVE_REQUIREMENT = 'a multiple of 60 from 60 to 7200';
15
+ function resolveTimeToLive(value) {
16
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
17
+ return undefined;
18
+ }
19
+ let seconds;
20
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
21
+ seconds = value;
22
+ }
23
+ else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
24
+ seconds = Number.parseInt(value.trim(), 10);
25
+ }
26
+ if (seconds === undefined || seconds < 60 || seconds > 7200 || seconds % 60 !== 0) {
27
+ throw new ConfigurationError('kbz_pay', 'time_to_live_in_seconds', true, TIME_TO_LIVE_REQUIREMENT);
28
+ }
29
+ return seconds;
30
+ }
13
31
  /**
14
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
32
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
33
+ * default to production; UAT and production issue separate credentials.
15
34
  */
16
35
  export class KbzPayConfig {
17
- static SANDBOX_API_URL = 'http://api-uat.kbzpay.com/payment/gateway/uat';
18
36
  static PRODUCTION_API_URL = 'https://api.kbzpay.com/payment/gateway';
19
- static SANDBOX_PWA_URL = 'https://static.kbzpay.com/pgw/uat/pwa/#/';
20
37
  static PRODUCTION_PWA_URL = 'https://wap.kbzpay.com/pgw/pwa/#/';
21
38
  appId;
22
39
  appKey;
23
40
  merchantCode;
24
- sandbox;
41
+ /** Seconds before the default HTTP client gives up on a request. */
42
+ timeoutSeconds;
25
43
  /** The API base URL in use. */
26
44
  apiUrl;
27
45
  /** The PWA checkout URL in use, always ending in `/`. */
28
46
  pwaUrl;
47
+ /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
48
+ timeToLiveSeconds;
29
49
  constructor(options) {
30
50
  this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
31
51
  this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
32
52
  this.merchantCode = requireSetting('kbz_pay', 'merchant_code', options.merchantCode);
33
- this.sandbox = options.sandbox ?? true;
34
- this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ??
35
- (this.sandbox ? KbzPayConfig.SANDBOX_API_URL : KbzPayConfig.PRODUCTION_API_URL));
36
- this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ??
37
- (this.sandbox ? KbzPayConfig.SANDBOX_PWA_URL : KbzPayConfig.PRODUCTION_PWA_URL))}/`;
53
+ this.timeoutSeconds = resolveTimeout('kbz_pay', options.timeoutSeconds);
54
+ this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
55
+ this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
56
+ this.timeToLiveSeconds = resolveTimeToLive(options.timeToLiveSeconds);
38
57
  }
39
58
  /**
40
- * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`, `KBZ_PAY_SANDBOX`,
41
- * `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
59
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
60
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
61
+ * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
42
62
  */
43
63
  static fromEnv(env = defaultEnv()) {
44
64
  return new KbzPayConfig({
45
65
  appId: envFirst(env, 'KBZ_PAY_APP_ID'),
46
66
  appKey: envFirst(env, 'KBZ_PAY_APP_KEY'),
47
67
  merchantCode: envFirst(env, 'KBZ_PAY_MERCHANT_CODE'),
48
- sandbox: envSandbox(env, 'KBZ_PAY_SANDBOX'),
68
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
49
69
  apiUrl: envFirst(env, 'KBZ_PAY_BASE_URL'),
50
70
  pwaUrl: envFirst(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
71
+ timeToLiveSeconds: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_SECONDS'),
51
72
  });
52
73
  }
53
74
  }
@@ -57,7 +78,7 @@ const STATUSES = {
57
78
  WAIT_PAY: 'pending',
58
79
  PAYING: 'pending',
59
80
  PAY_FAILED: 'failed',
60
- ORDER_CLOSED: 'cancelled',
81
+ ORDER_CLOSED: 'canceled',
61
82
  ORDER_EXPIRED: 'expired',
62
83
  };
63
84
  /**
@@ -73,7 +94,7 @@ export class KbzPay {
73
94
  constructor(config, options = {}) {
74
95
  this.config = config instanceof KbzPayConfig ? config : new KbzPayConfig(config);
75
96
  this.signer = new KbzPaySigner(this.config.appKey);
76
- this.transport = new Transport(httpClientFrom(options));
97
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
77
98
  }
78
99
  /** A gateway configured from the `KBZ_PAY_*` environment variables. */
79
100
  static fromEnv(env = defaultEnv(), options = {}) {
@@ -92,7 +113,8 @@ export class KbzPay {
92
113
  .max('callbackUrl', callbackUrl, 512)
93
114
  .when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
94
115
  .string('title', data.title)
95
- .between('timeoutMinutes', data.timeoutMinutes, 1, 120)
116
+ .between('timeToLiveSeconds', data.timeToLiveSeconds, 60, 7200)
117
+ .when(Number.isInteger(data.timeToLiveSeconds) && data.timeToLiveSeconds % 60 !== 0, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be a multiple of 60.')
96
118
  .string('callbackInfo', data.callbackInfo)
97
119
  .max('callbackInfo', typeof data.callbackInfo === 'string' ? queryEscape(data.callbackInfo) : undefined, 512)
98
120
  .validate();
@@ -125,9 +147,9 @@ export class KbzPay {
125
147
  return new QrPayment({
126
148
  orderId: data.orderId,
127
149
  qrString: qrCode,
128
- expiresAt: data.timeoutMinutes === undefined
150
+ expiresAt: order.timeToLiveSeconds === undefined
129
151
  ? undefined
130
- : new Date(Date.now() + data.timeoutMinutes * 60_000),
152
+ : new Date(Date.now() + order.timeToLiveSeconds * 1000),
131
153
  reference: order.prepayId,
132
154
  raw: toPlainObject(order.response),
133
155
  });
@@ -198,8 +220,9 @@ export class KbzPay {
198
220
  if (data.title) {
199
221
  biz.title = data.title;
200
222
  }
201
- if (data.timeoutMinutes !== undefined) {
202
- biz.timeout_express = `${data.timeoutMinutes}m`;
223
+ const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
224
+ if (timeToLiveSeconds !== undefined) {
225
+ biz.timeout_express = `${timeToLiveSeconds / 60}m`;
203
226
  }
204
227
  if (data.callbackInfo) {
205
228
  biz.callback_info = queryEscape(data.callbackInfo);
@@ -211,7 +234,7 @@ export class KbzPay {
211
234
  if (prepayId === '') {
212
235
  throw new ApiError('KBZ Pay did not return a prepay_id.', { raw: toPlainObject(response) });
213
236
  }
214
- return { prepayId, nonce, timestamp: time, response };
237
+ return { prepayId, nonce, timestamp: time, timeToLiveSeconds, response };
215
238
  }
216
239
  /** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
217
240
  async call(endpoint, method, version, biz, extra, nonce, time, signal) {
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import { safeEqual, sha256Hex } from '../core/crypto.js';
2
- import { scalarString } from '../core/values.js';
2
+ import { isNested, scalarString } from '../core/values.js';
3
3
  /**
4
4
  * KBZ Pay's signature: every non-empty scalar field except `sign` and `sign_type`, sorted by key,
5
5
  * joined as raw `key=value` pairs (not URL-encoded), with `&key=<app key>` appended, hashed with
@@ -24,9 +24,14 @@ export class KbzPaySigner {
24
24
  sign(fields) {
25
25
  return sha256Hex(`${this.signString(fields)}&key=${this.appKey}`).toUpperCase();
26
26
  }
27
- /** Whether `fields.sign` matches, compared in constant time. */
27
+ /**
28
+ * Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
29
+ * fields holding an object or array fail rather than being partly trusted.
30
+ */
28
31
  verify(fields) {
29
32
  const sign = fields.sign;
30
- return typeof sign === 'string' && safeEqual(this.sign(fields), sign.toUpperCase());
33
+ return (typeof sign === 'string' &&
34
+ !Object.values(fields).some(isNested) &&
35
+ safeEqual(this.sign(fields), sign.toUpperCase()));
31
36
  }
32
37
  }
@@ -11,38 +11,41 @@ export interface WaveMoneyConfigOptions {
11
11
  secretKey: string;
12
12
  /** Your business name, shown on Wave's payment page. */
13
13
  merchantName: string;
14
- /** How long the customer has to pay, in seconds (default 300). */
15
- timeToLiveSeconds?: number | undefined;
16
- /** Use the test environment (default `true`). */
17
- sandbox?: boolean | undefined;
18
- /** Overrides the API base URL. */
14
+ /** How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). */
15
+ timeToLiveSeconds: number | string;
16
+ /**
17
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
18
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
19
+ * client you pass keeps its own timeout.
20
+ */
21
+ timeoutSeconds?: number | string | null | undefined;
22
+ /** Overrides the API base URL, e.g. to point at UAT. */
19
23
  baseUrl?: string | undefined;
20
24
  /** Overrides the host the customer is redirected to. Wave serves it without the API port. */
21
25
  authenticateUrl?: string | undefined;
22
26
  }
23
27
  /**
24
- * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
28
+ * Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
29
+ * default to production.
25
30
  */
26
31
  export declare class WaveMoneyConfig {
27
- static readonly SANDBOX_URL = "https://preprodpayments.wavemoney.io:8107";
28
32
  static readonly PRODUCTION_URL = "https://payments.wavemoney.io";
29
- static readonly SANDBOX_AUTHENTICATE_URL = "https://preprodpayments.wavemoney.io";
30
33
  static readonly PRODUCTION_AUTHENTICATE_URL = "https://payments.wavemoney.io";
31
- static readonly DEFAULT_TIME_TO_LIVE_SECONDS = 300;
32
34
  readonly merchantId: string;
33
35
  readonly secretKey: string;
34
36
  readonly merchantName: string;
35
37
  /** Seconds the customer has to pay. */
36
38
  readonly timeToLiveSeconds: number;
37
- readonly sandbox: boolean;
39
+ /** Seconds before the default HTTP client gives up on a request. */
40
+ readonly timeoutSeconds: number;
38
41
  /** The API base URL in use. */
39
42
  readonly baseUrl: string;
40
43
  /** The host the customer is redirected to. */
41
44
  readonly authenticateUrl: string;
42
45
  constructor(options: WaveMoneyConfigOptions);
43
46
  /**
44
- * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME` (falling back
45
- * to `APP_NAME`), `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `WAVE_MONEY_SANDBOX`, `WAVE_MONEY_BASE_URL`
47
+ * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
48
+ * `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
46
49
  * and `WAVE_MONEY_AUTHENTICATE_URL`.
47
50
  */
48
51
  static fromEnv(env?: EnvSource): WaveMoneyConfig;
@@ -76,6 +79,16 @@ export interface WaveMoneyPaymentData {
76
79
  * this.
77
80
  */
78
81
  merchantReferenceId?: string | undefined;
82
+ /**
83
+ * The business name shown on Wave's page for this payment. Defaults to the configured
84
+ * `merchantName` when unset.
85
+ */
86
+ merchantName?: string | null | undefined;
87
+ /**
88
+ * How long the customer has to pay this payment, in seconds: a whole number greater than 0.
89
+ * Defaults to the configured `timeToLiveSeconds` when unset.
90
+ */
91
+ timeToLiveSeconds?: number | null | undefined;
79
92
  }
80
93
  /**
81
94
  * Wave Money (WavePay payment gateway): redirect payments and callbacks. Wave has no status API,
@@ -104,7 +117,7 @@ export declare class WaveMoney {
104
117
  /**
105
118
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
106
119
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
107
- * optional.
120
+ * optional. A hashed field holding an object or array fails verification.
108
121
  */
109
122
  handleCallback(request: CallbackRequest): PaymentCallback;
110
123
  private hash;
@@ -1,29 +1,28 @@
1
1
  import { Amount } from '../core/amount.js';
2
2
  import { CallbackRequest, losslessBody, PaymentCallback } from '../core/callback.js';
3
3
  import { hmacSha256Hex, queryEscape, randomHex, safeEqual } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envInt, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
4
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSeconds, requireSetting, resolveTimeout, 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, toPlainObject } from '../core/json.js';
8
8
  import { RedirectPayment } from '../core/results.js';
9
9
  import { resolveStatus } from '../core/status.js';
10
10
  import { toAmount, Validator } from '../core/validate.js';
11
- import { get, optional, scalarString, trimmed } from '../core/values.js';
11
+ import { get, isNested, optional, scalarString, trimmed } from '../core/values.js';
12
12
  /**
13
- * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
13
+ * Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
14
+ * default to production.
14
15
  */
15
16
  export class WaveMoneyConfig {
16
- static SANDBOX_URL = 'https://preprodpayments.wavemoney.io:8107';
17
17
  static PRODUCTION_URL = 'https://payments.wavemoney.io';
18
- static SANDBOX_AUTHENTICATE_URL = 'https://preprodpayments.wavemoney.io';
19
18
  static PRODUCTION_AUTHENTICATE_URL = 'https://payments.wavemoney.io';
20
- static DEFAULT_TIME_TO_LIVE_SECONDS = 300;
21
19
  merchantId;
22
20
  secretKey;
23
21
  merchantName;
24
22
  /** Seconds the customer has to pay. */
25
23
  timeToLiveSeconds;
26
- sandbox;
24
+ /** Seconds before the default HTTP client gives up on a request. */
25
+ timeoutSeconds;
27
26
  /** The API base URL in use. */
28
27
  baseUrl;
29
28
  /** The host the customer is redirected to. */
@@ -32,31 +31,23 @@ export class WaveMoneyConfig {
32
31
  this.merchantId = requireSetting('wave_money', 'merchant_id', options.merchantId);
33
32
  this.secretKey = requireSetting('wave_money', 'secret_key', options.secretKey);
34
33
  this.merchantName = requireSetting('wave_money', 'merchant_name', options.merchantName);
35
- const ttl = options.timeToLiveSeconds;
36
- this.timeToLiveSeconds =
37
- ttl !== undefined && Number.isInteger(ttl) && ttl > 0
38
- ? ttl
39
- : WaveMoneyConfig.DEFAULT_TIME_TO_LIVE_SECONDS;
40
- this.sandbox = options.sandbox ?? true;
41
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
42
- (this.sandbox ? WaveMoneyConfig.SANDBOX_URL : WaveMoneyConfig.PRODUCTION_URL));
43
- this.authenticateUrl = trimUrl(optionalSetting(options.authenticateUrl) ??
44
- (this.sandbox
45
- ? WaveMoneyConfig.SANDBOX_AUTHENTICATE_URL
46
- : WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL));
34
+ this.timeToLiveSeconds = requireSeconds('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
35
+ this.timeoutSeconds = resolveTimeout('wave_money', options.timeoutSeconds);
36
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? WaveMoneyConfig.PRODUCTION_URL);
37
+ this.authenticateUrl = trimUrl(optionalSetting(options.authenticateUrl) ?? WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL);
47
38
  }
48
39
  /**
49
- * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME` (falling back
50
- * to `APP_NAME`), `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `WAVE_MONEY_SANDBOX`, `WAVE_MONEY_BASE_URL`
40
+ * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
41
+ * `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
51
42
  * and `WAVE_MONEY_AUTHENTICATE_URL`.
52
43
  */
53
44
  static fromEnv(env = defaultEnv()) {
54
45
  return new WaveMoneyConfig({
55
46
  merchantId: envFirst(env, 'WAVE_MONEY_MERCHANT_ID'),
56
47
  secretKey: envFirst(env, 'WAVE_MONEY_SECRET_KEY'),
57
- merchantName: envFirst(env, 'WAVE_MONEY_MERCHANT_NAME', 'APP_NAME'),
58
- timeToLiveSeconds: envInt(env, 'WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS'),
59
- sandbox: envSandbox(env, 'WAVE_MONEY_SANDBOX'),
48
+ merchantName: envFirst(env, 'WAVE_MONEY_MERCHANT_NAME'),
49
+ timeToLiveSeconds: envFirst(env, 'WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS'),
50
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
60
51
  baseUrl: envFirst(env, 'WAVE_MONEY_BASE_URL'),
61
52
  authenticateUrl: envFirst(env, 'WAVE_MONEY_AUTHENTICATE_URL'),
62
53
  });
@@ -67,7 +58,7 @@ const STATUSES = {
67
58
  INSUFFICIENT_BALANCE: 'pending',
68
59
  ACCOUNT_LOCKED: 'failed',
69
60
  BILL_COLLECTION_FAILED: 'failed',
70
- PAYMENT_REQUEST_CANCELLED: 'cancelled',
61
+ PAYMENT_REQUEST_CANCELLED: 'canceled',
71
62
  TRANSACTION_TIMED_OUT: 'expired',
72
63
  SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
73
64
  };
@@ -94,7 +85,7 @@ export class WaveMoney {
94
85
  transport;
95
86
  constructor(config, options = {}) {
96
87
  this.config = config instanceof WaveMoneyConfig ? config : new WaveMoneyConfig(config);
97
- this.transport = new Transport(httpClientFrom(options));
88
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
98
89
  }
99
90
  /** A gateway configured from the `WAVE_MONEY_*` environment variables. */
100
91
  static fromEnv(env = defaultEnv(), options = {}) {
@@ -132,7 +123,11 @@ export class WaveMoney {
132
123
  .url('returnUrl', data.returnUrl)
133
124
  .required('description', data.description)
134
125
  .when(items.length === 0, 'items', 'The items field must have at least one item.')
135
- .string('merchantReferenceId', data.merchantReferenceId);
126
+ .string('merchantReferenceId', data.merchantReferenceId)
127
+ .string('merchantName', data.merchantName)
128
+ .when(typeof data.merchantName === 'string' && data.merchantName.trim() === '', 'merchantName', 'The merchantName field is required.')
129
+ .when(isSet(data.timeToLiveSeconds) && !Number.isInteger(data.timeToLiveSeconds), 'timeToLiveSeconds', 'The timeToLiveSeconds field must be an integer.')
130
+ .when(isSet(data.timeToLiveSeconds) && data.timeToLiveSeconds < 1, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be greater than 0.');
136
131
  items.forEach((item, index) => {
137
132
  validator
138
133
  .required(`items.${index}.name`, item?.name)
@@ -158,7 +153,7 @@ export class WaveMoney {
158
153
  data.merchantReferenceId = randomHex();
159
154
  }
160
155
  const amount = WaveMoney.resolvedAmount(data).toString();
161
- const ttl = String(this.config.timeToLiveSeconds);
156
+ const ttl = String(data.timeToLiveSeconds ?? this.config.timeToLiveSeconds);
162
157
  // Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
163
158
  // text is written as a JSON number without passing through a float.
164
159
  const items = `[${data.items
@@ -173,7 +168,7 @@ export class WaveMoney {
173
168
  backend_result_url: data.callbackUrl,
174
169
  amount,
175
170
  payment_description: data.description,
176
- merchant_name: this.config.merchantName,
171
+ merchant_name: data.merchantName ?? this.config.merchantName,
177
172
  items,
178
173
  hash: this.hash([
179
174
  ttl,
@@ -205,13 +200,15 @@ export class WaveMoney {
205
200
  /**
206
201
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
207
202
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
208
- * optional.
203
+ * optional. A hashed field holding an object or array fails verification.
209
204
  */
210
205
  handleCallback(request) {
211
206
  const payload = losslessBody(request);
212
207
  const expected = this.hash(CALLBACK_FIELDS.map((field) => scalarString(payload[field]) ?? 'null'));
213
208
  const hashValue = payload.hashValue;
214
- if (typeof hashValue !== 'string' || !safeEqual(expected, hashValue.toLowerCase())) {
209
+ if (typeof hashValue !== 'string' ||
210
+ CALLBACK_FIELDS.some((field) => isNested(payload[field])) ||
211
+ !safeEqual(expected, hashValue.toLowerCase())) {
215
212
  throw new SignatureVerificationError('Wave Money callback hash verification failed.', toPlainObject(payload));
216
213
  }
217
214
  const gatewayStatus = trimmed(payload, 'status');
@@ -228,6 +225,9 @@ export class WaveMoney {
228
225
  return hmacSha256Hex(this.config.secretKey, parts.join(''));
229
226
  }
230
227
  }
228
+ function isSet(value) {
229
+ return value !== undefined && value !== null;
230
+ }
231
231
  function errorMessage(body) {
232
232
  const errors = body.errors;
233
233
  if (isLosslessObject(errors)) {