@laranex/myanmar-payments 4.0.0-dev.1 → 4.0.0-dev.11

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 +21 -5
  12. package/dist/cjs/core/env.js +49 -12
  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 +36 -12
  28. package/dist/cjs/kbz-pay/kbz-pay.js +54 -23
  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 +34 -16
  32. package/dist/cjs/wave-money/wave-money.js +38 -31
  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 +21 -5
  44. package/dist/esm/core/env.js +45 -10
  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 +36 -12
  60. package/dist/esm/kbz-pay/kbz-pay.js +56 -25
  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 +34 -16
  64. package/dist/esm/wave-money/wave-money.js +41 -34
  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
@@ -12,44 +12,65 @@ const status_js_1 = require("../core/status.js");
12
12
  const validate_js_1 = require("../core/validate.js");
13
13
  const values_js_1 = require("../core/values.js");
14
14
  const signer_js_1 = require("./signer.js");
15
+ /** What a valid KBZ Pay `time_to_live_in_minutes` is: KBZ counts whole minutes, up to 120. */
16
+ const TIME_TO_LIVE_REQUIREMENT = 'a whole number from 1 to 120';
17
+ function resolveTimeToLive(value) {
18
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
19
+ return undefined;
20
+ }
21
+ let minutes;
22
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
23
+ minutes = value;
24
+ }
25
+ else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
26
+ minutes = Number.parseInt(value.trim(), 10);
27
+ }
28
+ if (minutes === undefined || minutes < 1 || minutes > 120) {
29
+ throw new errors_js_1.ConfigurationError('kbz_pay', 'time_to_live_in_minutes', true, TIME_TO_LIVE_REQUIREMENT);
30
+ }
31
+ return minutes;
32
+ }
15
33
  /**
16
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
34
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
35
+ * default to production; UAT and production issue separate credentials.
17
36
  */
18
37
  class KbzPayConfig {
19
- static SANDBOX_API_URL = 'http://api-uat.kbzpay.com/payment/gateway/uat';
20
38
  static PRODUCTION_API_URL = 'https://api.kbzpay.com/payment/gateway';
21
- static SANDBOX_PWA_URL = 'https://static.kbzpay.com/pgw/uat/pwa/#/';
22
39
  static PRODUCTION_PWA_URL = 'https://wap.kbzpay.com/pgw/pwa/#/';
23
40
  appId;
24
41
  appKey;
25
42
  merchantCode;
26
- sandbox;
43
+ /** Seconds before the default HTTP client gives up on a request. */
44
+ timeoutSeconds;
27
45
  /** The API base URL in use. */
28
46
  apiUrl;
29
47
  /** The PWA checkout URL in use, always ending in `/`. */
30
48
  pwaUrl;
49
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
50
+ timeToLiveMinutes;
31
51
  constructor(options) {
32
52
  this.appId = (0, env_js_1.requireSetting)('kbz_pay', 'app_id', options.appId);
33
53
  this.appKey = (0, env_js_1.requireSetting)('kbz_pay', 'app_key', options.appKey);
34
54
  this.merchantCode = (0, env_js_1.requireSetting)('kbz_pay', 'merchant_code', options.merchantCode);
35
- this.sandbox = options.sandbox ?? true;
36
- this.apiUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.apiUrl) ??
37
- (this.sandbox ? KbzPayConfig.SANDBOX_API_URL : KbzPayConfig.PRODUCTION_API_URL));
38
- this.pwaUrl = `${(0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.pwaUrl) ??
39
- (this.sandbox ? KbzPayConfig.SANDBOX_PWA_URL : KbzPayConfig.PRODUCTION_PWA_URL))}/`;
55
+ this.timeoutSeconds = (0, env_js_1.resolveTimeout)('kbz_pay', options.timeoutSeconds);
56
+ this.apiUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
57
+ this.pwaUrl = `${(0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
58
+ this.timeToLiveMinutes = resolveTimeToLive(options.timeToLiveMinutes);
40
59
  }
41
60
  /**
42
- * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`, `KBZ_PAY_SANDBOX`,
43
- * `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
61
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
62
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
63
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
44
64
  */
45
65
  static fromEnv(env = (0, env_js_1.defaultEnv)()) {
46
66
  return new KbzPayConfig({
47
67
  appId: (0, env_js_1.envFirst)(env, 'KBZ_PAY_APP_ID'),
48
68
  appKey: (0, env_js_1.envFirst)(env, 'KBZ_PAY_APP_KEY'),
49
69
  merchantCode: (0, env_js_1.envFirst)(env, 'KBZ_PAY_MERCHANT_CODE'),
50
- sandbox: (0, env_js_1.envSandbox)(env, 'KBZ_PAY_SANDBOX'),
70
+ timeoutSeconds: (0, env_js_1.envFirst)(env, env_js_1.HTTP_TIMEOUT_VARIABLE),
51
71
  apiUrl: (0, env_js_1.envFirst)(env, 'KBZ_PAY_BASE_URL'),
52
72
  pwaUrl: (0, env_js_1.envFirst)(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
73
+ timeToLiveMinutes: (0, env_js_1.envFirst)(env, 'KBZ_PAY_TIME_TO_LIVE_IN_MINUTES'),
53
74
  });
54
75
  }
55
76
  }
@@ -60,7 +81,7 @@ const STATUSES = {
60
81
  WAIT_PAY: 'pending',
61
82
  PAYING: 'pending',
62
83
  PAY_FAILED: 'failed',
63
- ORDER_CLOSED: 'cancelled',
84
+ ORDER_CLOSED: 'canceled',
64
85
  ORDER_EXPIRED: 'expired',
65
86
  };
66
87
  /**
@@ -76,7 +97,7 @@ class KbzPay {
76
97
  constructor(config, options = {}) {
77
98
  this.config = config instanceof KbzPayConfig ? config : new KbzPayConfig(config);
78
99
  this.signer = new signer_js_1.KbzPaySigner(this.config.appKey);
79
- this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options));
100
+ this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options, this.config.timeoutSeconds));
80
101
  }
81
102
  /** A gateway configured from the `KBZ_PAY_*` environment variables. */
82
103
  static fromEnv(env = (0, env_js_1.defaultEnv)(), options = {}) {
@@ -95,7 +116,7 @@ class KbzPay {
95
116
  .max('callbackUrl', callbackUrl, 512)
96
117
  .when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
97
118
  .string('title', data.title)
98
- .between('timeoutMinutes', data.timeoutMinutes, 1, 120)
119
+ .between('timeToLiveMinutes', data.timeToLiveMinutes, 1, 120)
99
120
  .string('callbackInfo', data.callbackInfo)
100
121
  .max('callbackInfo', typeof data.callbackInfo === 'string' ? (0, crypto_js_1.queryEscape)(data.callbackInfo) : undefined, 512)
101
122
  .validate();
@@ -105,6 +126,8 @@ class KbzPay {
105
126
  *
106
127
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
107
128
  * the URL registered with KBZ.
129
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
130
+ * a time to live.
108
131
  */
109
132
  async pwa(data, options = {}) {
110
133
  const order = await this.precreate(data, 'PWAAPP', options);
@@ -116,7 +139,11 @@ class KbzPay {
116
139
  raw: (0, json_js_1.toPlainObject)(order.response),
117
140
  });
118
141
  }
119
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
142
+ /**
143
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
144
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
145
+ * a time to live.
146
+ */
120
147
  async qr(data, options = {}) {
121
148
  const order = await this.precreate(data, 'PAY_BY_QRCODE', options);
122
149
  const qrCode = (0, values_js_1.get)(order.response, 'qrCode');
@@ -128,9 +155,7 @@ class KbzPay {
128
155
  return new results_js_1.QrPayment({
129
156
  orderId: data.orderId,
130
157
  qrString: qrCode,
131
- expiresAt: data.timeoutMinutes === undefined
132
- ? undefined
133
- : new Date(Date.now() + data.timeoutMinutes * 60_000),
158
+ expiresAt: new Date(Date.now() + order.timeToLiveMinutes * 60_000),
134
159
  reference: order.prepayId,
135
160
  raw: (0, json_js_1.toPlainObject)(order.response),
136
161
  });
@@ -138,6 +163,8 @@ class KbzPay {
138
163
  /**
139
164
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
140
165
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
166
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
167
+ * a time to live.
141
168
  */
142
169
  async app(data, options = {}) {
143
170
  const order = await this.precreate(data, 'APP', options);
@@ -190,6 +217,12 @@ class KbzPay {
190
217
  }
191
218
  async precreate(data, tradeType, options) {
192
219
  KbzPay.validate(data);
220
+ const timeToLiveMinutes = data.timeToLiveMinutes ?? this.config.timeToLiveMinutes;
221
+ if (timeToLiveMinutes === undefined) {
222
+ throw new errors_js_1.InvalidPaymentDataError({
223
+ timeToLiveMinutes: 'The timeToLiveMinutes field is required when the kbz_pay configuration has no [time_to_live_in_minutes].',
224
+ });
225
+ }
193
226
  const biz = {
194
227
  appid: this.config.appId,
195
228
  merch_code: this.config.merchantCode,
@@ -201,9 +234,7 @@ class KbzPay {
201
234
  if (data.title) {
202
235
  biz.title = data.title;
203
236
  }
204
- if (data.timeoutMinutes !== undefined) {
205
- biz.timeout_express = `${data.timeoutMinutes}m`;
206
- }
237
+ biz.timeout_express = `${timeToLiveMinutes}m`;
207
238
  if (data.callbackInfo) {
208
239
  biz.callback_info = (0, crypto_js_1.queryEscape)(data.callbackInfo);
209
240
  }
@@ -214,7 +245,7 @@ class KbzPay {
214
245
  if (prepayId === '') {
215
246
  throw new errors_js_1.ApiError('KBZ Pay did not return a prepay_id.', { raw: (0, json_js_1.toPlainObject)(response) });
216
247
  }
217
- return { prepayId, nonce, timestamp: time, response };
248
+ return { prepayId, nonce, timestamp: time, timeToLiveMinutes, response };
218
249
  }
219
250
  /** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
220
251
  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
  }
@@ -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;
@@ -11,38 +11,45 @@ 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
+ /**
15
+ * How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). A
16
+ * payment's own `timeToLiveSeconds` overrides it. Unset or blank is allowed, but then every
17
+ * payment must set its own.
18
+ */
19
+ timeToLiveSeconds?: number | string | null | undefined;
20
+ /**
21
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
22
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
23
+ * client you pass keeps its own timeout.
24
+ */
25
+ timeoutSeconds?: number | string | null | undefined;
26
+ /** Overrides the API base URL, e.g. to point at UAT. */
19
27
  baseUrl?: string | undefined;
20
28
  /** Overrides the host the customer is redirected to. Wave serves it without the API port. */
21
29
  authenticateUrl?: string | undefined;
22
30
  }
23
31
  /**
24
- * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
32
+ * Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
33
+ * default to production.
25
34
  */
26
35
  export declare class WaveMoneyConfig {
27
- static readonly SANDBOX_URL = "https://preprodpayments.wavemoney.io:8107";
28
36
  static readonly PRODUCTION_URL = "https://payments.wavemoney.io";
29
- static readonly SANDBOX_AUTHENTICATE_URL = "https://preprodpayments.wavemoney.io";
30
37
  static readonly PRODUCTION_AUTHENTICATE_URL = "https://payments.wavemoney.io";
31
- static readonly DEFAULT_TIME_TO_LIVE_SECONDS = 300;
32
38
  readonly merchantId: string;
33
39
  readonly secretKey: string;
34
40
  readonly merchantName: string;
35
- /** Seconds the customer has to pay. */
36
- readonly timeToLiveSeconds: number;
37
- readonly sandbox: boolean;
41
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
42
+ readonly timeToLiveSeconds: number | undefined;
43
+ /** Seconds before the default HTTP client gives up on a request. */
44
+ readonly timeoutSeconds: number;
38
45
  /** The API base URL in use. */
39
46
  readonly baseUrl: string;
40
47
  /** The host the customer is redirected to. */
41
48
  readonly authenticateUrl: string;
42
49
  constructor(options: WaveMoneyConfigOptions);
43
50
  /**
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`
51
+ * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
52
+ * `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
46
53
  * and `WAVE_MONEY_AUTHENTICATE_URL`.
47
54
  */
48
55
  static fromEnv(env?: EnvSource): WaveMoneyConfig;
@@ -76,6 +83,16 @@ export interface WaveMoneyPaymentData {
76
83
  * this.
77
84
  */
78
85
  merchantReferenceId?: string | undefined;
86
+ /**
87
+ * The business name shown on Wave's page for this payment. Defaults to the configured
88
+ * `merchantName` when unset.
89
+ */
90
+ merchantName?: string | null | undefined;
91
+ /**
92
+ * How long the customer has to pay this payment, in seconds: a whole number greater than 0.
93
+ * Defaults to the configured `timeToLiveSeconds` when unset; one of the two is required.
94
+ */
95
+ timeToLiveSeconds?: number | null | undefined;
79
96
  }
80
97
  /**
81
98
  * Wave Money (WavePay payment gateway): redirect payments and callbacks. Wave has no status API,
@@ -98,13 +115,14 @@ export declare class WaveMoney {
98
115
  /**
99
116
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
100
117
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
101
- * afterwards. Invalid data is left untouched.
118
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
119
+ * when neither the payment nor the config sets a time to live.
102
120
  */
103
121
  initiate(data: WaveMoneyPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
104
122
  /**
105
123
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
106
124
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
107
- * optional.
125
+ * optional. A hashed field holding an object or array fails verification.
108
126
  */
109
127
  handleCallback(request: CallbackRequest): PaymentCallback;
110
128
  private hash;
@@ -13,20 +13,19 @@ const status_js_1 = require("../core/status.js");
13
13
  const validate_js_1 = require("../core/validate.js");
14
14
  const values_js_1 = require("../core/values.js");
15
15
  /**
16
- * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
16
+ * Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
17
+ * default to production.
17
18
  */
18
19
  class WaveMoneyConfig {
19
- static SANDBOX_URL = 'https://preprodpayments.wavemoney.io:8107';
20
20
  static PRODUCTION_URL = 'https://payments.wavemoney.io';
21
- static SANDBOX_AUTHENTICATE_URL = 'https://preprodpayments.wavemoney.io';
22
21
  static PRODUCTION_AUTHENTICATE_URL = 'https://payments.wavemoney.io';
23
- static DEFAULT_TIME_TO_LIVE_SECONDS = 300;
24
22
  merchantId;
25
23
  secretKey;
26
24
  merchantName;
27
- /** Seconds the customer has to pay. */
25
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
28
26
  timeToLiveSeconds;
29
- sandbox;
27
+ /** Seconds before the default HTTP client gives up on a request. */
28
+ timeoutSeconds;
30
29
  /** The API base URL in use. */
31
30
  baseUrl;
32
31
  /** The host the customer is redirected to. */
@@ -35,31 +34,23 @@ class WaveMoneyConfig {
35
34
  this.merchantId = (0, env_js_1.requireSetting)('wave_money', 'merchant_id', options.merchantId);
36
35
  this.secretKey = (0, env_js_1.requireSetting)('wave_money', 'secret_key', options.secretKey);
37
36
  this.merchantName = (0, env_js_1.requireSetting)('wave_money', 'merchant_name', options.merchantName);
38
- const ttl = options.timeToLiveSeconds;
39
- this.timeToLiveSeconds =
40
- ttl !== undefined && Number.isInteger(ttl) && ttl > 0
41
- ? ttl
42
- : WaveMoneyConfig.DEFAULT_TIME_TO_LIVE_SECONDS;
43
- this.sandbox = options.sandbox ?? true;
44
- this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
45
- (this.sandbox ? WaveMoneyConfig.SANDBOX_URL : WaveMoneyConfig.PRODUCTION_URL));
46
- this.authenticateUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.authenticateUrl) ??
47
- (this.sandbox
48
- ? WaveMoneyConfig.SANDBOX_AUTHENTICATE_URL
49
- : WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL));
37
+ this.timeToLiveSeconds = (0, env_js_1.optionalSeconds)('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
38
+ this.timeoutSeconds = (0, env_js_1.resolveTimeout)('wave_money', options.timeoutSeconds);
39
+ this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ?? WaveMoneyConfig.PRODUCTION_URL);
40
+ this.authenticateUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.authenticateUrl) ?? WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL);
50
41
  }
51
42
  /**
52
- * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME` (falling back
53
- * to `APP_NAME`), `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `WAVE_MONEY_SANDBOX`, `WAVE_MONEY_BASE_URL`
43
+ * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
44
+ * `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
54
45
  * and `WAVE_MONEY_AUTHENTICATE_URL`.
55
46
  */
56
47
  static fromEnv(env = (0, env_js_1.defaultEnv)()) {
57
48
  return new WaveMoneyConfig({
58
49
  merchantId: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_MERCHANT_ID'),
59
50
  secretKey: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_SECRET_KEY'),
60
- merchantName: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_MERCHANT_NAME', 'APP_NAME'),
61
- timeToLiveSeconds: (0, env_js_1.envInt)(env, 'WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS'),
62
- sandbox: (0, env_js_1.envSandbox)(env, 'WAVE_MONEY_SANDBOX'),
51
+ merchantName: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_MERCHANT_NAME'),
52
+ timeToLiveSeconds: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS'),
53
+ timeoutSeconds: (0, env_js_1.envFirst)(env, env_js_1.HTTP_TIMEOUT_VARIABLE),
63
54
  baseUrl: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_BASE_URL'),
64
55
  authenticateUrl: (0, env_js_1.envFirst)(env, 'WAVE_MONEY_AUTHENTICATE_URL'),
65
56
  });
@@ -71,7 +62,7 @@ const STATUSES = {
71
62
  INSUFFICIENT_BALANCE: 'pending',
72
63
  ACCOUNT_LOCKED: 'failed',
73
64
  BILL_COLLECTION_FAILED: 'failed',
74
- PAYMENT_REQUEST_CANCELLED: 'cancelled',
65
+ PAYMENT_REQUEST_CANCELLED: 'canceled',
75
66
  TRANSACTION_TIMED_OUT: 'expired',
76
67
  SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
77
68
  };
@@ -98,7 +89,7 @@ class WaveMoney {
98
89
  transport;
99
90
  constructor(config, options = {}) {
100
91
  this.config = config instanceof WaveMoneyConfig ? config : new WaveMoneyConfig(config);
101
- this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options));
92
+ this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options, this.config.timeoutSeconds));
102
93
  }
103
94
  /** A gateway configured from the `WAVE_MONEY_*` environment variables. */
104
95
  static fromEnv(env = (0, env_js_1.defaultEnv)(), options = {}) {
@@ -136,7 +127,11 @@ class WaveMoney {
136
127
  .url('returnUrl', data.returnUrl)
137
128
  .required('description', data.description)
138
129
  .when(items.length === 0, 'items', 'The items field must have at least one item.')
139
- .string('merchantReferenceId', data.merchantReferenceId);
130
+ .string('merchantReferenceId', data.merchantReferenceId)
131
+ .string('merchantName', data.merchantName)
132
+ .when(typeof data.merchantName === 'string' && data.merchantName.trim() === '', 'merchantName', 'The merchantName field is required.')
133
+ .when(isSet(data.timeToLiveSeconds) && !Number.isInteger(data.timeToLiveSeconds), 'timeToLiveSeconds', 'The timeToLiveSeconds field must be an integer.')
134
+ .when(isSet(data.timeToLiveSeconds) && data.timeToLiveSeconds < 1, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be greater than 0.');
140
135
  items.forEach((item, index) => {
141
136
  validator
142
137
  .required(`items.${index}.name`, item?.name)
@@ -154,15 +149,22 @@ class WaveMoney {
154
149
  /**
155
150
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
156
151
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
157
- * afterwards. Invalid data is left untouched.
152
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
153
+ * when neither the payment nor the config sets a time to live.
158
154
  */
159
155
  async initiate(data, options = {}) {
160
156
  WaveMoney.validate(data);
157
+ const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
158
+ if (timeToLiveSeconds === undefined) {
159
+ throw new errors_js_1.InvalidPaymentDataError({
160
+ timeToLiveSeconds: 'The timeToLiveSeconds field is required when the wave_money configuration has no [time_to_live_in_seconds].',
161
+ });
162
+ }
161
163
  if (!data.merchantReferenceId) {
162
164
  data.merchantReferenceId = (0, crypto_js_1.randomHex)();
163
165
  }
164
166
  const amount = WaveMoney.resolvedAmount(data).toString();
165
- const ttl = String(this.config.timeToLiveSeconds);
167
+ const ttl = String(timeToLiveSeconds);
166
168
  // Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
167
169
  // text is written as a JSON number without passing through a float.
168
170
  const items = `[${data.items
@@ -177,7 +179,7 @@ class WaveMoney {
177
179
  backend_result_url: data.callbackUrl,
178
180
  amount,
179
181
  payment_description: data.description,
180
- merchant_name: this.config.merchantName,
182
+ merchant_name: data.merchantName ?? this.config.merchantName,
181
183
  items,
182
184
  hash: this.hash([
183
185
  ttl,
@@ -209,13 +211,15 @@ class WaveMoney {
209
211
  /**
210
212
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
211
213
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
212
- * optional.
214
+ * optional. A hashed field holding an object or array fails verification.
213
215
  */
214
216
  handleCallback(request) {
215
217
  const payload = (0, callback_js_1.losslessBody)(request);
216
218
  const expected = this.hash(CALLBACK_FIELDS.map((field) => (0, values_js_1.scalarString)(payload[field]) ?? 'null'));
217
219
  const hashValue = payload.hashValue;
218
- if (typeof hashValue !== 'string' || !(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
220
+ if (typeof hashValue !== 'string' ||
221
+ CALLBACK_FIELDS.some((field) => (0, values_js_1.isNested)(payload[field])) ||
222
+ !(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
219
223
  throw new errors_js_1.SignatureVerificationError('Wave Money callback hash verification failed.', (0, json_js_1.toPlainObject)(payload));
220
224
  }
221
225
  const gatewayStatus = (0, values_js_1.trimmed)(payload, 'status');
@@ -233,6 +237,9 @@ class WaveMoney {
233
237
  }
234
238
  }
235
239
  exports.WaveMoney = WaveMoney;
240
+ function isSet(value) {
241
+ return value !== undefined && value !== null;
242
+ }
236
243
  function errorMessage(body) {
237
244
  const errors = body.errors;
238
245
  if ((0, json_js_1.isLosslessObject)(errors)) {
@@ -16,35 +16,39 @@ 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;
21
- /** Overrides the API base URL. */
19
+ /** The `{version}` segment of the API paths, e.g. `v1rc`. */
20
+ apiVersion: string;
21
+ /**
22
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
23
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
24
+ * client you pass keeps its own timeout.
25
+ */
26
+ timeoutSeconds?: number | string | null | undefined;
27
+ /** Overrides the API base URL, e.g. to point at UAT. */
22
28
  baseUrl?: string | undefined;
23
- /** The `{version}` segment of the API paths (default `v1rc`). */
24
- apiVersion?: string | undefined;
25
29
  }
26
30
  /**
27
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
31
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
32
+ * defaults to production.
28
33
  */
29
34
  export declare class YomaMmqrConfig {
30
- static readonly SANDBOX_URL = "https://devapi.yomabank.net";
31
35
  static readonly PRODUCTION_URL = "https://paymenthubapi.yomabank.com";
32
- static readonly DEFAULT_API_VERSION = "v1rc";
33
36
  readonly merchantId: string;
34
37
  readonly clientId: string;
35
38
  readonly clientSecret: string;
36
39
  readonly webhookHashKey: string;
37
40
  readonly webhookSecret: string | undefined;
38
- readonly sandbox: boolean;
41
+ /** The `{version}` segment of the API paths. */
42
+ readonly apiVersion: string;
43
+ /** Seconds before the default HTTP client gives up on a request. */
44
+ readonly timeoutSeconds: number;
39
45
  /** The base URL in use. */
40
46
  readonly baseUrl: string;
41
- /** The API version in use. */
42
- readonly apiVersion: string;
43
47
  constructor(options: YomaMmqrConfigOptions);
44
48
  /**
45
49
  * Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
46
- * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
47
- * `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
50
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
51
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
48
52
  */
49
53
  static fromEnv(env?: EnvSource): YomaMmqrConfig;
50
54
  }
@@ -13,37 +13,36 @@ const status_js_1 = require("../core/status.js");
13
13
  const validate_js_1 = require("../core/validate.js");
14
14
  const values_js_1 = require("../core/values.js");
15
15
  /**
16
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
16
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
17
+ * defaults to production.
17
18
  */
18
19
  class YomaMmqrConfig {
19
- static SANDBOX_URL = 'https://devapi.yomabank.net';
20
20
  static PRODUCTION_URL = 'https://paymenthubapi.yomabank.com';
21
- static DEFAULT_API_VERSION = 'v1rc';
22
21
  merchantId;
23
22
  clientId;
24
23
  clientSecret;
25
24
  webhookHashKey;
26
25
  webhookSecret;
27
- sandbox;
26
+ /** The `{version}` segment of the API paths. */
27
+ apiVersion;
28
+ /** Seconds before the default HTTP client gives up on a request. */
29
+ timeoutSeconds;
28
30
  /** The base URL in use. */
29
31
  baseUrl;
30
- /** The API version in use. */
31
- apiVersion;
32
32
  constructor(options) {
33
33
  this.merchantId = (0, env_js_1.requireSetting)('yoma_mmqr', 'merchant_id', options.merchantId);
34
34
  this.clientId = (0, env_js_1.requireSetting)('yoma_mmqr', 'client_id', options.clientId);
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
+ this.apiVersion = (0, env_js_1.requireSetting)('yoma_mmqr', 'api_version', options.apiVersion);
38
+ this.timeoutSeconds = (0, env_js_1.resolveTimeout)('yoma_mmqr', options.timeoutSeconds);
37
39
  this.webhookSecret = (0, env_js_1.optionalSetting)(options.webhookSecret);
38
- this.sandbox = options.sandbox ?? true;
39
- this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
40
- (this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
41
- this.apiVersion = (0, env_js_1.optionalSetting)(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
40
+ this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ?? YomaMmqrConfig.PRODUCTION_URL);
42
41
  }
43
42
  /**
44
43
  * Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
45
- * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
46
- * `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
44
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
45
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
47
46
  */
48
47
  static fromEnv(env = (0, env_js_1.defaultEnv)()) {
49
48
  return new YomaMmqrConfig({
@@ -51,10 +50,10 @@ class YomaMmqrConfig {
51
50
  clientId: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_CLIENT_ID'),
52
51
  clientSecret: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_CLIENT_SECRET'),
53
52
  webhookHashKey: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_WEBHOOK_HASHKEY'),
53
+ apiVersion: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_API_VERSION'),
54
+ timeoutSeconds: (0, env_js_1.envFirst)(env, env_js_1.HTTP_TIMEOUT_VARIABLE),
54
55
  webhookSecret: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_WEBHOOK_SECRET'),
55
- sandbox: (0, env_js_1.envSandbox)(env, 'YOMA_MMQR_SANDBOX'),
56
56
  baseUrl: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_BASE_URL'),
57
- apiVersion: (0, env_js_1.envFirst)(env, 'YOMA_MMQR_API_VERSION'),
58
57
  });
59
58
  }
60
59
  }
@@ -79,7 +78,7 @@ class YomaMmqr {
79
78
  pendingToken;
80
79
  constructor(config, options = {}) {
81
80
  this.config = config instanceof YomaMmqrConfig ? config : new YomaMmqrConfig(config);
82
- this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options));
81
+ this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options, this.config.timeoutSeconds));
83
82
  this.cache = options.tokenCache ?? new cache_js_1.MemoryTokenCache();
84
83
  }
85
84
  /** A gateway configured from the `YOMA_MMQR_*` environment variables. */
@@ -161,7 +160,9 @@ class YomaMmqr {
161
160
  const orderNumber = (0, values_js_1.get)(payload, 'orderNumber');
162
161
  const status = (0, values_js_1.trimmed)(payload, 'status');
163
162
  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())) {
163
+ if (orderNumber === '' ||
164
+ (0, values_js_1.isNested)(payload.status) ||
165
+ !(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'hashValue').toLowerCase())) {
165
166
  throw new errors_js_1.SignatureVerificationError('Yoma MMQR callback hash verification failed.', (0, json_js_1.toPlainObject)(payload));
166
167
  }
167
168
  return new callback_js_1.PaymentCallback({
@@ -210,13 +211,14 @@ class YomaMmqr {
210
211
  if (!response.successful() || token === '') {
211
212
  throw apiError('token', response.status, body, (0, values_js_1.get)(body, 'error'));
212
213
  }
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);
214
+ // The leading digits of `expires_in`; missing, non-numeric or 0 means an hour.
215
+ const expiresIn = Number(/^\d+/.exec((0, values_js_1.get)(body, 'expires_in'))?.[0] ?? 0);
216
+ const ttl = Math.max(60, (expiresIn > 0 ? expiresIn : 3600) - 60);
215
217
  await this.cache.set(this.tokenCacheKey(), token, ttl);
216
218
  return token;
217
219
  }
218
220
  tokenCacheKey() {
219
- return `node-myanmar-payments.yoma-mmqr.token.${(0, crypto_js_1.sha256Hex)(`${this.config.baseUrl}|${this.config.clientId}`)}`;
221
+ return `myanmar-payments.yoma-mmqr.token.${(0, crypto_js_1.sha256Hex)(`${this.config.baseUrl}|${this.config.clientId}`)}`;
220
222
  }
221
223
  }
222
224
  exports.YomaMmqr = YomaMmqr;