@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
@@ -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>;
@@ -5,14 +5,17 @@ import { parseJsonObject, setKey, toPlainObject } from './json.js';
5
5
  * {@link CallbackRequest.fromNodeRequest} or {@link CallbackRequest.fromWebRequest}.
6
6
  */
7
7
  export class CallbackRequest {
8
- /** The raw request body, decoded as UTF-8. */
8
+ /** The request body as text: a string body as given, bytes decoded as UTF-8. */
9
9
  body;
10
+ /** The exact bytes received. A string body is encoded as UTF-8. */
11
+ rawBody;
10
12
  /** The request headers, with lowercase names. Repeated headers are joined with `, `. */
11
13
  headers;
12
14
  /** The query string parameters (the first value of each). */
13
15
  query;
14
16
  constructor(init = {}) {
15
- this.body = bodyText(init.body);
17
+ this.rawBody = bodyBytes(init.body);
18
+ this.body = typeof init.body === 'string' ? init.body : new TextDecoder().decode(this.rawBody);
16
19
  this.headers = Object.freeze(normalizeHeaders(init.headers));
17
20
  this.query = Object.freeze(normalizeQuery(init.query));
18
21
  }
@@ -31,10 +34,11 @@ export class CallbackRequest {
31
34
  });
32
35
  }
33
36
  /**
34
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
35
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
36
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
37
- * again as JSON or a form.
37
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
38
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
39
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
40
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
41
+ * {@link CallbackRequest.from} with the raw body there.
38
42
  */
39
43
  static async fromNodeRequest(request) {
40
44
  const headers = normalizeHeaders(request.headers);
@@ -67,7 +71,10 @@ export class CallbackRequest {
67
71
  header(name) {
68
72
  return this.headers[name.toLowerCase()];
69
73
  }
70
- /** The body decoded as JSON or as a urlencoded form. */
74
+ /**
75
+ * The body decoded as JSON or as a urlencoded form. JSON numbers keep their exact text, as strings
76
+ * (`1000.50` stays `"1000.50"`).
77
+ */
71
78
  parsedBody() {
72
79
  return toPlainObject(losslessBody(this));
73
80
  }
@@ -206,14 +213,15 @@ export class PaymentStatusResult {
206
213
  return this.status === 'successful';
207
214
  }
208
215
  }
209
- function bodyText(body) {
216
+ function bodyBytes(body) {
210
217
  if (body === undefined || body === null) {
211
- return '';
218
+ return new Uint8Array();
212
219
  }
213
220
  if (typeof body === 'string') {
214
- return body;
221
+ return new TextEncoder().encode(body);
215
222
  }
216
- return new TextDecoder().decode(body);
223
+ // A copy, so later changes to the caller's buffer cannot alter what was verified.
224
+ return body instanceof ArrayBuffer ? new Uint8Array(body.slice(0)) : Uint8Array.from(body);
217
225
  }
218
226
  function rawBody(value) {
219
227
  if (typeof value === 'string' || value instanceof Uint8Array || value instanceof ArrayBuffer) {
@@ -15,5 +15,10 @@ export declare function randomHex(bytes?: number): string;
15
15
  * @internal
16
16
  */
17
17
  export declare function queryEscape(value: string): string;
18
- /** Decodes strict standard base64 (with padding), or `undefined`. @internal */
18
+ /**
19
+ * Decodes standard base64 (`A-Z a-z 0-9 + /`), either correctly padded or without any padding, to
20
+ * UTF-8 text. Partial padding, other characters and bytes that are not UTF-8 return `undefined`.
21
+ *
22
+ * @internal
23
+ */
19
24
  export declare function decodeBase64(value: string): string | undefined;
@@ -32,10 +32,24 @@ export function queryEscape(value) {
32
32
  .replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
33
33
  .replace(/%20/g, '+');
34
34
  }
35
- /** Decodes strict standard base64 (with padding), or `undefined`. @internal */
35
+ const UTF8 = new TextDecoder('utf-8', { fatal: true });
36
+ /**
37
+ * Decodes standard base64 (`A-Z a-z 0-9 + /`), either correctly padded or without any padding, to
38
+ * UTF-8 text. Partial padding, other characters and bytes that are not UTF-8 return `undefined`.
39
+ *
40
+ * @internal
41
+ */
36
42
  export function decodeBase64(value) {
37
- if (value === '' || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
43
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
44
+ return undefined;
45
+ }
46
+ if (value.includes('=') ? value.length % 4 !== 0 : value.length % 4 === 1) {
47
+ return undefined;
48
+ }
49
+ try {
50
+ return UTF8.decode(Buffer.from(value, 'base64'));
51
+ }
52
+ catch {
38
53
  return undefined;
39
54
  }
40
- return Buffer.from(value, 'base64').toString('utf8');
41
55
  }
@@ -3,14 +3,30 @@ export type EnvSource = Readonly<Record<string, string | undefined>>;
3
3
  /** The first non-empty value among `keys`, trimmed, or `''`. @internal */
4
4
  export declare function envFirst(env: EnvSource, ...keys: string[]): string;
5
5
  /**
6
- * Reads a `*_SANDBOX` variable. Only `false`, `0`, `f`, `no` and `off` (any case) select
7
- * production; unset or unrecognized values mean sandbox.
6
+ * A required whole number of seconds: an integer greater than 0, or text such as `'300'`. Unset
7
+ * or blank throws the missing {@link ConfigurationError}; anything else throws the invalid one.
8
8
  *
9
9
  * @internal
10
10
  */
11
- export declare function envSandbox(env: EnvSource, key: string): boolean;
12
- /** An integer variable, or `undefined` when unset or not an integer. @internal */
13
- export declare function envInt(env: EnvSource, key: string): number | undefined;
11
+ export declare function requireSeconds(gateway: string, key: string, value: unknown): number;
12
+ /**
13
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
14
+ * as {@link requireSeconds}.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function optionalSeconds(gateway: string, key: string, value: unknown): number | undefined;
19
+ /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
20
+ export declare const HTTP_TIMEOUT_VARIABLE = "MYANMAR_PAYMENTS_HTTP_TIMEOUT";
21
+ /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
22
+ export declare const DEFAULT_TIMEOUT_SECONDS = 30;
23
+ /**
24
+ * The HTTP timeout in seconds: {@link DEFAULT_TIMEOUT_SECONDS} when unset or blank, otherwise a
25
+ * whole number greater than 0 (or its text), or the invalid {@link ConfigurationError}.
26
+ *
27
+ * @internal
28
+ */
29
+ export declare function resolveTimeout(gateway: string, value: unknown): number;
14
30
  /** The trimmed value, or throws a {@link ConfigurationError} naming `key`. @internal */
15
31
  export declare function requireSetting(gateway: string, key: string, value: unknown): string;
16
32
  /** A trimmed optional string, `undefined` when blank. @internal */
@@ -10,19 +10,54 @@ export function envFirst(env, ...keys) {
10
10
  return '';
11
11
  }
12
12
  /**
13
- * Reads a `*_SANDBOX` variable. Only `false`, `0`, `f`, `no` and `off` (any case) select
14
- * production; unset or unrecognized values mean sandbox.
13
+ * A required whole number of seconds: an integer greater than 0, or text such as `'300'`. Unset
14
+ * or blank throws the missing {@link ConfigurationError}; anything else throws the invalid one.
15
15
  *
16
16
  * @internal
17
17
  */
18
- export function envSandbox(env, key) {
19
- const value = env[key]?.trim().toLowerCase() ?? '';
20
- return !['false', '0', 'f', 'no', 'off'].includes(value);
21
- }
22
- /** An integer variable, or `undefined` when unset or not an integer. @internal */
23
- export function envInt(env, key) {
24
- const value = env[key]?.trim() ?? '';
25
- return /^[+-]?\d+$/.test(value) ? Number.parseInt(value, 10) : undefined;
18
+ export function requireSeconds(gateway, key, value) {
19
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
20
+ throw new ConfigurationError(gateway, key);
21
+ }
22
+ let seconds;
23
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
24
+ seconds = value;
25
+ }
26
+ else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
27
+ seconds = Number.parseInt(value.trim(), 10);
28
+ }
29
+ if (seconds === undefined || !Number.isSafeInteger(seconds) || seconds <= 0) {
30
+ throw new ConfigurationError(gateway, key, true);
31
+ }
32
+ return seconds;
33
+ }
34
+ /**
35
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
36
+ * as {@link requireSeconds}.
37
+ *
38
+ * @internal
39
+ */
40
+ export function optionalSeconds(gateway, key, value) {
41
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
42
+ return undefined;
43
+ }
44
+ return requireSeconds(gateway, key, value);
45
+ }
46
+ /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
47
+ export const HTTP_TIMEOUT_VARIABLE = 'MYANMAR_PAYMENTS_HTTP_TIMEOUT';
48
+ /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
49
+ export const DEFAULT_TIMEOUT_SECONDS = 30;
50
+ /**
51
+ * The HTTP timeout in seconds: {@link DEFAULT_TIMEOUT_SECONDS} when unset or blank, otherwise a
52
+ * whole number greater than 0 (or its text), or the invalid {@link ConfigurationError}.
53
+ *
54
+ * @internal
55
+ */
56
+ export function resolveTimeout(gateway, value) {
57
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
58
+ return DEFAULT_TIMEOUT_SECONDS;
59
+ }
60
+ return requireSeconds(gateway, 'timeout_in_seconds', value);
26
61
  }
27
62
  /** The trimmed value, or throws a {@link ConfigurationError} naming `key`. @internal */
28
63
  export function requireSetting(gateway, key, value) {
@@ -45,12 +45,18 @@ export declare class SignatureVerificationError extends PaymentError {
45
45
  constructor(message: string, raw?: Record<string, unknown>);
46
46
  }
47
47
  /**
48
- * Thrown when a gateway is missing a credential or setting it needs.
48
+ * Thrown when a gateway is missing a credential or setting it needs, or a whole-number setting
49
+ * is not a whole number greater than 0.
49
50
  */
50
51
  export declare class ConfigurationError extends PaymentError {
51
52
  /** The gateway, e.g. `kbz_pay`. */
52
53
  readonly gateway: string;
53
- /** The missing setting, e.g. `app_key`. */
54
+ /** The missing or invalid setting, e.g. `app_key`. */
54
55
  readonly key: string;
55
- constructor(gateway: string, key: string);
56
+ /**
57
+ * @param invalid `true` when the setting is set but out of range; `false` (the default) when it
58
+ * is missing.
59
+ * @param requirement What a valid value is, used in the message when `invalid`.
60
+ */
61
+ constructor(gateway: string, key: string, invalid?: boolean, requirement?: string);
56
62
  }
@@ -55,15 +55,23 @@ export class SignatureVerificationError extends PaymentError {
55
55
  }
56
56
  }
57
57
  /**
58
- * Thrown when a gateway is missing a credential or setting it needs.
58
+ * Thrown when a gateway is missing a credential or setting it needs, or a whole-number setting
59
+ * is not a whole number greater than 0.
59
60
  */
60
61
  export class ConfigurationError extends PaymentError {
61
62
  /** The gateway, e.g. `kbz_pay`. */
62
63
  gateway;
63
- /** The missing setting, e.g. `app_key`. */
64
+ /** The missing or invalid setting, e.g. `app_key`. */
64
65
  key;
65
- constructor(gateway, key) {
66
- super(`The ${gateway} configuration is missing [${key}].`);
66
+ /**
67
+ * @param invalid `true` when the setting is set but out of range; `false` (the default) when it
68
+ * is missing.
69
+ * @param requirement What a valid value is, used in the message when `invalid`.
70
+ */
71
+ constructor(gateway, key, invalid = false, requirement = 'a whole number greater than 0') {
72
+ super(invalid
73
+ ? `The ${gateway} configuration [${key}] must be ${requirement}.`
74
+ : `The ${gateway} configuration is missing [${key}].`);
67
75
  this.gateway = gateway;
68
76
  this.key = key;
69
77
  }
@@ -27,29 +27,28 @@ export type FetchFunction = (input: string, init: RequestInit) => Promise<Respon
27
27
  export interface FetchHttpClientOptions {
28
28
  /** The `fetch` to call; defaults to the global `fetch`. */
29
29
  fetch?: FetchFunction | undefined;
30
- /** Milliseconds before a request is aborted; defaults to 30000. `0` disables the timeout. */
31
- timeoutMs?: number | undefined;
30
+ /** Milliseconds before a request is aborted. `0` disables the timeout. */
31
+ timeoutMs: number;
32
32
  }
33
- /** The default request timeout: 30 seconds. */
34
- export declare const DEFAULT_TIMEOUT_MS = 30000;
35
33
  /**
36
- * The default {@link HttpClient}: the global `fetch` (or the one you pass) with a 30 second
37
- * timeout.
34
+ * The default {@link HttpClient}: the global `fetch` (or the one you pass) with the timeout you
35
+ * give it. Gateways build one with their `timeoutSeconds` setting when you pass no client.
38
36
  */
39
37
  export declare class FetchHttpClient implements HttpClient {
40
38
  private readonly fetchFunction;
41
39
  private readonly timeoutMs;
42
- constructor(options?: FetchHttpClientOptions);
40
+ constructor(options: FetchHttpClientOptions);
43
41
  send(request: HttpRequest): Promise<HttpResponse>;
44
42
  }
45
43
  /** Options every gateway that calls an API accepts. */
46
44
  export interface GatewayOptions {
47
- /** Sends the requests. Takes precedence over `fetch` and `timeoutMs`. */
45
+ /**
46
+ * Sends the requests, with its own timeout. Takes precedence over `fetch`. Without one, the
47
+ * gateway uses a {@link FetchHttpClient} with the config's `timeoutSeconds`.
48
+ */
48
49
  httpClient?: HttpClient | undefined;
49
50
  /** The `fetch` the default client calls, e.g. a mocked one in tests. */
50
51
  fetch?: FetchFunction | undefined;
51
- /** Milliseconds before the default client aborts a request; defaults to 30000. */
52
- timeoutMs?: number | undefined;
53
52
  }
54
53
  /** Per-call options of every network method. */
55
54
  export interface RequestOptions {
@@ -57,7 +56,7 @@ export interface RequestOptions {
57
56
  signal?: AbortSignal | undefined;
58
57
  }
59
58
  /** @internal */
60
- export declare function httpClientFrom(options?: GatewayOptions): HttpClient;
59
+ export declare function httpClientFrom(options: GatewayOptions, timeoutSeconds: number): HttpClient;
61
60
  /** A decoded gateway response. @internal */
62
61
  export declare class GatewayResponse {
63
62
  readonly status: number;
@@ -1,17 +1,15 @@
1
1
  import { ApiError, PaymentError } from './errors.js';
2
2
  import { parseJsonObject } from './json.js';
3
- /** The default request timeout: 30 seconds. */
4
- export const DEFAULT_TIMEOUT_MS = 30_000;
5
3
  /**
6
- * The default {@link HttpClient}: the global `fetch` (or the one you pass) with a 30 second
7
- * timeout.
4
+ * The default {@link HttpClient}: the global `fetch` (or the one you pass) with the timeout you
5
+ * give it. Gateways build one with their `timeoutSeconds` setting when you pass no client.
8
6
  */
9
7
  export class FetchHttpClient {
10
8
  fetchFunction;
11
9
  timeoutMs;
12
- constructor(options = {}) {
10
+ constructor(options) {
13
11
  this.fetchFunction = options.fetch;
14
- this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
+ this.timeoutMs = options.timeoutMs;
15
13
  }
16
14
  async send(request) {
17
15
  const fetchFunction = this.fetchFunction ?? globalThis.fetch;
@@ -38,9 +36,9 @@ export class FetchHttpClient {
38
36
  }
39
37
  }
40
38
  /** @internal */
41
- export function httpClientFrom(options = {}) {
39
+ export function httpClientFrom(options, timeoutSeconds) {
42
40
  return (options.httpClient ??
43
- new FetchHttpClient({ fetch: options.fetch, timeoutMs: options.timeoutMs }));
41
+ new FetchHttpClient({ fetch: options.fetch, timeoutMs: timeoutSeconds * 1000 }));
44
42
  }
45
43
  /** A decoded gateway response. @internal */
46
44
  export class GatewayResponse {
@@ -1,8 +1,8 @@
1
1
  export { Amount, type AmountInput } from './amount.js';
2
2
  export { MemoryTokenCache, type TokenCache } from './cache.js';
3
3
  export { Acknowledgement, CallbackRequest, PaymentCallback, PaymentStatusResult, type AcknowledgementInit, type BodyInput, type CallbackRequestInit, type HeadersInput, type NodeRequestLike, type PaymentCallbackInit, type PaymentStatusResultInit, type QueryInput, type ServerResponseLike, type WebRequestLike, } from './callback.js';
4
- export type { EnvSource } from './env.js';
4
+ export { DEFAULT_TIMEOUT_SECONDS, type EnvSource } from './env.js';
5
5
  export { ApiError, ConfigurationError, InvalidPaymentDataError, PaymentError, SignatureVerificationError, } from './errors.js';
6
- export { DEFAULT_TIMEOUT_MS, FetchHttpClient, type FetchFunction, type FetchHttpClientOptions, type GatewayOptions, type HttpClient, type HttpRequest, type HttpResponse, type RequestOptions, } from './http.js';
6
+ export { FetchHttpClient, type FetchFunction, type FetchHttpClientOptions, type GatewayOptions, type HttpClient, type HttpRequest, type HttpResponse, type RequestOptions, } from './http.js';
7
7
  export { AppPayment, FormPayment, QrPayment, RedirectPayment, type FormField, type PaymentResult, } from './results.js';
8
8
  export { PaymentFlow, PaymentStatus, resolveStatus } from './status.js';
@@ -1,7 +1,8 @@
1
1
  export { Amount } from './amount.js';
2
2
  export { MemoryTokenCache } from './cache.js';
3
3
  export { Acknowledgement, CallbackRequest, PaymentCallback, PaymentStatusResult, } from './callback.js';
4
+ export { DEFAULT_TIMEOUT_SECONDS } from './env.js';
4
5
  export { ApiError, ConfigurationError, InvalidPaymentDataError, PaymentError, SignatureVerificationError, } from './errors.js';
5
- export { DEFAULT_TIMEOUT_MS, FetchHttpClient, } from './http.js';
6
+ export { FetchHttpClient, } from './http.js';
6
7
  export { AppPayment, FormPayment, QrPayment, RedirectPayment, } from './results.js';
7
8
  export { PaymentFlow, PaymentStatus, resolveStatus } from './status.js';
@@ -32,8 +32,8 @@ export declare function parseJsonObject(text: string): LosslessObject | undefine
32
32
  /** @internal */
33
33
  export declare function isLosslessObject(value: unknown): value is LosslessObject;
34
34
  /**
35
- * Converts a lossless value to plain JavaScript values (numbers become `number`), for the `raw`
36
- * fields of results.
35
+ * Converts a lossless value to plain JavaScript values for the `raw` fields of results. Numbers
36
+ * become their exact text as a string (`1000.50` stays `"1000.50"`), so no precision is lost.
37
37
  *
38
38
  * @internal
39
39
  */
@@ -46,14 +46,14 @@ export function isLosslessObject(value) {
46
46
  !(value instanceof JsonNumber));
47
47
  }
48
48
  /**
49
- * Converts a lossless value to plain JavaScript values (numbers become `number`), for the `raw`
50
- * fields of results.
49
+ * Converts a lossless value to plain JavaScript values for the `raw` fields of results. Numbers
50
+ * become their exact text as a string (`1000.50` stays `"1000.50"`), so no precision is lost.
51
51
  *
52
52
  * @internal
53
53
  */
54
54
  export function toPlain(value) {
55
55
  if (value instanceof JsonNumber) {
56
- return Number(value.text);
56
+ return value.text;
57
57
  }
58
58
  if (Array.isArray(value)) {
59
59
  return value.map(toPlain);