@laranex/myanmar-payments 4.0.0-dev.1 → 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
@@ -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;
@@ -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
25
  /** Seconds the customer has to pay. */
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.requireSeconds)('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)
@@ -162,7 +157,7 @@ class WaveMoney {
162
157
  data.merchantReferenceId = (0, crypto_js_1.randomHex)();
163
158
  }
164
159
  const amount = WaveMoney.resolvedAmount(data).toString();
165
- const ttl = String(this.config.timeToLiveSeconds);
160
+ const ttl = String(data.timeToLiveSeconds ?? this.config.timeToLiveSeconds);
166
161
  // Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
167
162
  // text is written as a JSON number without passing through a float.
168
163
  const items = `[${data.items
@@ -177,7 +172,7 @@ class WaveMoney {
177
172
  backend_result_url: data.callbackUrl,
178
173
  amount,
179
174
  payment_description: data.description,
180
- merchant_name: this.config.merchantName,
175
+ merchant_name: data.merchantName ?? this.config.merchantName,
181
176
  items,
182
177
  hash: this.hash([
183
178
  ttl,
@@ -209,13 +204,15 @@ class WaveMoney {
209
204
  /**
210
205
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
211
206
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
212
- * optional.
207
+ * optional. A hashed field holding an object or array fails verification.
213
208
  */
214
209
  handleCallback(request) {
215
210
  const payload = (0, callback_js_1.losslessBody)(request);
216
211
  const expected = this.hash(CALLBACK_FIELDS.map((field) => (0, values_js_1.scalarString)(payload[field]) ?? 'null'));
217
212
  const hashValue = payload.hashValue;
218
- if (typeof hashValue !== 'string' || !(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
213
+ if (typeof hashValue !== 'string' ||
214
+ CALLBACK_FIELDS.some((field) => (0, values_js_1.isNested)(payload[field])) ||
215
+ !(0, crypto_js_1.safeEqual)(expected, hashValue.toLowerCase())) {
219
216
  throw new errors_js_1.SignatureVerificationError('Wave Money callback hash verification failed.', (0, json_js_1.toPlainObject)(payload));
220
217
  }
221
218
  const gatewayStatus = (0, values_js_1.trimmed)(payload, 'status');
@@ -233,6 +230,9 @@ class WaveMoney {
233
230
  }
234
231
  }
235
232
  exports.WaveMoney = WaveMoney;
233
+ function isSet(value) {
234
+ return value !== undefined && value !== null;
235
+ }
236
236
  function errorMessage(body) {
237
237
  const errors = body.errors;
238
238
  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;
@@ -9,27 +9,31 @@ 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;
14
- /** Overrides the gateway base URL. */
12
+ /**
13
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
14
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
15
+ * client you pass keeps its own timeout.
16
+ */
17
+ timeoutSeconds?: number | string | null | undefined;
18
+ /** Overrides the gateway base URL, e.g. to point at UAT. */
15
19
  baseUrl?: string | undefined;
16
20
  }
17
21
  /**
18
- * AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
19
- * `ConfigurationError`.
22
+ * AYA Payment Gateway (APG) credentials and endpoints. A missing setting throws a
23
+ * `ConfigurationError`. The URL defaults to production.
20
24
  */
21
25
  export declare class AyaPayConfig {
22
- static readonly SANDBOX_URL = "https://uat-pgw.ayainnovation.com";
23
26
  static readonly PRODUCTION_URL = "https://pgw.ayainnovation.com";
24
27
  readonly appKey: string;
25
28
  readonly appSecret: string;
26
- readonly sandbox: boolean;
29
+ /** Seconds before the default HTTP client gives up on a request. */
30
+ readonly timeoutSeconds: number;
27
31
  /** The base URL in use. */
28
32
  readonly baseUrl: string;
29
33
  constructor(options: AyaPayConfigOptions);
30
34
  /**
31
- * Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `AYA_PAY_SANDBOX` and `AYA_PAY_BASE_URL`, falling
32
- * back to the `AYA_PGW_*` names.
35
+ * Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT` and
36
+ * `AYA_PAY_BASE_URL`, falling back to the `AYA_PGW_*` names.
33
37
  */
34
38
  static fromEnv(env?: EnvSource): AyaPayConfig;
35
39
  }
@@ -1,42 +1,41 @@
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, HTTP_TIMEOUT_VARIABLE, optionalSetting, 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, 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
- * AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
14
- * `ConfigurationError`.
13
+ * AYA Payment Gateway (APG) credentials and endpoints. A missing setting throws a
14
+ * `ConfigurationError`. The URL defaults to production.
15
15
  */
16
16
  export class AyaPayConfig {
17
- static SANDBOX_URL = 'https://uat-pgw.ayainnovation.com';
18
17
  static PRODUCTION_URL = 'https://pgw.ayainnovation.com';
19
18
  appKey;
20
19
  appSecret;
21
- sandbox;
20
+ /** Seconds before the default HTTP client gives up on a request. */
21
+ timeoutSeconds;
22
22
  /** The base URL in use. */
23
23
  baseUrl;
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;
28
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
29
- (this.sandbox ? AyaPayConfig.SANDBOX_URL : AyaPayConfig.PRODUCTION_URL));
27
+ this.timeoutSeconds = resolveTimeout('aya_pay', options.timeoutSeconds);
28
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? AyaPayConfig.PRODUCTION_URL);
30
29
  }
31
30
  /**
32
- * Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `AYA_PAY_SANDBOX` and `AYA_PAY_BASE_URL`, falling
33
- * back to the `AYA_PGW_*` names.
31
+ * Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT` and
32
+ * `AYA_PAY_BASE_URL`, falling back to the `AYA_PGW_*` names.
34
33
  */
35
34
  static fromEnv(env = defaultEnv()) {
36
35
  return new AyaPayConfig({
37
36
  appKey: envFirst(env, 'AYA_PAY_APP_KEY', 'AYA_PGW_APP_KEY'),
38
37
  appSecret: envFirst(env, 'AYA_PAY_APP_SECRET', 'AYA_PGW_APP_SECRET'),
39
- sandbox: envSandbox(env, 'AYA_PAY_SANDBOX'),
38
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
40
39
  baseUrl: envFirst(env, 'AYA_PAY_BASE_URL', 'AYA_PGW_BASE_URL'),
41
40
  });
42
41
  }
@@ -119,7 +118,7 @@ export class AyaPay {
119
118
  transport;
120
119
  constructor(config, options = {}) {
121
120
  this.config = config instanceof AyaPayConfig ? config : new AyaPayConfig(config);
122
- this.transport = new Transport(httpClientFrom(options));
121
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
123
122
  }
124
123
  /** A gateway configured from the `AYA_PAY_*` (or `AYA_PGW_*`) environment variables. */
125
124
  static fromEnv(env = defaultEnv(), options = {}) {
@@ -242,7 +241,8 @@ export class AyaPay {
242
241
  /** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
243
242
  verifiedPayload(input, context) {
244
243
  const fail = () => new SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, toPlainObject(input));
245
- const json = decodeBase64(get(input, 'payload'));
244
+ // Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
245
+ const json = decodeBase64(get(input, 'payload').replace(/ /g, '+'));
246
246
  const payload = json === undefined ? undefined : parseJsonObject(json);
247
247
  if (payload === undefined) {
248
248
  throw fail();
@@ -251,6 +251,10 @@ export class AyaPay {
251
251
  for (const field of PAYLOAD_FIELDS) {
252
252
  const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
253
253
  if (Object.prototype.hasOwnProperty.call(payload, key)) {
254
+ // A nested value is never signed, so a payload carrying one is rejected.
255
+ if (isNested(payload[key])) {
256
+ throw fail();
257
+ }
254
258
  parts.push(scalarString(payload[key]) ?? '');
255
259
  }
256
260
  }
@@ -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
  }
@@ -16,8 +16,8 @@ export interface CallbackRequestInit {
16
16
  query?: QueryInput | null | undefined;
17
17
  }
18
18
  /**
19
- * The parts of a Node `http.IncomingMessage` this package reads, so Express, Fastify (`request.raw`)
20
- * and Koa (`ctx.req`) requests work too.
19
+ * The parts of a Node `http.IncomingMessage` this package reads, so Express requests work too (and
20
+ * Koa's `ctx.req` when no body parser consumed it).
21
21
  */
22
22
  export interface NodeRequestLike extends AsyncIterable<unknown> {
23
23
  headers: Readonly<Record<string, string | readonly string[] | undefined>>;
@@ -42,8 +42,10 @@ export interface WebRequestLike {
42
42
  * {@link CallbackRequest.fromNodeRequest} or {@link CallbackRequest.fromWebRequest}.
43
43
  */
44
44
  export declare class CallbackRequest {
45
- /** The 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). */
@@ -57,10 +59,11 @@ export declare class CallbackRequest {
57
59
  */
58
60
  static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
59
61
  /**
60
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
61
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
62
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
63
- * again as JSON or a form.
62
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
63
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
64
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
65
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
66
+ * {@link CallbackRequest.from} with the raw body there.
64
67
  */
65
68
  static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
66
69
  /**
@@ -70,7 +73,10 @@ export declare class CallbackRequest {
70
73
  static fromWebRequest(request: WebRequestLike): Promise<CallbackRequest>;
71
74
  /** The named header, matched case-insensitively, or `undefined`. */
72
75
  header(name: string): string | undefined;
73
- /** 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
+ */
74
80
  parsedBody(): Record<string, unknown>;
75
81
  /** The parsed body merged over the query string. */
76
82
  input(): Record<string, unknown>;