@laranex/myanmar-payments 4.0.0-dev.2 → 4.0.0-dev.3
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 +6 -1
- package/README.md +1 -1
- package/dist/cjs/aya-pay/aya-pay.js +2 -1
- package/dist/cjs/core/callback.d.ts +7 -6
- package/dist/cjs/core/callback.js +5 -4
- package/dist/cjs/core/status.d.ts +3 -3
- package/dist/cjs/core/status.js +2 -2
- package/dist/cjs/cyber-source/cyber-source.d.ts +2 -1
- package/dist/cjs/cyber-source/cyber-source.js +26 -12
- package/dist/cjs/kbz-pay/kbz-pay.js +1 -1
- package/dist/cjs/wave-money/wave-money.js +1 -1
- package/dist/esm/aya-pay/aya-pay.js +2 -1
- package/dist/esm/core/callback.d.ts +7 -6
- package/dist/esm/core/callback.js +5 -4
- package/dist/esm/core/status.d.ts +3 -3
- package/dist/esm/core/status.js +2 -2
- package/dist/esm/cyber-source/cyber-source.d.ts +2 -1
- package/dist/esm/cyber-source/cyber-source.js +27 -13
- package/dist/esm/kbz-pay/kbz-pay.js +1 -1
- package/dist/esm/wave-money/wave-money.js +1 -1
- package/package.json +1 -1
- package/skills/node-myanmar-payments/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,10 +12,15 @@ Initial release. The version number matches the other Laranex Myanmar payments p
|
|
|
12
12
|
- Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole-number `number` or `bigint` and rejects fractions. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
|
|
13
13
|
- A config class per gateway with `fromEnv(process.env)`, reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel and Go packages; a missing credential throws `ConfigurationError`. `MyanmarPayments` builds every gateway lazily from one config object or the environment.
|
|
14
14
|
- Result classes: `RedirectPayment`, `FormPayment` (with `toHtml()`), `QrPayment` (with `qrImageDataUri()`) and `AppPayment`; `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, sent with `send(res)` or `toResponse()`) and `PaymentStatusResult`.
|
|
15
|
-
- `CallbackRequest.fromNodeRequest()` (node:http, Express
|
|
15
|
+
- `CallbackRequest.fromNodeRequest()` (node:http, Express), `CallbackRequest.fromWebRequest()` (Fetch API servers such as Next.js, Hono and Bun), `CallbackRequest.from()` and `CallbackRequest.fromJson()`. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text.
|
|
16
16
|
- Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause`), `SignatureVerificationError` and `ConfigurationError`.
|
|
17
17
|
- No runtime dependencies: the global `fetch` (or an injected `fetch` / `HttpClient`, 30 second default timeout, `AbortSignal` per call) and `node:crypto`; an injectable `TokenCache` with an in-memory default for Yoma's access token.
|
|
18
|
+
- CyberSource callbacks trust only the fields listed in `signed_field_names`: `decision` and `req_reference_number` must be signed, and unsigned fields are left out of the result and `raw`.
|
|
19
|
+
- AYA Pay reads a `payload` whose `+` signs became spaces in an unencoded return query string.
|
|
18
20
|
- Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
|
|
19
21
|
- Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
|
|
20
22
|
- Agent skill in `skills/node-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/node-myanmar-payments`.
|
|
21
23
|
- Requires Node.js 20 or higher.
|
|
24
|
+
|
|
25
|
+
### Changed since the pre-releases
|
|
26
|
+
- `PaymentStatus.Cancelled` is renamed to `PaymentStatus.Canceled` and its value from `'cancelled'` to `'canceled'` (American English), with no alias. Code or stored statuses from the `v4.0.0-dev` pre-releases need the new name; gateway status literals such as Wave Money's `PAYMENT_REQUEST_CANCELLED` are unchanged.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Node Myanmar Payments
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@laranex/myanmar-payments)
|
|
4
|
-
[](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml)
|
|
5
5
|
[](LICENSE.md)
|
|
6
6
|
|
|
7
7
|
Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed TypeScript API, ESM and CommonJS, no runtime dependencies. Built for humans and AI agents.
|
|
@@ -246,7 +246,8 @@ class AyaPay {
|
|
|
246
246
|
/** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
|
|
247
247
|
verifiedPayload(input, context) {
|
|
248
248
|
const fail = () => new errors_js_1.SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, (0, json_js_1.toPlainObject)(input));
|
|
249
|
-
|
|
249
|
+
// Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
|
|
250
|
+
const json = (0, crypto_js_1.decodeBase64)((0, values_js_1.get)(input, 'payload').replace(/ /g, '+'));
|
|
250
251
|
const payload = json === undefined ? undefined : (0, json_js_1.parseJsonObject)(json);
|
|
251
252
|
if (payload === undefined) {
|
|
252
253
|
throw fail();
|
|
@@ -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
|
|
20
|
-
*
|
|
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>>;
|
|
@@ -57,10 +57,11 @@ export declare class CallbackRequest {
|
|
|
57
57
|
*/
|
|
58
58
|
static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
|
|
59
59
|
/**
|
|
60
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
61
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
62
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
63
|
-
* again as JSON or a form.
|
|
60
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
61
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
62
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
63
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
64
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
64
65
|
*/
|
|
65
66
|
static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
|
|
66
67
|
/**
|
|
@@ -37,10 +37,11 @@ class CallbackRequest {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
41
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
42
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
43
|
-
* again as JSON or a form.
|
|
40
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
41
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
42
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
43
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
44
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
44
45
|
*/
|
|
45
46
|
static async fromNodeRequest(request) {
|
|
46
47
|
const headers = normalizeHeaders(request.headers);
|
|
@@ -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' | '
|
|
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
|
-
|
|
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", "
|
|
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
|
}>;
|
package/dist/cjs/core/status.js
CHANGED
|
@@ -18,7 +18,7 @@ exports.PaymentStatus = Object.freeze({
|
|
|
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
|
-
|
|
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`. */
|
|
@@ -28,7 +28,7 @@ exports.PaymentStatus = Object.freeze({
|
|
|
28
28
|
'successful',
|
|
29
29
|
'pending',
|
|
30
30
|
'failed',
|
|
31
|
-
'
|
|
31
|
+
'canceled',
|
|
32
32
|
'expired',
|
|
33
33
|
'unknown',
|
|
34
34
|
]),
|
|
@@ -87,7 +87,8 @@ export declare class CyberSource {
|
|
|
87
87
|
initiate(data: CyberSourcePaymentData): FormPayment;
|
|
88
88
|
/**
|
|
89
89
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
90
|
-
* page.
|
|
90
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
91
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
91
92
|
*/
|
|
92
93
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
93
94
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
@@ -77,7 +77,7 @@ const STATUSES = {
|
|
|
77
77
|
REVIEW: 'pending',
|
|
78
78
|
DECLINE: 'failed',
|
|
79
79
|
ERROR: 'failed',
|
|
80
|
-
CANCEL: '
|
|
80
|
+
CANCEL: 'canceled',
|
|
81
81
|
};
|
|
82
82
|
/**
|
|
83
83
|
* CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
|
|
@@ -149,31 +149,39 @@ class CyberSource {
|
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
151
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
152
|
-
* page.
|
|
152
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
153
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
153
154
|
*/
|
|
154
155
|
handleCallback(request) {
|
|
155
156
|
const payload = (0, callback_js_1.losslessInput)(request);
|
|
156
157
|
const expected = this.sign(payload);
|
|
157
|
-
|
|
158
|
+
const names = signedNames(payload);
|
|
159
|
+
if (expected === undefined ||
|
|
160
|
+
!(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'signature')) ||
|
|
161
|
+
!names.includes('decision') ||
|
|
162
|
+
!names.includes('req_reference_number')) {
|
|
158
163
|
throw new errors_js_1.SignatureVerificationError('CyberSource callback signature verification failed.', (0, json_js_1.toPlainObject)(payload));
|
|
159
164
|
}
|
|
160
|
-
|
|
165
|
+
// A signature only covers the listed fields, so anything else in the post could have been
|
|
166
|
+
// added by the sender (e.g. a `decision` next to the signed request fields).
|
|
167
|
+
const signed = {};
|
|
168
|
+
for (const name of [...names, 'signature']) {
|
|
169
|
+
(0, json_js_1.setKey)(signed, name, payload[name]);
|
|
170
|
+
}
|
|
171
|
+
const decision = (0, values_js_1.trimmed)(signed, 'decision').toUpperCase();
|
|
161
172
|
return new callback_js_1.PaymentCallback({
|
|
162
|
-
orderId: (0, values_js_1.get)(
|
|
173
|
+
orderId: (0, values_js_1.get)(signed, 'req_reference_number'),
|
|
163
174
|
status: (0, status_js_1.resolveStatus)(STATUSES, decision),
|
|
164
175
|
gatewayStatus: decision,
|
|
165
|
-
gatewayReference: (0, values_js_1.optional)(
|
|
166
|
-
amount: (0, values_js_1.get)(
|
|
167
|
-
raw: (0, json_js_1.toPlainObject)(
|
|
176
|
+
gatewayReference: (0, values_js_1.optional)(signed, 'transaction_id'),
|
|
177
|
+
amount: (0, values_js_1.get)(signed, 'auth_amount') || (0, values_js_1.optional)(signed, 'req_amount'),
|
|
178
|
+
raw: (0, json_js_1.toPlainObject)(signed),
|
|
168
179
|
});
|
|
169
180
|
}
|
|
170
181
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
171
182
|
sign(fields) {
|
|
172
183
|
const pairs = [];
|
|
173
|
-
for (const name of (
|
|
174
|
-
if (name === '') {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
184
|
+
for (const name of signedNames(fields)) {
|
|
177
185
|
const value = (0, values_js_1.scalarString)(fields[name]);
|
|
178
186
|
if (value === undefined) {
|
|
179
187
|
return undefined;
|
|
@@ -186,3 +194,9 @@ class CyberSource {
|
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
exports.CyberSource = CyberSource;
|
|
197
|
+
/** The names listed in `signed_field_names`, in order. */
|
|
198
|
+
function signedNames(fields) {
|
|
199
|
+
return (0, values_js_1.get)(fields, 'signed_field_names')
|
|
200
|
+
.split(',')
|
|
201
|
+
.filter((name) => name !== '');
|
|
202
|
+
}
|
|
@@ -71,7 +71,7 @@ const STATUSES = {
|
|
|
71
71
|
INSUFFICIENT_BALANCE: 'pending',
|
|
72
72
|
ACCOUNT_LOCKED: 'failed',
|
|
73
73
|
BILL_COLLECTION_FAILED: 'failed',
|
|
74
|
-
PAYMENT_REQUEST_CANCELLED: '
|
|
74
|
+
PAYMENT_REQUEST_CANCELLED: 'canceled',
|
|
75
75
|
TRANSACTION_TIMED_OUT: 'expired',
|
|
76
76
|
SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
|
|
77
77
|
};
|
|
@@ -242,7 +242,8 @@ export class AyaPay {
|
|
|
242
242
|
/** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
|
|
243
243
|
verifiedPayload(input, context) {
|
|
244
244
|
const fail = () => new SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, toPlainObject(input));
|
|
245
|
-
|
|
245
|
+
// Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
|
|
246
|
+
const json = decodeBase64(get(input, 'payload').replace(/ /g, '+'));
|
|
246
247
|
const payload = json === undefined ? undefined : parseJsonObject(json);
|
|
247
248
|
if (payload === undefined) {
|
|
248
249
|
throw fail();
|
|
@@ -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
|
|
20
|
-
*
|
|
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>>;
|
|
@@ -57,10 +57,11 @@ export declare class CallbackRequest {
|
|
|
57
57
|
*/
|
|
58
58
|
static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
|
|
59
59
|
/**
|
|
60
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
61
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
62
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
63
|
-
* again as JSON or a form.
|
|
60
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
61
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
62
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
63
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
64
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
64
65
|
*/
|
|
65
66
|
static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
|
|
66
67
|
/**
|
|
@@ -31,10 +31,11 @@ export class CallbackRequest {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Reads a Node `http.IncomingMessage` (also Express `req`,
|
|
35
|
-
* Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
36
|
-
* text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
37
|
-
* again as JSON or a form.
|
|
34
|
+
* Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
|
|
35
|
+
* parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
|
|
36
|
+
* it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
|
|
37
|
+
* encoded again as JSON or a form. Fastify parses the body before your handler runs: use
|
|
38
|
+
* {@link CallbackRequest.from} with the raw body there.
|
|
38
39
|
*/
|
|
39
40
|
static async fromNodeRequest(request) {
|
|
40
41
|
const headers = normalizeHeaders(request.headers);
|
|
@@ -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' | '
|
|
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
|
-
|
|
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", "
|
|
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
|
}>;
|
package/dist/esm/core/status.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
27
|
+
'canceled',
|
|
28
28
|
'expired',
|
|
29
29
|
'unknown',
|
|
30
30
|
]),
|
|
@@ -87,7 +87,8 @@ export declare class CyberSource {
|
|
|
87
87
|
initiate(data: CyberSourcePaymentData): FormPayment;
|
|
88
88
|
/**
|
|
89
89
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
90
|
-
* page.
|
|
90
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
91
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
91
92
|
*/
|
|
92
93
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
93
94
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
@@ -3,7 +3,7 @@ import { CallbackRequest, losslessInput, PaymentCallback } from '../core/callbac
|
|
|
3
3
|
import { hmacSha256Base64, randomHex, safeEqual } from '../core/crypto.js';
|
|
4
4
|
import { defaultEnv, envFirst, envSandbox, 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';
|
|
@@ -74,7 +74,7 @@ const STATUSES = {
|
|
|
74
74
|
REVIEW: 'pending',
|
|
75
75
|
DECLINE: 'failed',
|
|
76
76
|
ERROR: 'failed',
|
|
77
|
-
CANCEL: '
|
|
77
|
+
CANCEL: 'canceled',
|
|
78
78
|
};
|
|
79
79
|
/**
|
|
80
80
|
* CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
|
|
@@ -146,31 +146,39 @@ export class CyberSource {
|
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
148
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
149
|
-
* page.
|
|
149
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
150
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
150
151
|
*/
|
|
151
152
|
handleCallback(request) {
|
|
152
153
|
const payload = losslessInput(request);
|
|
153
154
|
const expected = this.sign(payload);
|
|
154
|
-
|
|
155
|
+
const names = signedNames(payload);
|
|
156
|
+
if (expected === undefined ||
|
|
157
|
+
!safeEqual(expected, get(payload, 'signature')) ||
|
|
158
|
+
!names.includes('decision') ||
|
|
159
|
+
!names.includes('req_reference_number')) {
|
|
155
160
|
throw new SignatureVerificationError('CyberSource callback signature verification failed.', toPlainObject(payload));
|
|
156
161
|
}
|
|
157
|
-
|
|
162
|
+
// A signature only covers the listed fields, so anything else in the post could have been
|
|
163
|
+
// added by the sender (e.g. a `decision` next to the signed request fields).
|
|
164
|
+
const signed = {};
|
|
165
|
+
for (const name of [...names, 'signature']) {
|
|
166
|
+
setKey(signed, name, payload[name]);
|
|
167
|
+
}
|
|
168
|
+
const decision = trimmed(signed, 'decision').toUpperCase();
|
|
158
169
|
return new PaymentCallback({
|
|
159
|
-
orderId: get(
|
|
170
|
+
orderId: get(signed, 'req_reference_number'),
|
|
160
171
|
status: resolveStatus(STATUSES, decision),
|
|
161
172
|
gatewayStatus: decision,
|
|
162
|
-
gatewayReference: optional(
|
|
163
|
-
amount: get(
|
|
164
|
-
raw: toPlainObject(
|
|
173
|
+
gatewayReference: optional(signed, 'transaction_id'),
|
|
174
|
+
amount: get(signed, 'auth_amount') || optional(signed, 'req_amount'),
|
|
175
|
+
raw: toPlainObject(signed),
|
|
165
176
|
});
|
|
166
177
|
}
|
|
167
178
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
168
179
|
sign(fields) {
|
|
169
180
|
const pairs = [];
|
|
170
|
-
for (const name of
|
|
171
|
-
if (name === '') {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
181
|
+
for (const name of signedNames(fields)) {
|
|
174
182
|
const value = scalarString(fields[name]);
|
|
175
183
|
if (value === undefined) {
|
|
176
184
|
return undefined;
|
|
@@ -182,3 +190,9 @@ export class CyberSource {
|
|
|
182
190
|
: hmacSha256Base64(this.config.secretKey, pairs.join(','));
|
|
183
191
|
}
|
|
184
192
|
}
|
|
193
|
+
/** The names listed in `signed_field_names`, in order. */
|
|
194
|
+
function signedNames(fields) {
|
|
195
|
+
return get(fields, 'signed_field_names')
|
|
196
|
+
.split(',')
|
|
197
|
+
.filter((name) => name !== '');
|
|
198
|
+
}
|
|
@@ -67,7 +67,7 @@ const STATUSES = {
|
|
|
67
67
|
INSUFFICIENT_BALANCE: 'pending',
|
|
68
68
|
ACCOUNT_LOCKED: 'failed',
|
|
69
69
|
BILL_COLLECTION_FAILED: 'failed',
|
|
70
|
-
PAYMENT_REQUEST_CANCELLED: '
|
|
70
|
+
PAYMENT_REQUEST_CANCELLED: 'canceled',
|
|
71
71
|
TRANSACTION_TIMED_OUT: 'expired',
|
|
72
72
|
SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
|
|
73
73
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laranex/myanmar-payments",
|
|
3
|
-
"version": "4.0.0-dev.
|
|
3
|
+
"version": "4.0.0-dev.3",
|
|
4
4
|
"description": "Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed TypeScript API, ESM and CommonJS, no runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"laranex",
|
|
@@ -84,13 +84,14 @@ try {
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
- Give the callback route the raw body: in Express use `express.raw({ type: '*/*' })` or mount it before `express.json()`.
|
|
87
|
+
- Fastify consumes the body before your handler: keep it as a string with a content-type parser (`parseAs: 'string'`) and build `CallbackRequest.from({ body, headers: request.headers, query: request.query })`.
|
|
87
88
|
- Check AYA's browser return with `aya.verifyRedirect(request)`.
|
|
88
89
|
- For production, store the verified call, acknowledge immediately, then process it once in the background.
|
|
89
90
|
|
|
90
91
|
### Check status and handle errors
|
|
91
92
|
|
|
92
93
|
- `kbz.status(orderId)`, `aya.status(orderId)` and `yoma.status(reference)` return `PaymentStatusResult` with `status` and `isSuccessful()`. Wave Money and CyberSource have no status API.
|
|
93
|
-
- Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `
|
|
94
|
+
- Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Canceled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
|
|
94
95
|
- Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause` for network errors and aborts). All errors extend `PaymentError`.
|
|
95
96
|
|
|
96
97
|
## Test your app
|