@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A gateway-independent payment status. Every gateway's own status values are mapped onto these.
3
3
  */
4
- export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'cancelled' | 'expired' | 'unknown';
4
+ export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'canceled' | 'expired' | 'unknown';
5
5
  /**
6
6
  * The payment statuses, plus helpers.
7
7
  *
@@ -18,13 +18,13 @@ export declare const PaymentStatus: Readonly<{
18
18
  /** The payment was attempted and failed or was rejected. */
19
19
  Failed: "failed";
20
20
  /** The payment or order was canceled or closed before completing. */
21
- Cancelled: "cancelled";
21
+ Canceled: "canceled";
22
22
  /** The payment window ran out before the customer paid. */
23
23
  Expired: "expired";
24
24
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
25
25
  Unknown: "unknown";
26
26
  /** Every status, in the order above. */
27
- values: readonly ["successful", "pending", "failed", "cancelled", "expired", "unknown"];
27
+ values: readonly ["successful", "pending", "failed", "canceled", "expired", "unknown"];
28
28
  /** Whether the status will not change any more: `false` for `pending` and `unknown`. */
29
29
  isFinal(status: PaymentStatus): boolean;
30
30
  }>;
@@ -14,7 +14,7 @@ export const PaymentStatus = Object.freeze({
14
14
  /** The payment was attempted and failed or was rejected. */
15
15
  Failed: 'failed',
16
16
  /** The payment or order was canceled or closed before completing. */
17
- Cancelled: 'cancelled',
17
+ Canceled: 'canceled',
18
18
  /** The payment window ran out before the customer paid. */
19
19
  Expired: 'expired',
20
20
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
@@ -24,7 +24,7 @@ export const PaymentStatus = Object.freeze({
24
24
  'successful',
25
25
  'pending',
26
26
  'failed',
27
- 'cancelled',
27
+ 'canceled',
28
28
  'expired',
29
29
  'unknown',
30
30
  ]),
@@ -14,3 +14,5 @@ export declare function optional(source: Record<string, unknown>, key: string):
14
14
  export declare function trimmed(source: Record<string, unknown>, key: string): string;
15
15
  /** The value at `key` when it is an object (not an array). @internal */
16
16
  export declare function object(source: Record<string, unknown>, key: string): LosslessObject | undefined;
17
+ /** Whether `value` is an object or array, which a gateway never signs. @internal */
18
+ export declare function isNested(value: unknown): boolean;
@@ -36,3 +36,7 @@ export function object(source, key) {
36
36
  const value = source[key];
37
37
  return isLosslessObject(value) ? value : undefined;
38
38
  }
39
+ /** Whether `value` is an object or array, which a gateway never signs. @internal */
40
+ export function isNested(value) {
41
+ return typeof value === 'object' && value !== null && !(value instanceof JsonNumber);
42
+ }
@@ -10,27 +10,24 @@ export interface CyberSourceConfigOptions {
10
10
  accessKey: string;
11
11
  /** The profile's secret key, which signs the fields. */
12
12
  secretKey: string;
13
- /** Use the test environment (default `true`). */
14
- sandbox?: boolean | undefined;
15
- /** Overrides the Secure Acceptance base URL. */
13
+ /** Overrides the Secure Acceptance base URL, e.g. to point at the test environment. */
16
14
  baseUrl?: string | undefined;
17
15
  }
18
16
  /**
19
- * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`.
17
+ * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`. The
18
+ * URL defaults to production.
20
19
  */
21
20
  export declare class CyberSourceConfig {
22
- static readonly SANDBOX_URL = "https://testsecureacceptance.cybersource.com";
23
21
  static readonly PRODUCTION_URL = "https://secureacceptance.cybersource.com";
24
22
  readonly profileId: string;
25
23
  readonly accessKey: string;
26
24
  readonly secretKey: string;
27
- readonly sandbox: boolean;
28
25
  /** The base URL in use. */
29
26
  readonly baseUrl: string;
30
27
  constructor(options: CyberSourceConfigOptions);
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?: EnvSource): CyberSourceConfig;
36
33
  }
@@ -61,12 +58,12 @@ export interface CyberSourcePaymentData {
61
58
  returnUrl?: string | undefined;
62
59
  /** The page shown on cancel (`override_custom_cancel_page`), at most 255 characters. */
63
60
  cancelUrl?: string | undefined;
64
- /** An ISO 4217 code (default `MMK`). */
65
- currency?: string | undefined;
66
- /** What to do with the card (default `sale`). */
67
- transactionType?: CyberSourceTransactionType | undefined;
68
- /** The hosted page language, e.g. `en-us` (the default). */
69
- locale?: string | undefined;
61
+ /** An ISO 4217 code, e.g. `MMK`. */
62
+ currency: string;
63
+ /** What to do with the card, e.g. `sale`. */
64
+ transactionType: CyberSourceTransactionType;
65
+ /** The hosted page language, e.g. `en-us`. */
66
+ locale: string;
70
67
  }
71
68
  /**
72
69
  * CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
@@ -87,7 +84,8 @@ export declare class CyberSource {
87
84
  initiate(data: CyberSourcePaymentData): FormPayment;
88
85
  /**
89
86
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
90
- * page.
87
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
88
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
91
89
  */
92
90
  handleCallback(request: CallbackRequest): PaymentCallback;
93
91
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
@@ -1,43 +1,39 @@
1
1
  import {} from '../core/amount.js';
2
2
  import { CallbackRequest, losslessInput, PaymentCallback } from '../core/callback.js';
3
3
  import { hmacSha256Base64, randomHex, safeEqual } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
4
+ import { defaultEnv, envFirst, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { SignatureVerificationError } from '../core/errors.js';
6
- import { toPlainObject } from '../core/json.js';
6
+ import { setKey, toPlainObject } from '../core/json.js';
7
7
  import { FormPayment } from '../core/results.js';
8
8
  import { resolveStatus } from '../core/status.js';
9
9
  import { toAmount, Validator } from '../core/validate.js';
10
10
  import { get, optional, scalarString, trimmed } from '../core/values.js';
11
11
  /**
12
- * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`.
12
+ * A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`. The
13
+ * URL defaults to production.
13
14
  */
14
15
  export class CyberSourceConfig {
15
- static SANDBOX_URL = 'https://testsecureacceptance.cybersource.com';
16
16
  static PRODUCTION_URL = 'https://secureacceptance.cybersource.com';
17
17
  profileId;
18
18
  accessKey;
19
19
  secretKey;
20
- sandbox;
21
20
  /** The base URL in use. */
22
21
  baseUrl;
23
22
  constructor(options) {
24
23
  this.profileId = requireSetting('cyber_source', 'profile_id', options.profileId);
25
24
  this.accessKey = requireSetting('cyber_source', 'access_key', options.accessKey);
26
25
  this.secretKey = requireSetting('cyber_source', 'secret_key', options.secretKey);
27
- this.sandbox = options.sandbox ?? true;
28
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
29
- (this.sandbox ? CyberSourceConfig.SANDBOX_URL : CyberSourceConfig.PRODUCTION_URL));
26
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? CyberSourceConfig.PRODUCTION_URL);
30
27
  }
31
28
  /**
32
29
  * Reads `CYBER_SOURCE_PROFILE_ID`, `CYBER_SOURCE_ACCESS_KEY`, `CYBER_SOURCE_SECRET_KEY`,
33
- * `CYBER_SOURCE_SANDBOX` and `CYBER_SOURCE_BASE_URL`.
30
+ * and `CYBER_SOURCE_BASE_URL`.
34
31
  */
35
32
  static fromEnv(env = defaultEnv()) {
36
33
  return new CyberSourceConfig({
37
34
  profileId: envFirst(env, 'CYBER_SOURCE_PROFILE_ID'),
38
35
  accessKey: envFirst(env, 'CYBER_SOURCE_ACCESS_KEY'),
39
36
  secretKey: envFirst(env, 'CYBER_SOURCE_SECRET_KEY'),
40
- sandbox: envSandbox(env, 'CYBER_SOURCE_SANDBOX'),
41
37
  baseUrl: envFirst(env, 'CYBER_SOURCE_BASE_URL'),
42
38
  });
43
39
  }
@@ -74,7 +70,7 @@ const STATUSES = {
74
70
  REVIEW: 'pending',
75
71
  DECLINE: 'failed',
76
72
  ERROR: 'failed',
77
- CANCEL: 'cancelled',
73
+ CANCEL: 'canceled',
78
74
  };
79
75
  /**
80
76
  * CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
@@ -110,9 +106,12 @@ export class CyberSource {
110
106
  .string('cancelUrl', data.cancelUrl)
111
107
  .url('cancelUrl', data.cancelUrl)
112
108
  .max('cancelUrl', data.cancelUrl, 255)
113
- .pattern('currency', String(data.currency || 'MMK'), /^[A-Z]{3}$/, 'a three letter ISO 4217 code')
114
- .pattern('locale', String(data.locale || 'en-us'), /^[a-z]{2}-[a-z]{2}$/, 'a locale code such as en-us')
115
- .when(!TRANSACTION_TYPES.includes(String(data.transactionType || 'sale')), 'transactionType', 'The transactionType field is not a supported transaction type.')
109
+ .required('currency', data.currency)
110
+ .pattern('currency', data.currency, /^[A-Z]{3}$/, 'a three letter ISO 4217 code')
111
+ .required('transactionType', data.transactionType)
112
+ .when(!TRANSACTION_TYPES.includes(String(data.transactionType)), 'transactionType', 'The transactionType field is not a supported transaction type.')
113
+ .required('locale', data.locale)
114
+ .pattern('locale', data.locale, /^[a-z]{2}-[a-z]{2}$/, 'a locale code such as en-us')
116
115
  .validate();
117
116
  }
118
117
  /**
@@ -127,11 +126,11 @@ export class CyberSource {
127
126
  transaction_uuid: randomHex(),
128
127
  signed_field_names: SIGNED_FIELDS.join(','),
129
128
  signed_date_time: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
130
- locale: data.locale || 'en-us',
131
- transaction_type: data.transactionType || 'sale',
129
+ locale: data.locale,
130
+ transaction_type: data.transactionType,
132
131
  reference_number: data.orderId,
133
132
  amount: String(toAmount(data.amount)),
134
- currency: data.currency || 'MMK',
133
+ currency: data.currency,
135
134
  override_custom_receipt_page: data.returnUrl ?? '',
136
135
  override_backoffice_post_url: data.callbackUrl,
137
136
  override_custom_cancel_page: data.cancelUrl ?? '',
@@ -146,31 +145,39 @@ export class CyberSource {
146
145
  }
147
146
  /**
148
147
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
149
- * page.
148
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
149
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
150
150
  */
151
151
  handleCallback(request) {
152
152
  const payload = losslessInput(request);
153
153
  const expected = this.sign(payload);
154
- if (expected === undefined || !safeEqual(expected, get(payload, 'signature'))) {
154
+ const names = signedNames(payload);
155
+ if (expected === undefined ||
156
+ !safeEqual(expected, get(payload, 'signature')) ||
157
+ !names.includes('decision') ||
158
+ !names.includes('req_reference_number')) {
155
159
  throw new SignatureVerificationError('CyberSource callback signature verification failed.', toPlainObject(payload));
156
160
  }
157
- const decision = trimmed(payload, 'decision').toUpperCase();
161
+ // A signature only covers the listed fields, so anything else in the post could have been
162
+ // added by the sender (e.g. a `decision` next to the signed request fields).
163
+ const signed = {};
164
+ for (const name of [...names, 'signature']) {
165
+ setKey(signed, name, payload[name]);
166
+ }
167
+ const decision = trimmed(signed, 'decision').toUpperCase();
158
168
  return new PaymentCallback({
159
- orderId: get(payload, 'req_reference_number'),
169
+ orderId: get(signed, 'req_reference_number'),
160
170
  status: resolveStatus(STATUSES, decision),
161
171
  gatewayStatus: decision,
162
- gatewayReference: optional(payload, 'transaction_id'),
163
- amount: get(payload, 'auth_amount') || optional(payload, 'req_amount'),
164
- raw: toPlainObject(payload),
172
+ gatewayReference: optional(signed, 'transaction_id'),
173
+ amount: get(signed, 'auth_amount') || optional(signed, 'req_amount'),
174
+ raw: toPlainObject(signed),
165
175
  });
166
176
  }
167
177
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
168
178
  sign(fields) {
169
179
  const pairs = [];
170
- for (const name of get(fields, 'signed_field_names').split(',')) {
171
- if (name === '') {
172
- continue;
173
- }
180
+ for (const name of signedNames(fields)) {
174
181
  const value = scalarString(fields[name]);
175
182
  if (value === undefined) {
176
183
  return undefined;
@@ -182,3 +189,9 @@ export class CyberSource {
182
189
  : hmacSha256Base64(this.config.secretKey, pairs.join(','));
183
190
  }
184
191
  }
192
+ /** The names listed in `signed_field_names`, in order. */
193
+ function signedNames(fields) {
194
+ return get(fields, 'signed_field_names')
195
+ .split(',')
196
+ .filter((name) => name !== '');
197
+ }
@@ -12,33 +12,46 @@ export interface KbzPayConfigOptions {
12
12
  appKey: string;
13
13
  /** The `merch_code` KBZ issued. */
14
14
  merchantCode: string;
15
- /** Use the UAT endpoints (default `true`). UAT and production issue separate credentials. */
16
- sandbox?: boolean | undefined;
17
- /** Overrides the API base URL, e.g. to go through a proxy. */
15
+ /**
16
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
17
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
18
+ * client you pass keeps its own timeout.
19
+ */
20
+ timeoutSeconds?: number | string | null | undefined;
21
+ /** Overrides the API base URL, e.g. to point at UAT or go through a proxy. */
18
22
  apiUrl?: string | undefined;
19
23
  /** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
20
24
  pwaUrl?: string | undefined;
25
+ /**
26
+ * Minutes the customer has to pay each order, a whole number from 1 to 120 (or its text).
27
+ * A payment's own `timeToLiveMinutes` overrides it. Unset or blank is allowed, but then every
28
+ * payment must set its own.
29
+ */
30
+ timeToLiveMinutes?: number | string | null | undefined;
21
31
  }
22
32
  /**
23
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
33
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
34
+ * default to production; UAT and production issue separate credentials.
24
35
  */
25
36
  export declare class KbzPayConfig {
26
- static readonly SANDBOX_API_URL = "http://api-uat.kbzpay.com/payment/gateway/uat";
27
37
  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
38
  static readonly PRODUCTION_PWA_URL = "https://wap.kbzpay.com/pgw/pwa/#/";
30
39
  readonly appId: string;
31
40
  readonly appKey: string;
32
41
  readonly merchantCode: string;
33
- readonly sandbox: boolean;
42
+ /** Seconds before the default HTTP client gives up on a request. */
43
+ readonly timeoutSeconds: number;
34
44
  /** The API base URL in use. */
35
45
  readonly apiUrl: string;
36
46
  /** The PWA checkout URL in use, always ending in `/`. */
37
47
  readonly pwaUrl: string;
48
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
49
+ readonly timeToLiveMinutes: number | undefined;
38
50
  constructor(options: KbzPayConfigOptions);
39
51
  /**
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`.
52
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
53
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
54
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
42
55
  */
43
56
  static fromEnv(env?: EnvSource): KbzPayConfig;
44
57
  }
@@ -54,8 +67,11 @@ export interface KbzPayPaymentData {
54
67
  callbackUrl: string;
55
68
  /** The product name shown to the customer. */
56
69
  title?: string | undefined;
57
- /** How long the order stays payable, 1 to 120 minutes. KBZ defaults to 120. */
58
- timeoutMinutes?: number | undefined;
70
+ /**
71
+ * Minutes the customer has to pay before the order expires, 1 to 120. Overrides the config's
72
+ * `timeToLiveMinutes`; one of the two is required.
73
+ */
74
+ timeToLiveMinutes?: number | undefined;
59
75
  /** Free text KBZ sends back unchanged in the callback, at most 512 characters once URL-encoded. */
60
76
  callbackInfo?: string | undefined;
61
77
  }
@@ -79,13 +95,21 @@ export declare class KbzPay {
79
95
  *
80
96
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
81
97
  * the URL registered with KBZ.
98
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
99
+ * a time to live.
82
100
  */
83
101
  pwa(data: KbzPayPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
84
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
102
+ /**
103
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
104
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
105
+ * a time to live.
106
+ */
85
107
  qr(data: KbzPayPaymentData, options?: RequestOptions): Promise<QrPayment>;
86
108
  /**
87
109
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
88
110
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
111
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
112
+ * a time to live.
89
113
  */
90
114
  app(data: KbzPayPaymentData, options?: RequestOptions): Promise<AppPayment>;
91
115
  /** Asks KBZ Pay for the current state of an order (`queryorder`). */
@@ -1,8 +1,8 @@
1
1
  import {} from '../core/amount.js';
2
2
  import { Acknowledgement, CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
3
3
  import { queryEscape, randomHex } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
- import { ApiError, SignatureVerificationError } from '../core/errors.js';
4
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSetting, resolveTimeout, trimUrl, } from '../core/env.js';
5
+ import { ApiError, ConfigurationError, InvalidPaymentDataError, SignatureVerificationError, } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { toPlainObject } from '../core/json.js';
8
8
  import { AppPayment, QrPayment, RedirectPayment } from '../core/results.js';
@@ -10,44 +10,65 @@ import { resolveStatus } from '../core/status.js';
10
10
  import { toAmount, Validator } from '../core/validate.js';
11
11
  import { get, object, optional, trimmed } from '../core/values.js';
12
12
  import { KbzPaySigner } from './signer.js';
13
+ /** What a valid KBZ Pay `time_to_live_in_minutes` is: KBZ counts whole minutes, up to 120. */
14
+ const TIME_TO_LIVE_REQUIREMENT = 'a whole number from 1 to 120';
15
+ function resolveTimeToLive(value) {
16
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
17
+ return undefined;
18
+ }
19
+ let minutes;
20
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
21
+ minutes = value;
22
+ }
23
+ else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
24
+ minutes = Number.parseInt(value.trim(), 10);
25
+ }
26
+ if (minutes === undefined || minutes < 1 || minutes > 120) {
27
+ throw new ConfigurationError('kbz_pay', 'time_to_live_in_minutes', true, TIME_TO_LIVE_REQUIREMENT);
28
+ }
29
+ return minutes;
30
+ }
13
31
  /**
14
- * KBZ Pay credentials and endpoints. A missing credential throws a `ConfigurationError`.
32
+ * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
33
+ * default to production; UAT and production issue separate credentials.
15
34
  */
16
35
  export class KbzPayConfig {
17
- static SANDBOX_API_URL = 'http://api-uat.kbzpay.com/payment/gateway/uat';
18
36
  static PRODUCTION_API_URL = 'https://api.kbzpay.com/payment/gateway';
19
- static SANDBOX_PWA_URL = 'https://static.kbzpay.com/pgw/uat/pwa/#/';
20
37
  static PRODUCTION_PWA_URL = 'https://wap.kbzpay.com/pgw/pwa/#/';
21
38
  appId;
22
39
  appKey;
23
40
  merchantCode;
24
- sandbox;
41
+ /** Seconds before the default HTTP client gives up on a request. */
42
+ timeoutSeconds;
25
43
  /** The API base URL in use. */
26
44
  apiUrl;
27
45
  /** The PWA checkout URL in use, always ending in `/`. */
28
46
  pwaUrl;
47
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
48
+ timeToLiveMinutes;
29
49
  constructor(options) {
30
50
  this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
31
51
  this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
32
52
  this.merchantCode = requireSetting('kbz_pay', 'merchant_code', options.merchantCode);
33
- this.sandbox = options.sandbox ?? true;
34
- this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ??
35
- (this.sandbox ? KbzPayConfig.SANDBOX_API_URL : KbzPayConfig.PRODUCTION_API_URL));
36
- this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ??
37
- (this.sandbox ? KbzPayConfig.SANDBOX_PWA_URL : KbzPayConfig.PRODUCTION_PWA_URL))}/`;
53
+ this.timeoutSeconds = resolveTimeout('kbz_pay', options.timeoutSeconds);
54
+ this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
55
+ this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
56
+ this.timeToLiveMinutes = resolveTimeToLive(options.timeToLiveMinutes);
38
57
  }
39
58
  /**
40
- * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`, `KBZ_PAY_SANDBOX`,
41
- * `KBZ_PAY_BASE_URL` and `KBZ_PAY_PWA_BASE_REDIRECT_URL`.
59
+ * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
60
+ * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
61
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
42
62
  */
43
63
  static fromEnv(env = defaultEnv()) {
44
64
  return new KbzPayConfig({
45
65
  appId: envFirst(env, 'KBZ_PAY_APP_ID'),
46
66
  appKey: envFirst(env, 'KBZ_PAY_APP_KEY'),
47
67
  merchantCode: envFirst(env, 'KBZ_PAY_MERCHANT_CODE'),
48
- sandbox: envSandbox(env, 'KBZ_PAY_SANDBOX'),
68
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
49
69
  apiUrl: envFirst(env, 'KBZ_PAY_BASE_URL'),
50
70
  pwaUrl: envFirst(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
71
+ timeToLiveMinutes: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_MINUTES'),
51
72
  });
52
73
  }
53
74
  }
@@ -57,7 +78,7 @@ const STATUSES = {
57
78
  WAIT_PAY: 'pending',
58
79
  PAYING: 'pending',
59
80
  PAY_FAILED: 'failed',
60
- ORDER_CLOSED: 'cancelled',
81
+ ORDER_CLOSED: 'canceled',
61
82
  ORDER_EXPIRED: 'expired',
62
83
  };
63
84
  /**
@@ -73,7 +94,7 @@ export class KbzPay {
73
94
  constructor(config, options = {}) {
74
95
  this.config = config instanceof KbzPayConfig ? config : new KbzPayConfig(config);
75
96
  this.signer = new KbzPaySigner(this.config.appKey);
76
- this.transport = new Transport(httpClientFrom(options));
97
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
77
98
  }
78
99
  /** A gateway configured from the `KBZ_PAY_*` environment variables. */
79
100
  static fromEnv(env = defaultEnv(), options = {}) {
@@ -92,7 +113,7 @@ export class KbzPay {
92
113
  .max('callbackUrl', callbackUrl, 512)
93
114
  .when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
94
115
  .string('title', data.title)
95
- .between('timeoutMinutes', data.timeoutMinutes, 1, 120)
116
+ .between('timeToLiveMinutes', data.timeToLiveMinutes, 1, 120)
96
117
  .string('callbackInfo', data.callbackInfo)
97
118
  .max('callbackInfo', typeof data.callbackInfo === 'string' ? queryEscape(data.callbackInfo) : undefined, 512)
98
119
  .validate();
@@ -102,6 +123,8 @@ export class KbzPay {
102
123
  *
103
124
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
104
125
  * the URL registered with KBZ.
126
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
127
+ * a time to live.
105
128
  */
106
129
  async pwa(data, options = {}) {
107
130
  const order = await this.precreate(data, 'PWAAPP', options);
@@ -113,7 +136,11 @@ export class KbzPay {
113
136
  raw: toPlainObject(order.response),
114
137
  });
115
138
  }
116
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
139
+ /**
140
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
141
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
142
+ * a time to live.
143
+ */
117
144
  async qr(data, options = {}) {
118
145
  const order = await this.precreate(data, 'PAY_BY_QRCODE', options);
119
146
  const qrCode = get(order.response, 'qrCode');
@@ -125,9 +152,7 @@ export class KbzPay {
125
152
  return new QrPayment({
126
153
  orderId: data.orderId,
127
154
  qrString: qrCode,
128
- expiresAt: data.timeoutMinutes === undefined
129
- ? undefined
130
- : new Date(Date.now() + data.timeoutMinutes * 60_000),
155
+ expiresAt: new Date(Date.now() + order.timeToLiveMinutes * 60_000),
131
156
  reference: order.prepayId,
132
157
  raw: toPlainObject(order.response),
133
158
  });
@@ -135,6 +160,8 @@ export class KbzPay {
135
160
  /**
136
161
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
137
162
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
163
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
164
+ * a time to live.
138
165
  */
139
166
  async app(data, options = {}) {
140
167
  const order = await this.precreate(data, 'APP', options);
@@ -187,6 +214,12 @@ export class KbzPay {
187
214
  }
188
215
  async precreate(data, tradeType, options) {
189
216
  KbzPay.validate(data);
217
+ const timeToLiveMinutes = data.timeToLiveMinutes ?? this.config.timeToLiveMinutes;
218
+ if (timeToLiveMinutes === undefined) {
219
+ throw new InvalidPaymentDataError({
220
+ timeToLiveMinutes: 'The timeToLiveMinutes field is required when the kbz_pay configuration has no [time_to_live_in_minutes].',
221
+ });
222
+ }
190
223
  const biz = {
191
224
  appid: this.config.appId,
192
225
  merch_code: this.config.merchantCode,
@@ -198,9 +231,7 @@ export class KbzPay {
198
231
  if (data.title) {
199
232
  biz.title = data.title;
200
233
  }
201
- if (data.timeoutMinutes !== undefined) {
202
- biz.timeout_express = `${data.timeoutMinutes}m`;
203
- }
234
+ biz.timeout_express = `${timeToLiveMinutes}m`;
204
235
  if (data.callbackInfo) {
205
236
  biz.callback_info = queryEscape(data.callbackInfo);
206
237
  }
@@ -211,7 +242,7 @@ export class KbzPay {
211
242
  if (prepayId === '') {
212
243
  throw new ApiError('KBZ Pay did not return a prepay_id.', { raw: toPlainObject(response) });
213
244
  }
214
- return { prepayId, nonce, timestamp: time, response };
245
+ return { prepayId, nonce, timestamp: time, timeToLiveMinutes, response };
215
246
  }
216
247
  /** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
217
248
  async call(endpoint, method, version, biz, extra, nonce, time, signal) {
@@ -10,6 +10,9 @@ export declare class KbzPaySigner {
10
10
  signString(fields: Readonly<Record<string, unknown>>): string;
11
11
  /** The uppercase SHA-256 signature of `fields`. */
12
12
  sign(fields: Readonly<Record<string, unknown>>): string;
13
- /** Whether `fields.sign` matches, compared in constant time. */
13
+ /**
14
+ * Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
15
+ * fields holding an object or array fail rather than being partly trusted.
16
+ */
14
17
  verify(fields: Readonly<Record<string, unknown>>): boolean;
15
18
  }
@@ -1,5 +1,5 @@
1
1
  import { safeEqual, sha256Hex } from '../core/crypto.js';
2
- import { scalarString } from '../core/values.js';
2
+ import { isNested, scalarString } from '../core/values.js';
3
3
  /**
4
4
  * KBZ Pay's signature: every non-empty scalar field except `sign` and `sign_type`, sorted by key,
5
5
  * joined as raw `key=value` pairs (not URL-encoded), with `&key=<app key>` appended, hashed with
@@ -24,9 +24,14 @@ export class KbzPaySigner {
24
24
  sign(fields) {
25
25
  return sha256Hex(`${this.signString(fields)}&key=${this.appKey}`).toUpperCase();
26
26
  }
27
- /** Whether `fields.sign` matches, compared in constant time. */
27
+ /**
28
+ * Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
29
+ * fields holding an object or array fail rather than being partly trusted.
30
+ */
28
31
  verify(fields) {
29
32
  const sign = fields.sign;
30
- return typeof sign === 'string' && safeEqual(this.sign(fields), sign.toUpperCase());
33
+ return (typeof sign === 'string' &&
34
+ !Object.values(fields).some(isNested) &&
35
+ safeEqual(this.sign(fields), sign.toUpperCase()));
31
36
  }
32
37
  }