ipaynow-nodejs-sdk 1.0.0
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/README.md +205 -0
- package/package.json +22 -0
- package/src/client.js +106 -0
- package/src/config.js +87 -0
- package/src/errors.js +36 -0
- package/src/form.js +14 -0
- package/src/index.js +50 -0
- package/src/notify.js +52 -0
- package/src/params.js +366 -0
- package/src/requests.js +269 -0
- package/src/responses.js +128 -0
- package/src/signer.js +110 -0
package/src/responses.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用响应基类:字段访问 + isSuccess 判定(正向交易 responseCode==A001,见规范 §6)。
|
|
3
|
+
*/
|
|
4
|
+
export class IpaynowResponse {
|
|
5
|
+
constructor() {
|
|
6
|
+
/** @type {Record<string, string>} */
|
|
7
|
+
this._fields = {};
|
|
8
|
+
/** @type {string|undefined} */
|
|
9
|
+
this._rawBody = undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Record<string, string>} fields
|
|
14
|
+
* @param {string} [rawBody]
|
|
15
|
+
*/
|
|
16
|
+
load(fields, rawBody) {
|
|
17
|
+
this._fields = { ...fields };
|
|
18
|
+
this._rawBody = rawBody;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {string} name */
|
|
22
|
+
get(name) {
|
|
23
|
+
return this._fields[name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @returns {Record<string, string>} 字段表副本 */
|
|
27
|
+
fields() {
|
|
28
|
+
return { ...this._fields };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @returns {string|undefined} 原始报文串 */
|
|
32
|
+
rawBody() {
|
|
33
|
+
return this._rawBody;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @returns {string|undefined} responseCode */
|
|
37
|
+
code() {
|
|
38
|
+
return this.get("responseCode");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {string|undefined} responseMsg */
|
|
42
|
+
message() {
|
|
43
|
+
return this.get("responseMsg");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {boolean} */
|
|
47
|
+
isSuccess() {
|
|
48
|
+
return this.get("responseCode") === "A001";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** WP001 统一下单响应公共便捷访问器 */
|
|
53
|
+
export class UnifiedOrderResponse extends IpaynowResponse {
|
|
54
|
+
/** @returns {string|undefined} 支付要素(兼容大写 TN) */
|
|
55
|
+
tn() {
|
|
56
|
+
const tn = this.get("tn");
|
|
57
|
+
return tn !== undefined ? tn : this.get("TN");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns {string|undefined} 现在支付流水号 */
|
|
61
|
+
nowPayOrderNo() {
|
|
62
|
+
return this.get("nowPayOrderNo");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class JsapiUnifiedOrderResponse extends UnifiedOrderResponse {}
|
|
67
|
+
export class AggregateCodeUnifiedOrderResponse extends UnifiedOrderResponse {}
|
|
68
|
+
export class MiniProgramUnifiedOrderResponse extends UnifiedOrderResponse {}
|
|
69
|
+
export class H5UnifiedOrderResponse extends UnifiedOrderResponse {}
|
|
70
|
+
export class AppUnifiedOrderResponse extends UnifiedOrderResponse {}
|
|
71
|
+
|
|
72
|
+
/** MQ002 订单查询响应 */
|
|
73
|
+
export class OrderQueryResponse extends IpaynowResponse {
|
|
74
|
+
transStatus() {
|
|
75
|
+
return this.get("transStatus");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
nowPayOrderNo() {
|
|
79
|
+
return this.get("nowPayOrderNo");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
payTime() {
|
|
83
|
+
return this.get("payTime");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
channelOrderNo() {
|
|
87
|
+
return this.get("channelOrderNo");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 退款族响应:responseCode==R000 才算成功 */
|
|
92
|
+
export class RefundResponse extends IpaynowResponse {
|
|
93
|
+
isSuccess() {
|
|
94
|
+
return this.get("responseCode") === "R000";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class RefundQueryResponse extends IpaynowResponse {
|
|
99
|
+
isSuccess() {
|
|
100
|
+
return this.get("responseCode") === "R000";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ReverseResponse extends IpaynowResponse {
|
|
105
|
+
isSuccess() {
|
|
106
|
+
return this.get("responseCode") === "R000";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class ReverseQueryResponse extends IpaynowResponse {
|
|
111
|
+
isSuccess() {
|
|
112
|
+
return this.get("responseCode") === "R000";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** N001 支付回调:transStatus==A001 才算成功 */
|
|
117
|
+
export class PaymentNotifyResponse extends IpaynowResponse {
|
|
118
|
+
isSuccess() {
|
|
119
|
+
return this.get("transStatus") === "A001";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** RN001 退款回调:tradeStatus==A001 才算成功 */
|
|
124
|
+
export class RefundNotifyResponse extends IpaynowResponse {
|
|
125
|
+
isSuccess() {
|
|
126
|
+
return this.get("tradeStatus") === "A001";
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/signer.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { SignType } from "./config.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} value
|
|
6
|
+
* @returns {string} 小写 hex md5
|
|
7
|
+
*/
|
|
8
|
+
export function md5Hex(value) {
|
|
9
|
+
return crypto.createHash("md5").update(value, "utf8").digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 剔除 null / undefined / 空串值,其余值转为字符串。
|
|
14
|
+
* @param {Record<string, unknown>} fields
|
|
15
|
+
* @returns {Record<string, string>}
|
|
16
|
+
*/
|
|
17
|
+
export function clean(fields) {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
20
|
+
if (value === undefined || value === null || value === "") continue;
|
|
21
|
+
out[key] = String(value);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonical(fields, excludeSignType = false, ...excludeFields) {
|
|
27
|
+
const cleaned = clean(fields);
|
|
28
|
+
if (excludeSignType) delete cleaned.signType;
|
|
29
|
+
delete cleaned.mhtSignature;
|
|
30
|
+
delete cleaned.signature;
|
|
31
|
+
for (const field of excludeFields) delete cleaned[field];
|
|
32
|
+
return Object.keys(cleaned)
|
|
33
|
+
.sort()
|
|
34
|
+
.map((key) => `${key}=${cleaned[key]}`)
|
|
35
|
+
.join("&");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function md5Sign(canonicalValue, config) {
|
|
39
|
+
return md5Hex(`${canonicalValue}&${md5Hex(config.appKey)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rsaSign(canonicalValue, config) {
|
|
43
|
+
return crypto.sign("RSA-SHA256", Buffer.from(canonicalValue, "utf8"), config.mchPrivateKeyObject).toString("base64");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function strategy(config) {
|
|
47
|
+
return config.signType === SignType.RSA
|
|
48
|
+
? { sign: rsaSign, verify: rsaVerify }
|
|
49
|
+
: { sign: md5Sign, verify: md5Verify };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function md5Verify(canonicalValue, actual, config) {
|
|
53
|
+
const expectedBuf = Buffer.from(md5Sign(canonicalValue, config), "utf8");
|
|
54
|
+
const actualBuf = Buffer.from(String(actual).toLowerCase(), "utf8");
|
|
55
|
+
return expectedBuf.length === actualBuf.length && crypto.timingSafeEqual(expectedBuf, actualBuf);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rsaVerify(canonicalValue, actual, config) {
|
|
59
|
+
try {
|
|
60
|
+
return crypto.verify(
|
|
61
|
+
"RSA-SHA256",
|
|
62
|
+
Buffer.from(canonicalValue, "utf8"),
|
|
63
|
+
config.ipaynowPublicKeyObject,
|
|
64
|
+
Buffer.from(String(actual).replace(/\s+/g, ""), "base64")
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 现在支付签名算法:
|
|
73
|
+
* 1. 剔除 null/空串值
|
|
74
|
+
* 2. 剔除 mhtSignature / signature(以及调用方额外指定的字段)
|
|
75
|
+
* 3. excludeSignType=true 时再剔除 signType(mhtSignType 是不同字段名,永远参与签名)
|
|
76
|
+
* 4. 按 key 字典序排序拼接 k=v&k2=v2...
|
|
77
|
+
* 5. 追加 &md5(appKey),整体再取 md5
|
|
78
|
+
*
|
|
79
|
+
* @param {Record<string, unknown>} fields
|
|
80
|
+
* @param {import("./config.js").IpaynowConfig} config
|
|
81
|
+
* @param {boolean} [excludeSignType]
|
|
82
|
+
* @param {...string} excludeFields 额外需要剔除的字段(如当前签名字段本身)
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function sign(fields, config, excludeSignType = false, ...excludeFields) {
|
|
86
|
+
return strategy(config).sign(canonical(fields, excludeSignType, ...excludeFields), config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 验签(安全红线):签名缺失即失败,常量时间比较。
|
|
91
|
+
* @param {Record<string, unknown>} fields
|
|
92
|
+
* @param {import("./config.js").IpaynowConfig} config
|
|
93
|
+
* @param {string} signatureField
|
|
94
|
+
* @param {boolean} [excludeSignType]
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function verify(fields, config, signatureField, excludeSignType = false) {
|
|
98
|
+
const actual = fields[signatureField];
|
|
99
|
+
if (actual === undefined || actual === null || actual === "") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (fields.signType && String(fields.signType).toUpperCase() !== config.signType) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return strategy(config).verify(
|
|
106
|
+
canonical(fields, excludeSignType, signatureField, "mhtSignature"),
|
|
107
|
+
actual,
|
|
108
|
+
config
|
|
109
|
+
);
|
|
110
|
+
}
|