@laranex/myanmar-payments 4.0.0-dev.3 → 4.0.0-dev.5

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 (63) hide show
  1. package/CHANGELOG.md +10 -3
  2. package/dist/cjs/aya-pay/aya-pay.d.ts +12 -9
  3. package/dist/cjs/aya-pay/aya-pay.js +14 -11
  4. package/dist/cjs/core/amount.d.ts +4 -3
  5. package/dist/cjs/core/amount.js +13 -4
  6. package/dist/cjs/core/callback.d.ts +7 -2
  7. package/dist/cjs/core/callback.js +14 -7
  8. package/dist/cjs/core/crypto.d.ts +6 -1
  9. package/dist/cjs/core/crypto.js +17 -3
  10. package/dist/cjs/core/env.d.ts +5 -5
  11. package/dist/cjs/core/env.js +21 -12
  12. package/dist/cjs/core/errors.d.ts +8 -3
  13. package/dist/cjs/core/errors.js +11 -4
  14. package/dist/cjs/core/http.d.ts +10 -11
  15. package/dist/cjs/core/http.js +7 -9
  16. package/dist/cjs/core/index.d.ts +1 -1
  17. package/dist/cjs/core/index.js +1 -2
  18. package/dist/cjs/core/json.d.ts +2 -2
  19. package/dist/cjs/core/json.js +3 -3
  20. package/dist/cjs/core/values.d.ts +2 -0
  21. package/dist/cjs/core/values.js +5 -0
  22. package/dist/cjs/cyber-source/cyber-source.d.ts +10 -13
  23. package/dist/cjs/cyber-source/cyber-source.js +13 -14
  24. package/dist/cjs/kbz-pay/kbz-pay.d.ts +12 -9
  25. package/dist/cjs/kbz-pay/kbz-pay.js +11 -13
  26. package/dist/cjs/kbz-pay/signer.d.ts +4 -1
  27. package/dist/cjs/kbz-pay/signer.js +7 -2
  28. package/dist/cjs/wave-money/wave-money.d.ts +15 -13
  29. package/dist/cjs/wave-money/wave-money.js +18 -25
  30. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +16 -13
  31. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +21 -19
  32. package/dist/esm/aya-pay/aya-pay.d.ts +12 -9
  33. package/dist/esm/aya-pay/aya-pay.js +16 -13
  34. package/dist/esm/core/amount.d.ts +4 -3
  35. package/dist/esm/core/amount.js +13 -4
  36. package/dist/esm/core/callback.d.ts +7 -2
  37. package/dist/esm/core/callback.js +14 -7
  38. package/dist/esm/core/crypto.d.ts +6 -1
  39. package/dist/esm/core/crypto.js +17 -3
  40. package/dist/esm/core/env.d.ts +5 -5
  41. package/dist/esm/core/env.js +19 -10
  42. package/dist/esm/core/errors.d.ts +8 -3
  43. package/dist/esm/core/errors.js +11 -4
  44. package/dist/esm/core/http.d.ts +10 -11
  45. package/dist/esm/core/http.js +6 -8
  46. package/dist/esm/core/index.d.ts +1 -1
  47. package/dist/esm/core/index.js +1 -1
  48. package/dist/esm/core/json.d.ts +2 -2
  49. package/dist/esm/core/json.js +3 -3
  50. package/dist/esm/core/values.d.ts +2 -0
  51. package/dist/esm/core/values.js +4 -0
  52. package/dist/esm/cyber-source/cyber-source.d.ts +10 -13
  53. package/dist/esm/cyber-source/cyber-source.js +14 -15
  54. package/dist/esm/kbz-pay/kbz-pay.d.ts +12 -9
  55. package/dist/esm/kbz-pay/kbz-pay.js +12 -14
  56. package/dist/esm/kbz-pay/signer.d.ts +4 -1
  57. package/dist/esm/kbz-pay/signer.js +8 -3
  58. package/dist/esm/wave-money/wave-money.d.ts +15 -13
  59. package/dist/esm/wave-money/wave-money.js +20 -27
  60. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +16 -13
  61. package/dist/esm/yoma-mmqr/yoma-mmqr.js +23 -21
  62. package/package.json +1 -1
  63. package/skills/node-myanmar-payments/SKILL.md +24 -21
@@ -1,7 +1,7 @@
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
6
  import { setKey, toPlainObject } from '../core/json.js';
7
7
  import { FormPayment } from '../core/results.js';
@@ -9,35 +9,31 @@ 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
  }
@@ -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 ?? '',
@@ -12,33 +12,36 @@ 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). A client you pass keeps its own timeout.
18
+ */
19
+ timeoutSeconds: number | string;
20
+ /** Overrides the API base URL, e.g. to point at UAT or go through a proxy. */
18
21
  apiUrl?: string | undefined;
19
22
  /** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
20
23
  pwaUrl?: string | undefined;
21
24
  }
22
25
  /**
23
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
26
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
27
+ * default to production; UAT and production issue separate credentials.
24
28
  */
25
29
  export declare class KbzPayConfig {
26
- static readonly SANDBOX_API_URL = "http://api-uat.kbzpay.com/payment/gateway/uat";
27
30
  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
31
  static readonly PRODUCTION_PWA_URL = "https://wap.kbzpay.com/pgw/pwa/#/";
30
32
  readonly appId: string;
31
33
  readonly appKey: string;
32
34
  readonly merchantCode: string;
33
- readonly sandbox: boolean;
35
+ /** Seconds before the default HTTP client gives up on a request. */
36
+ readonly timeoutSeconds: number;
34
37
  /** The API base URL in use. */
35
38
  readonly apiUrl: string;
36
39
  /** The PWA checkout URL in use, always ending in `/`. */
37
40
  readonly pwaUrl: string;
38
41
  constructor(options: KbzPayConfigOptions);
39
42
  /**
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`.
43
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
44
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
42
45
  */
43
46
  static fromEnv(env?: EnvSource): KbzPayConfig;
44
47
  }
@@ -1,7 +1,7 @@
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';
4
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSeconds, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { ApiError, SignatureVerificationError } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { toPlainObject } from '../core/json.js';
@@ -11,17 +11,17 @@ 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
13
  /**
14
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
14
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
15
+ * default to production; UAT and production issue separate credentials.
15
16
  */
16
17
  export class KbzPayConfig {
17
- static SANDBOX_API_URL = 'http://api-uat.kbzpay.com/payment/gateway/uat';
18
18
  static PRODUCTION_API_URL = 'https://api.kbzpay.com/payment/gateway';
19
- static SANDBOX_PWA_URL = 'https://static.kbzpay.com/pgw/uat/pwa/#/';
20
19
  static PRODUCTION_PWA_URL = 'https://wap.kbzpay.com/pgw/pwa/#/';
21
20
  appId;
22
21
  appKey;
23
22
  merchantCode;
24
- sandbox;
23
+ /** Seconds before the default HTTP client gives up on a request. */
24
+ timeoutSeconds;
25
25
  /** The API base URL in use. */
26
26
  apiUrl;
27
27
  /** The PWA checkout URL in use, always ending in `/`. */
@@ -30,22 +30,20 @@ export class KbzPayConfig {
30
30
  this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
31
31
  this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
32
32
  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))}/`;
33
+ this.timeoutSeconds = requireSeconds('kbz_pay', 'timeout_in_seconds', options.timeoutSeconds);
34
+ this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
35
+ this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
38
36
  }
39
37
  /**
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`.
38
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
39
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
42
40
  */
43
41
  static fromEnv(env = defaultEnv()) {
44
42
  return new KbzPayConfig({
45
43
  appId: envFirst(env, 'KBZ_PAY_APP_ID'),
46
44
  appKey: envFirst(env, 'KBZ_PAY_APP_KEY'),
47
45
  merchantCode: envFirst(env, 'KBZ_PAY_MERCHANT_CODE'),
48
- sandbox: envSandbox(env, 'KBZ_PAY_SANDBOX'),
46
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
49
47
  apiUrl: envFirst(env, 'KBZ_PAY_BASE_URL'),
50
48
  pwaUrl: envFirst(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
51
49
  });
@@ -73,7 +71,7 @@ export class KbzPay {
73
71
  constructor(config, options = {}) {
74
72
  this.config = config instanceof KbzPayConfig ? config : new KbzPayConfig(config);
75
73
  this.signer = new KbzPaySigner(this.config.appKey);
76
- this.transport = new Transport(httpClientFrom(options));
74
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
77
75
  }
78
76
  /** A gateway configured from the `KBZ_PAY_*` environment variables. */
79
77
  static fromEnv(env = defaultEnv(), options = {}) {
@@ -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,40 @@ 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). A client you pass keeps its own timeout.
19
+ */
20
+ timeoutSeconds: number | string;
21
+ /** Overrides the API base URL, e.g. to point at UAT. */
19
22
  baseUrl?: string | undefined;
20
23
  /** Overrides the host the customer is redirected to. Wave serves it without the API port. */
21
24
  authenticateUrl?: string | undefined;
22
25
  }
23
26
  /**
24
- * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
27
+ * Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
28
+ * default to production.
25
29
  */
26
30
  export declare class WaveMoneyConfig {
27
- static readonly SANDBOX_URL = "https://preprodpayments.wavemoney.io:8107";
28
31
  static readonly PRODUCTION_URL = "https://payments.wavemoney.io";
29
- static readonly SANDBOX_AUTHENTICATE_URL = "https://preprodpayments.wavemoney.io";
30
32
  static readonly PRODUCTION_AUTHENTICATE_URL = "https://payments.wavemoney.io";
31
- static readonly DEFAULT_TIME_TO_LIVE_SECONDS = 300;
32
33
  readonly merchantId: string;
33
34
  readonly secretKey: string;
34
35
  readonly merchantName: string;
35
36
  /** Seconds the customer has to pay. */
36
37
  readonly timeToLiveSeconds: number;
37
- readonly sandbox: boolean;
38
+ /** Seconds before the default HTTP client gives up on a request. */
39
+ readonly timeoutSeconds: number;
38
40
  /** The API base URL in use. */
39
41
  readonly baseUrl: string;
40
42
  /** The host the customer is redirected to. */
41
43
  readonly authenticateUrl: string;
42
44
  constructor(options: WaveMoneyConfigOptions);
43
45
  /**
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`
46
+ * Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
47
+ * `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
46
48
  * and `WAVE_MONEY_AUTHENTICATE_URL`.
47
49
  */
48
50
  static fromEnv(env?: EnvSource): WaveMoneyConfig;
@@ -104,7 +106,7 @@ export declare class WaveMoney {
104
106
  /**
105
107
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
106
108
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
107
- * optional.
109
+ * optional. A hashed field holding an object or array fails verification.
108
110
  */
109
111
  handleCallback(request: CallbackRequest): PaymentCallback;
110
112
  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, 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 = requireSeconds('wave_money', 'timeout_in_seconds', 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
  });
@@ -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 = {}) {
@@ -205,13 +196,15 @@ export class WaveMoney {
205
196
  /**
206
197
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
207
198
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
208
- * optional.
199
+ * optional. A hashed field holding an object or array fails verification.
209
200
  */
210
201
  handleCallback(request) {
211
202
  const payload = losslessBody(request);
212
203
  const expected = this.hash(CALLBACK_FIELDS.map((field) => scalarString(payload[field]) ?? 'null'));
213
204
  const hashValue = payload.hashValue;
214
- if (typeof hashValue !== 'string' || !safeEqual(expected, hashValue.toLowerCase())) {
205
+ if (typeof hashValue !== 'string' ||
206
+ CALLBACK_FIELDS.some((field) => isNested(payload[field])) ||
207
+ !safeEqual(expected, hashValue.toLowerCase())) {
215
208
  throw new SignatureVerificationError('Wave Money callback hash verification failed.', toPlainObject(payload));
216
209
  }
217
210
  const gatewayStatus = trimmed(payload, 'status');
@@ -16,35 +16,38 @@ 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). A client you pass keeps its own timeout.
24
+ */
25
+ timeoutSeconds: number | string;
26
+ /** Overrides the API base URL, e.g. to point at UAT. */
22
27
  baseUrl?: string | undefined;
23
- /** The `{version}` segment of the API paths (default `v1rc`). */
24
- apiVersion?: string | undefined;
25
28
  }
26
29
  /**
27
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
30
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
31
+ * defaults to production.
28
32
  */
29
33
  export declare class YomaMmqrConfig {
30
- static readonly SANDBOX_URL = "https://devapi.yomabank.net";
31
34
  static readonly PRODUCTION_URL = "https://paymenthubapi.yomabank.com";
32
- static readonly DEFAULT_API_VERSION = "v1rc";
33
35
  readonly merchantId: string;
34
36
  readonly clientId: string;
35
37
  readonly clientSecret: string;
36
38
  readonly webhookHashKey: string;
37
39
  readonly webhookSecret: string | undefined;
38
- readonly sandbox: boolean;
40
+ /** The `{version}` segment of the API paths. */
41
+ readonly apiVersion: string;
42
+ /** Seconds before the default HTTP client gives up on a request. */
43
+ readonly timeoutSeconds: number;
39
44
  /** The base URL in use. */
40
45
  readonly baseUrl: string;
41
- /** The API version in use. */
42
- readonly apiVersion: string;
43
46
  constructor(options: YomaMmqrConfigOptions);
44
47
  /**
45
48
  * 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`.
49
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
50
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
48
51
  */
49
52
  static fromEnv(env?: EnvSource): YomaMmqrConfig;
50
53
  }
@@ -2,46 +2,45 @@ import {} from '../core/amount.js';
2
2
  import { MemoryTokenCache } from '../core/cache.js';
3
3
  import { CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
4
4
  import { hmacSha256Hex, safeEqual, sha256Hex } from '../core/crypto.js';
5
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSeconds, requireSetting, trimUrl, } from '../core/env.js';
6
6
  import { ApiError, SignatureVerificationError } from '../core/errors.js';
7
7
  import { httpClientFrom, Transport, } from '../core/http.js';
8
8
  import { toPlainObject } from '../core/json.js';
9
9
  import { QrPayment } from '../core/results.js';
10
10
  import { resolveStatus } from '../core/status.js';
11
11
  import { toAmount, Validator } from '../core/validate.js';
12
- import { get, optional, trimmed } from '../core/values.js';
12
+ import { get, isNested, optional, trimmed } from '../core/values.js';
13
13
  /**
14
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
14
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
15
+ * defaults to production.
15
16
  */
16
17
  export class YomaMmqrConfig {
17
- static SANDBOX_URL = 'https://devapi.yomabank.net';
18
18
  static PRODUCTION_URL = 'https://paymenthubapi.yomabank.com';
19
- static DEFAULT_API_VERSION = 'v1rc';
20
19
  merchantId;
21
20
  clientId;
22
21
  clientSecret;
23
22
  webhookHashKey;
24
23
  webhookSecret;
25
- sandbox;
24
+ /** The `{version}` segment of the API paths. */
25
+ apiVersion;
26
+ /** Seconds before the default HTTP client gives up on a request. */
27
+ timeoutSeconds;
26
28
  /** The base URL in use. */
27
29
  baseUrl;
28
- /** The API version in use. */
29
- apiVersion;
30
30
  constructor(options) {
31
31
  this.merchantId = requireSetting('yoma_mmqr', 'merchant_id', options.merchantId);
32
32
  this.clientId = requireSetting('yoma_mmqr', 'client_id', options.clientId);
33
33
  this.clientSecret = requireSetting('yoma_mmqr', 'client_secret', options.clientSecret);
34
34
  this.webhookHashKey = requireSetting('yoma_mmqr', 'webhook_hashkey', options.webhookHashKey);
35
+ this.apiVersion = requireSetting('yoma_mmqr', 'api_version', options.apiVersion);
36
+ this.timeoutSeconds = requireSeconds('yoma_mmqr', 'timeout_in_seconds', options.timeoutSeconds);
35
37
  this.webhookSecret = optionalSetting(options.webhookSecret);
36
- this.sandbox = options.sandbox ?? true;
37
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
38
- (this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
39
- this.apiVersion = optionalSetting(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
38
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? YomaMmqrConfig.PRODUCTION_URL);
40
39
  }
41
40
  /**
42
41
  * Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
43
- * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
44
- * `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
42
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
43
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
45
44
  */
46
45
  static fromEnv(env = defaultEnv()) {
47
46
  return new YomaMmqrConfig({
@@ -49,10 +48,10 @@ export class YomaMmqrConfig {
49
48
  clientId: envFirst(env, 'YOMA_MMQR_CLIENT_ID'),
50
49
  clientSecret: envFirst(env, 'YOMA_MMQR_CLIENT_SECRET'),
51
50
  webhookHashKey: envFirst(env, 'YOMA_MMQR_WEBHOOK_HASHKEY'),
51
+ apiVersion: envFirst(env, 'YOMA_MMQR_API_VERSION'),
52
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
52
53
  webhookSecret: envFirst(env, 'YOMA_MMQR_WEBHOOK_SECRET'),
53
- sandbox: envSandbox(env, 'YOMA_MMQR_SANDBOX'),
54
54
  baseUrl: envFirst(env, 'YOMA_MMQR_BASE_URL'),
55
- apiVersion: envFirst(env, 'YOMA_MMQR_API_VERSION'),
56
55
  });
57
56
  }
58
57
  }
@@ -76,7 +75,7 @@ export class YomaMmqr {
76
75
  pendingToken;
77
76
  constructor(config, options = {}) {
78
77
  this.config = config instanceof YomaMmqrConfig ? config : new YomaMmqrConfig(config);
79
- this.transport = new Transport(httpClientFrom(options));
78
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
80
79
  this.cache = options.tokenCache ?? new MemoryTokenCache();
81
80
  }
82
81
  /** A gateway configured from the `YOMA_MMQR_*` environment variables. */
@@ -158,7 +157,9 @@ export class YomaMmqr {
158
157
  const orderNumber = get(payload, 'orderNumber');
159
158
  const status = trimmed(payload, 'status');
160
159
  const expected = hmacSha256Hex(orderNumber + this.config.webhookHashKey, `orderNumber=${orderNumber}&status=${status}`);
161
- if (orderNumber === '' || !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
160
+ if (orderNumber === '' ||
161
+ isNested(payload.status) ||
162
+ !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
162
163
  throw new SignatureVerificationError('Yoma MMQR callback hash verification failed.', toPlainObject(payload));
163
164
  }
164
165
  return new PaymentCallback({
@@ -207,13 +208,14 @@ export class YomaMmqr {
207
208
  if (!response.successful() || token === '') {
208
209
  throw apiError('token', response.status, body, get(body, 'error'));
209
210
  }
210
- const expiresIn = Number.parseInt(get(body, 'expires_in'), 10);
211
- const ttl = Math.max(60, (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) - 60);
211
+ // The leading digits of `expires_in`; missing, non-numeric or 0 means an hour.
212
+ const expiresIn = Number(/^\d+/.exec(get(body, 'expires_in'))?.[0] ?? 0);
213
+ const ttl = Math.max(60, (expiresIn > 0 ? expiresIn : 3600) - 60);
212
214
  await this.cache.set(this.tokenCacheKey(), token, ttl);
213
215
  return token;
214
216
  }
215
217
  tokenCacheKey() {
216
- return `node-myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
218
+ return `myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
217
219
  }
218
220
  }
219
221
  function apiError(endpoint, status, body, errorCode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laranex/myanmar-payments",
3
- "version": "4.0.0-dev.3",
3
+ "version": "4.0.0-dev.5",
4
4
  "description": "Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed TypeScript API, ESM and CommonJS, no runtime dependencies.",
5
5
  "keywords": [
6
6
  "laranex",