@laranex/myanmar-payments 4.0.0-dev.1 → 4.0.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -4
- package/README.md +3 -3
- package/dist/cjs/aya-pay/aya-pay.d.ts +13 -9
- package/dist/cjs/aya-pay/aya-pay.js +16 -12
- package/dist/cjs/core/amount.d.ts +4 -3
- package/dist/cjs/core/amount.js +13 -4
- package/dist/cjs/core/callback.d.ts +14 -8
- package/dist/cjs/core/callback.js +19 -11
- package/dist/cjs/core/crypto.d.ts +6 -1
- package/dist/cjs/core/crypto.js +17 -3
- package/dist/cjs/core/env.d.ts +14 -5
- package/dist/cjs/core/env.js +35 -11
- package/dist/cjs/core/errors.d.ts +9 -3
- package/dist/cjs/core/errors.js +12 -4
- package/dist/cjs/core/http.d.ts +10 -11
- package/dist/cjs/core/http.js +7 -9
- package/dist/cjs/core/index.d.ts +2 -2
- package/dist/cjs/core/index.js +3 -2
- package/dist/cjs/core/json.d.ts +2 -2
- package/dist/cjs/core/json.js +3 -3
- package/dist/cjs/core/status.d.ts +3 -3
- package/dist/cjs/core/status.js +2 -2
- package/dist/cjs/core/values.d.ts +2 -0
- package/dist/cjs/core/values.js +5 -0
- package/dist/cjs/cyber-source/cyber-source.d.ts +12 -14
- package/dist/cjs/cyber-source/cyber-source.js +39 -26
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +27 -11
- package/dist/cjs/kbz-pay/kbz-pay.js +43 -20
- package/dist/cjs/kbz-pay/signer.d.ts +4 -1
- package/dist/cjs/kbz-pay/signer.js +7 -2
- package/dist/cjs/wave-money/wave-money.d.ts +26 -13
- package/dist/cjs/wave-money/wave-money.js +29 -29
- package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +17 -13
- package/dist/cjs/yoma-mmqr/yoma-mmqr.js +21 -19
- package/dist/esm/aya-pay/aya-pay.d.ts +13 -9
- package/dist/esm/aya-pay/aya-pay.js +18 -14
- package/dist/esm/core/amount.d.ts +4 -3
- package/dist/esm/core/amount.js +13 -4
- package/dist/esm/core/callback.d.ts +14 -8
- package/dist/esm/core/callback.js +19 -11
- package/dist/esm/core/crypto.d.ts +6 -1
- package/dist/esm/core/crypto.js +17 -3
- package/dist/esm/core/env.d.ts +14 -5
- package/dist/esm/core/env.js +32 -9
- package/dist/esm/core/errors.d.ts +9 -3
- package/dist/esm/core/errors.js +12 -4
- package/dist/esm/core/http.d.ts +10 -11
- package/dist/esm/core/http.js +6 -8
- package/dist/esm/core/index.d.ts +2 -2
- package/dist/esm/core/index.js +2 -1
- package/dist/esm/core/json.d.ts +2 -2
- package/dist/esm/core/json.js +3 -3
- package/dist/esm/core/status.d.ts +3 -3
- package/dist/esm/core/status.js +2 -2
- package/dist/esm/core/values.d.ts +2 -0
- package/dist/esm/core/values.js +4 -0
- package/dist/esm/cyber-source/cyber-source.d.ts +12 -14
- package/dist/esm/cyber-source/cyber-source.js +41 -28
- package/dist/esm/kbz-pay/kbz-pay.d.ts +27 -11
- package/dist/esm/kbz-pay/kbz-pay.js +45 -22
- package/dist/esm/kbz-pay/signer.d.ts +4 -1
- package/dist/esm/kbz-pay/signer.js +8 -3
- package/dist/esm/wave-money/wave-money.d.ts +26 -13
- package/dist/esm/wave-money/wave-money.js +31 -31
- package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +17 -13
- package/dist/esm/yoma-mmqr/yoma-mmqr.js +23 -21
- package/package.json +2 -2
- package/skills/node-myanmar-payments/SKILL.md +26 -22
|
@@ -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,
|
|
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.
|
|
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
|
-
*
|
|
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: '
|
|
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
|
-
.
|
|
114
|
-
.pattern('
|
|
115
|
-
.
|
|
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
|
|
131
|
-
transaction_type: data.transactionType
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
169
|
+
orderId: get(signed, 'req_reference_number'),
|
|
160
170
|
status: resolveStatus(STATUSES, decision),
|
|
161
171
|
gatewayStatus: decision,
|
|
162
|
-
gatewayReference: optional(
|
|
163
|
-
amount: get(
|
|
164
|
-
raw: toPlainObject(
|
|
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
|
|
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,45 @@ export interface KbzPayConfigOptions {
|
|
|
12
12
|
appKey: string;
|
|
13
13
|
/** The `merch_code` KBZ issued. */
|
|
14
14
|
merchantCode: string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
* Seconds the customer has to pay each order, a multiple of 60 from 60 to 7200 (or its text).
|
|
27
|
+
* A payment's own `timeToLiveSeconds` overrides it; unset or blank leaves it to KBZ (7200).
|
|
28
|
+
*/
|
|
29
|
+
timeToLiveSeconds?: number | string | null | undefined;
|
|
21
30
|
}
|
|
22
31
|
/**
|
|
23
|
-
* KBZ Pay credentials and endpoints. A missing
|
|
32
|
+
* KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
|
|
33
|
+
* default to production; UAT and production issue separate credentials.
|
|
24
34
|
*/
|
|
25
35
|
export declare class KbzPayConfig {
|
|
26
|
-
static readonly SANDBOX_API_URL = "http://api-uat.kbzpay.com/payment/gateway/uat";
|
|
27
36
|
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
37
|
static readonly PRODUCTION_PWA_URL = "https://wap.kbzpay.com/pgw/pwa/#/";
|
|
30
38
|
readonly appId: string;
|
|
31
39
|
readonly appKey: string;
|
|
32
40
|
readonly merchantCode: string;
|
|
33
|
-
|
|
41
|
+
/** Seconds before the default HTTP client gives up on a request. */
|
|
42
|
+
readonly timeoutSeconds: number;
|
|
34
43
|
/** The API base URL in use. */
|
|
35
44
|
readonly apiUrl: string;
|
|
36
45
|
/** The PWA checkout URL in use, always ending in `/`. */
|
|
37
46
|
readonly pwaUrl: string;
|
|
47
|
+
/** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
|
|
48
|
+
readonly timeToLiveSeconds: number | undefined;
|
|
38
49
|
constructor(options: KbzPayConfigOptions);
|
|
39
50
|
/**
|
|
40
|
-
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
41
|
-
* `KBZ_PAY_BASE_URL` and
|
|
51
|
+
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
52
|
+
* `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
|
|
53
|
+
* `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
|
|
42
54
|
*/
|
|
43
55
|
static fromEnv(env?: EnvSource): KbzPayConfig;
|
|
44
56
|
}
|
|
@@ -54,8 +66,12 @@ export interface KbzPayPaymentData {
|
|
|
54
66
|
callbackUrl: string;
|
|
55
67
|
/** The product name shown to the customer. */
|
|
56
68
|
title?: string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Seconds the customer has to pay before the order expires, 60 to 7200 in whole minutes (a multiple
|
|
71
|
+
* of 60). KBZ counts minutes, so 1800 is sent as `30m`. Overrides the config's `timeToLiveSeconds`;
|
|
72
|
+
* when both are unset KBZ allows 7200 (120 minutes).
|
|
73
|
+
*/
|
|
74
|
+
timeToLiveSeconds?: 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
|
}
|
|
@@ -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,
|
|
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, 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_seconds` is: KBZ counts whole minutes, up to 120. */
|
|
14
|
+
const TIME_TO_LIVE_REQUIREMENT = 'a multiple of 60 from 60 to 7200';
|
|
15
|
+
function resolveTimeToLive(value) {
|
|
16
|
+
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
let seconds;
|
|
20
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
21
|
+
seconds = value;
|
|
22
|
+
}
|
|
23
|
+
else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
|
|
24
|
+
seconds = Number.parseInt(value.trim(), 10);
|
|
25
|
+
}
|
|
26
|
+
if (seconds === undefined || seconds < 60 || seconds > 7200 || seconds % 60 !== 0) {
|
|
27
|
+
throw new ConfigurationError('kbz_pay', 'time_to_live_in_seconds', true, TIME_TO_LIVE_REQUIREMENT);
|
|
28
|
+
}
|
|
29
|
+
return seconds;
|
|
30
|
+
}
|
|
13
31
|
/**
|
|
14
|
-
* KBZ Pay credentials and endpoints. A missing
|
|
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
|
-
|
|
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
|
+
/** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
|
|
48
|
+
timeToLiveSeconds;
|
|
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.
|
|
34
|
-
this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ??
|
|
35
|
-
|
|
36
|
-
this.
|
|
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.timeToLiveSeconds = resolveTimeToLive(options.timeToLiveSeconds);
|
|
38
57
|
}
|
|
39
58
|
/**
|
|
40
|
-
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
41
|
-
* `KBZ_PAY_BASE_URL` and
|
|
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_SECONDS`.
|
|
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
|
-
|
|
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
|
+
timeToLiveSeconds: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_SECONDS'),
|
|
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: '
|
|
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,8 @@ 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('
|
|
116
|
+
.between('timeToLiveSeconds', data.timeToLiveSeconds, 60, 7200)
|
|
117
|
+
.when(Number.isInteger(data.timeToLiveSeconds) && data.timeToLiveSeconds % 60 !== 0, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be a multiple of 60.')
|
|
96
118
|
.string('callbackInfo', data.callbackInfo)
|
|
97
119
|
.max('callbackInfo', typeof data.callbackInfo === 'string' ? queryEscape(data.callbackInfo) : undefined, 512)
|
|
98
120
|
.validate();
|
|
@@ -125,9 +147,9 @@ export class KbzPay {
|
|
|
125
147
|
return new QrPayment({
|
|
126
148
|
orderId: data.orderId,
|
|
127
149
|
qrString: qrCode,
|
|
128
|
-
expiresAt:
|
|
150
|
+
expiresAt: order.timeToLiveSeconds === undefined
|
|
129
151
|
? undefined
|
|
130
|
-
: new Date(Date.now() +
|
|
152
|
+
: new Date(Date.now() + order.timeToLiveSeconds * 1000),
|
|
131
153
|
reference: order.prepayId,
|
|
132
154
|
raw: toPlainObject(order.response),
|
|
133
155
|
});
|
|
@@ -198,8 +220,9 @@ export class KbzPay {
|
|
|
198
220
|
if (data.title) {
|
|
199
221
|
biz.title = data.title;
|
|
200
222
|
}
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
|
|
224
|
+
if (timeToLiveSeconds !== undefined) {
|
|
225
|
+
biz.timeout_express = `${timeToLiveSeconds / 60}m`;
|
|
203
226
|
}
|
|
204
227
|
if (data.callbackInfo) {
|
|
205
228
|
biz.callback_info = queryEscape(data.callbackInfo);
|
|
@@ -211,7 +234,7 @@ export class KbzPay {
|
|
|
211
234
|
if (prepayId === '') {
|
|
212
235
|
throw new ApiError('KBZ Pay did not return a prepay_id.', { raw: toPlainObject(response) });
|
|
213
236
|
}
|
|
214
|
-
return { prepayId, nonce, timestamp: time, response };
|
|
237
|
+
return { prepayId, nonce, timestamp: time, timeToLiveSeconds, response };
|
|
215
238
|
}
|
|
216
239
|
/** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
|
|
217
240
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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' &&
|
|
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,41 @@ export interface WaveMoneyConfigOptions {
|
|
|
11
11
|
secretKey: string;
|
|
12
12
|
/** Your business name, shown on Wave's payment page. */
|
|
13
13
|
merchantName: string;
|
|
14
|
-
/** How long the customer has to pay, in seconds (
|
|
15
|
-
timeToLiveSeconds
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/** How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). */
|
|
15
|
+
timeToLiveSeconds: number | string;
|
|
16
|
+
/**
|
|
17
|
+
* Seconds before the default HTTP client gives up on a request, a whole number greater than 0
|
|
18
|
+
* (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
|
|
19
|
+
* client you pass keeps its own timeout.
|
|
20
|
+
*/
|
|
21
|
+
timeoutSeconds?: number | string | null | undefined;
|
|
22
|
+
/** Overrides the API base URL, e.g. to point at UAT. */
|
|
19
23
|
baseUrl?: string | undefined;
|
|
20
24
|
/** Overrides the host the customer is redirected to. Wave serves it without the API port. */
|
|
21
25
|
authenticateUrl?: string | undefined;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
|
-
* Wave Money credentials and endpoints. A missing
|
|
28
|
+
* Wave Money credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
|
|
29
|
+
* default to production.
|
|
25
30
|
*/
|
|
26
31
|
export declare class WaveMoneyConfig {
|
|
27
|
-
static readonly SANDBOX_URL = "https://preprodpayments.wavemoney.io:8107";
|
|
28
32
|
static readonly PRODUCTION_URL = "https://payments.wavemoney.io";
|
|
29
|
-
static readonly SANDBOX_AUTHENTICATE_URL = "https://preprodpayments.wavemoney.io";
|
|
30
33
|
static readonly PRODUCTION_AUTHENTICATE_URL = "https://payments.wavemoney.io";
|
|
31
|
-
static readonly DEFAULT_TIME_TO_LIVE_SECONDS = 300;
|
|
32
34
|
readonly merchantId: string;
|
|
33
35
|
readonly secretKey: string;
|
|
34
36
|
readonly merchantName: string;
|
|
35
37
|
/** Seconds the customer has to pay. */
|
|
36
38
|
readonly timeToLiveSeconds: number;
|
|
37
|
-
|
|
39
|
+
/** Seconds before the default HTTP client gives up on a request. */
|
|
40
|
+
readonly timeoutSeconds: number;
|
|
38
41
|
/** The API base URL in use. */
|
|
39
42
|
readonly baseUrl: string;
|
|
40
43
|
/** The host the customer is redirected to. */
|
|
41
44
|
readonly authenticateUrl: string;
|
|
42
45
|
constructor(options: WaveMoneyConfigOptions);
|
|
43
46
|
/**
|
|
44
|
-
* Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME
|
|
45
|
-
*
|
|
47
|
+
* Reads `WAVE_MONEY_MERCHANT_ID`, `WAVE_MONEY_SECRET_KEY`, `WAVE_MONEY_MERCHANT_NAME`,
|
|
48
|
+
* `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `WAVE_MONEY_BASE_URL`
|
|
46
49
|
* and `WAVE_MONEY_AUTHENTICATE_URL`.
|
|
47
50
|
*/
|
|
48
51
|
static fromEnv(env?: EnvSource): WaveMoneyConfig;
|
|
@@ -76,6 +79,16 @@ export interface WaveMoneyPaymentData {
|
|
|
76
79
|
* this.
|
|
77
80
|
*/
|
|
78
81
|
merchantReferenceId?: string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* The business name shown on Wave's page for this payment. Defaults to the configured
|
|
84
|
+
* `merchantName` when unset.
|
|
85
|
+
*/
|
|
86
|
+
merchantName?: string | null | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* How long the customer has to pay this payment, in seconds: a whole number greater than 0.
|
|
89
|
+
* Defaults to the configured `timeToLiveSeconds` when unset.
|
|
90
|
+
*/
|
|
91
|
+
timeToLiveSeconds?: number | null | undefined;
|
|
79
92
|
}
|
|
80
93
|
/**
|
|
81
94
|
* Wave Money (WavePay payment gateway): redirect payments and callbacks. Wave has no status API,
|
|
@@ -104,7 +117,7 @@ export declare class WaveMoney {
|
|
|
104
117
|
/**
|
|
105
118
|
* Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
|
|
106
119
|
* back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
|
|
107
|
-
* optional.
|
|
120
|
+
* optional. A hashed field holding an object or array fails verification.
|
|
108
121
|
*/
|
|
109
122
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
110
123
|
private hash;
|
|
@@ -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,
|
|
4
|
+
import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSeconds, 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, 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
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
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 = resolveTimeout('wave_money', 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
|
|
50
|
-
*
|
|
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'
|
|
58
|
-
timeToLiveSeconds:
|
|
59
|
-
|
|
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
|
});
|
|
@@ -67,7 +58,7 @@ const STATUSES = {
|
|
|
67
58
|
INSUFFICIENT_BALANCE: 'pending',
|
|
68
59
|
ACCOUNT_LOCKED: 'failed',
|
|
69
60
|
BILL_COLLECTION_FAILED: 'failed',
|
|
70
|
-
PAYMENT_REQUEST_CANCELLED: '
|
|
61
|
+
PAYMENT_REQUEST_CANCELLED: 'canceled',
|
|
71
62
|
TRANSACTION_TIMED_OUT: 'expired',
|
|
72
63
|
SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
|
|
73
64
|
};
|
|
@@ -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 = {}) {
|
|
@@ -132,7 +123,11 @@ export class WaveMoney {
|
|
|
132
123
|
.url('returnUrl', data.returnUrl)
|
|
133
124
|
.required('description', data.description)
|
|
134
125
|
.when(items.length === 0, 'items', 'The items field must have at least one item.')
|
|
135
|
-
.string('merchantReferenceId', data.merchantReferenceId)
|
|
126
|
+
.string('merchantReferenceId', data.merchantReferenceId)
|
|
127
|
+
.string('merchantName', data.merchantName)
|
|
128
|
+
.when(typeof data.merchantName === 'string' && data.merchantName.trim() === '', 'merchantName', 'The merchantName field is required.')
|
|
129
|
+
.when(isSet(data.timeToLiveSeconds) && !Number.isInteger(data.timeToLiveSeconds), 'timeToLiveSeconds', 'The timeToLiveSeconds field must be an integer.')
|
|
130
|
+
.when(isSet(data.timeToLiveSeconds) && data.timeToLiveSeconds < 1, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be greater than 0.');
|
|
136
131
|
items.forEach((item, index) => {
|
|
137
132
|
validator
|
|
138
133
|
.required(`items.${index}.name`, item?.name)
|
|
@@ -158,7 +153,7 @@ export class WaveMoney {
|
|
|
158
153
|
data.merchantReferenceId = randomHex();
|
|
159
154
|
}
|
|
160
155
|
const amount = WaveMoney.resolvedAmount(data).toString();
|
|
161
|
-
const ttl = String(this.config.timeToLiveSeconds);
|
|
156
|
+
const ttl = String(data.timeToLiveSeconds ?? this.config.timeToLiveSeconds);
|
|
162
157
|
// Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
|
|
163
158
|
// text is written as a JSON number without passing through a float.
|
|
164
159
|
const items = `[${data.items
|
|
@@ -173,7 +168,7 @@ export class WaveMoney {
|
|
|
173
168
|
backend_result_url: data.callbackUrl,
|
|
174
169
|
amount,
|
|
175
170
|
payment_description: data.description,
|
|
176
|
-
merchant_name: this.config.merchantName,
|
|
171
|
+
merchant_name: data.merchantName ?? this.config.merchantName,
|
|
177
172
|
items,
|
|
178
173
|
hash: this.hash([
|
|
179
174
|
ttl,
|
|
@@ -205,13 +200,15 @@ export class WaveMoney {
|
|
|
205
200
|
/**
|
|
206
201
|
* Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
|
|
207
202
|
* back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
|
|
208
|
-
* optional.
|
|
203
|
+
* optional. A hashed field holding an object or array fails verification.
|
|
209
204
|
*/
|
|
210
205
|
handleCallback(request) {
|
|
211
206
|
const payload = losslessBody(request);
|
|
212
207
|
const expected = this.hash(CALLBACK_FIELDS.map((field) => scalarString(payload[field]) ?? 'null'));
|
|
213
208
|
const hashValue = payload.hashValue;
|
|
214
|
-
if (typeof hashValue !== 'string' ||
|
|
209
|
+
if (typeof hashValue !== 'string' ||
|
|
210
|
+
CALLBACK_FIELDS.some((field) => isNested(payload[field])) ||
|
|
211
|
+
!safeEqual(expected, hashValue.toLowerCase())) {
|
|
215
212
|
throw new SignatureVerificationError('Wave Money callback hash verification failed.', toPlainObject(payload));
|
|
216
213
|
}
|
|
217
214
|
const gatewayStatus = trimmed(payload, 'status');
|
|
@@ -228,6 +225,9 @@ export class WaveMoney {
|
|
|
228
225
|
return hmacSha256Hex(this.config.secretKey, parts.join(''));
|
|
229
226
|
}
|
|
230
227
|
}
|
|
228
|
+
function isSet(value) {
|
|
229
|
+
return value !== undefined && value !== null;
|
|
230
|
+
}
|
|
231
231
|
function errorMessage(body) {
|
|
232
232
|
const errors = body.errors;
|
|
233
233
|
if (isLosslessObject(errors)) {
|