alipay-sdk
Advanced tools
Comparing version 2.0.2 to 3.0.0
@@ -1,160 +0,302 @@ | ||
'use strict'; | ||
const crypto = require('crypto'); | ||
const urllib = require('urllib'); | ||
const extend = require('extend2'); | ||
const { formatParams, formatReqData, decamelize, camelcase, ALIPAY_ALGORITHM_MAPPING } = require('./util'); | ||
const defaultConfig = { | ||
params: { | ||
signType: 'RSA2', | ||
charset: 'utf-8', | ||
version: '1.0', | ||
method: '', | ||
}, | ||
getway: 'https://openapi.alipay.com/gateway.do', | ||
// 私钥 | ||
privateKey: '', | ||
// 支付宝公钥 | ||
alipayPublicKey: '', | ||
appId: '', | ||
timeout: 5000, // 5s | ||
urllib, | ||
// 返回 camelcase 数据 | ||
camelcase: false, | ||
}; | ||
"use strict"; | ||
/** | ||
* @author tudou527 | ||
* @email [tudou527@gmail.com] | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs = require("fs"); | ||
const is = require("is"); | ||
const crypto = require("crypto"); | ||
const urllib = require("urllib"); | ||
const request = require("request"); | ||
const camelcaseKeys = require("camelcase-keys"); | ||
const util_1 = require("./util"); | ||
const pkg = require('../package.json'); | ||
class AlipaySdk { | ||
constructor(config) { | ||
this.config = extend(true, {}, defaultConfig, camelcase(config)); | ||
} | ||
/** | ||
* 执行请求 | ||
* @param {string} method 调用接口方法名,比如 alipay.ebpp.bill.add | ||
* @param {object} bizContext 业务请求参数 | ||
* @param {object} publicArgs 公共请求参数 | ||
* @param {Boolean} validateSign 是否验签 | ||
* @param {object} log 可选日志记录对象 | ||
* @return {Promise} 请求执行结果 | ||
*/ | ||
execute(method, bizContext, publicArgs, validateSign, log) { | ||
const config = this.config; | ||
const params = formatReqData(method, bizContext, publicArgs, config); | ||
return new Promise((resolve, reject) => { | ||
log && log.info('start execute method: %s , params: %s, config: %s,', method, JSON.stringify(params), JSON.stringify(config)); | ||
config.urllib.request(config.getway, { | ||
data: decamelize(params), | ||
dataType: 'json', | ||
timeout: config.timeout, | ||
method: 'GET', | ||
}) | ||
.then(ret => { | ||
log && log.info('execute method: %s , params: %s, got resonse: %s', method, JSON.stringify(params), JSON.stringify(ret)); | ||
if (ret.status === 200) { | ||
// 示例响应格式 | ||
// { | ||
// "alipay_trade_precreate_response": { | ||
// "code": "10000", | ||
// "msg": "Success", | ||
// "out_trade_no": "6141161365682511", | ||
// "qr_code": "https:\/\/qr.alipay.com\/bax03206ug0kulveltqc80a8" | ||
// }, | ||
// "sign": "VrgnnGgRMNApB1QlNJimiOt5ocGn4a4pbXjdoqjHtnYMWPYGX9AS0ELt8YikVAl6LPfsD7hjSyGWGjwaAYJjzH1MH7B2/T3He0kLezuWHsikao2ktCjTrX0tmUfoMUBCxKGGuDHtmasQi4yAoDk+ux7og1J5tL49yWiiwgaJoBE=" | ||
// } | ||
const data = ret.data[method.replace(/\./g, '_') + '_response']; | ||
const sign = ret.data.sign; | ||
// 默认不验签 | ||
let validateSuccess = true; | ||
if (validateSign) { | ||
validateSuccess = this.checkResponseSign(data, sign, params.signType); | ||
} | ||
if (validateSuccess) { | ||
if (config.camelcase) { | ||
return resolve(camelcase(data)); | ||
constructor(config) { | ||
if (!config.appId) { | ||
throw Error('config.appId is required'); | ||
} | ||
if (!config.privateKey) { | ||
throw Error('config.privateKey is required'); | ||
} | ||
config.privateKey = this.formatKey(config.privateKey, 'RSA PRIVATE KEY'); | ||
if (config.alipayPublicKey) { | ||
config.alipayPublicKey = this.formatKey(config.alipayPublicKey, 'PUBLIC KEY'); | ||
} | ||
this.config = Object.assign({ | ||
urllib, | ||
gateway: 'https://openapi.alipay.com/gateway.do', | ||
timeout: 5000, | ||
camelcase: true, | ||
signType: 'RSA2', | ||
charset: 'utf-8', | ||
version: '1.0', | ||
}, camelcaseKeys(config)); | ||
this.sdkVersion = `alipay-sdk-nodejs-${pkg.version}`; | ||
} | ||
// 格式化 key | ||
formatKey(key, type) { | ||
const item = key.split('\n').map(val => val.trim()); | ||
// 删除包含 `RSA PRIVATE KEY / PUBLIC KEY` 等字样的第一行 | ||
if (item[0].includes(type)) { | ||
item.shift(); | ||
} | ||
// 删除包含 `RSA PRIVATE KEY / PUBLIC KEY` 等字样的最后一行 | ||
if (item[item.length - 1].includes(type)) { | ||
item.pop(); | ||
} | ||
return `-----BEGIN ${type}-----\n${item.join('')}\n-----END ${type}-----`; | ||
} | ||
// 格式化请求 url(按规范把某些固定的参数放入 url) | ||
formatUrl(url, params) { | ||
let requestUrl = url; | ||
// 需要放在 url 中的参数列表 | ||
const urlArgs = [ | ||
'app_id', 'method', 'format', 'charset', | ||
'sign_type', 'sign', 'timestamp', 'version', | ||
'notify_url', 'return_url', 'auth_token', 'app_auth_token', | ||
]; | ||
for (const key in params) { | ||
if (urlArgs.indexOf(key) > -1) { | ||
const val = encodeURIComponent(params[key]); | ||
requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'}${key}=${val}`; | ||
// 删除 postData 中对应的数据 | ||
delete params[key]; | ||
} | ||
return resolve(data); | ||
} | ||
} | ||
reject(ret); | ||
}) | ||
.catch(err => { | ||
log && log.error('execute method: %s , params: %s, got error: %s', method, JSON.stringify(params), JSON.stringify(err)); | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
/** | ||
* 执行 page 类接口请求 | ||
* @param {string} method 调用接口方法名,比如 alipay.trade.page.pay | ||
* @param {object} bizContext 业务请求参数 | ||
* @param {object} publicArgs 公共请求参数 | ||
* @param {object} log 可选日志记录对象 | ||
* @return {Promise} 可提交的表单 | ||
*/ | ||
page_execute(method, bizContext, publicArgs, log) { | ||
const config = this.config; | ||
const params = formatReqData(method, bizContext, publicArgs, config); | ||
return new Promise(resolve => { | ||
log && log.info('start page_execute method: %s , params: %s, config: %s,', method, JSON.stringify(params), JSON.stringify(config)); | ||
const formName = `alipaysubmit${Date.now()}`; | ||
const postData = decamelize(params); | ||
const inputs = []; | ||
Object.keys(postData).forEach(key => { | ||
const value = String(postData[key]).replace(/\"/g, '"'); | ||
inputs.push(`<input type="hidden" name="${key}" value="${value}" />`); | ||
}); | ||
resolve(`<form action="${config.getway}" method="post" name="${formName}" id="${formName}"> | ||
${inputs.join('')} | ||
</form> | ||
<script>document.forms["${formName}"].submit();</script>`); | ||
}); | ||
} | ||
// response 参数验签 | ||
checkResponseSign(signArgs, signStr, signType) { | ||
signType = signType || 'RSA2'; | ||
if (!this.config.alipayPublicKey || this.config.alipayPublicKey === '') { | ||
// 支付宝公钥不存在时不做验签 | ||
return true; | ||
return { execParams: params, url: requestUrl }; | ||
} | ||
if (!signArgs) { | ||
// 带验签的参数不存在时返回失败 | ||
return false; | ||
// 文件上传 | ||
multipartExec(method, option = {}) { | ||
const config = this.config; | ||
const signParams = {}; | ||
const formData = {}; | ||
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null; | ||
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null; | ||
option.formData.getFields().forEach((field) => { | ||
// 字段加入签名参数(文件不需要签名) | ||
signParams[field.name] = field.value; | ||
formData[field.name] = field.value; | ||
}); | ||
option.formData.getFiles().forEach((file) => { | ||
// 单独处理文件类型 | ||
formData[file.fieldName] = fs.createReadStream(file.path); | ||
}); | ||
// 计算签名 | ||
const signData = util_1.sign(method, signParams, config); | ||
// 格式化 url | ||
const { url } = this.formatUrl(config.gateway, signData); | ||
infoLog && infoLog('[AlipaySdk]start exec url: %s, method: %s, params: %s', url, method, JSON.stringify(signParams)); | ||
return new Promise((resolve, reject) => { | ||
request.post({ | ||
url, | ||
formData, | ||
json: false, | ||
timeout: config.timeout, | ||
headers: { 'user-agent': this.sdkVersion }, | ||
}, (err, {}, body) => { | ||
if (err) { | ||
err.message = '[AlipaySdk]exec error'; | ||
errorLog && errorLog(err); | ||
reject(err); | ||
} | ||
infoLog && infoLog('[AlipaySdk]exec response: %s', body); | ||
const result = JSON.parse(body); | ||
const responseKey = `${method.replace(/\./g, '_')}_response`; | ||
const data = result[responseKey]; | ||
// 验签 | ||
const validateSuccess = option.validateSign ? this.checkResponseSign(body, responseKey) : true; | ||
if (validateSuccess) { | ||
resolve(config.camelcase ? camelcaseKeys(data) : data); | ||
} | ||
else { | ||
reject({ serverResult: body, errorMessage: '[AlipaySdk]验签失败' }); | ||
} | ||
reject({ serverResult: body, errorMessage: '[AlipaySdk]HTTP 请求错误' }); | ||
}); | ||
}); | ||
} | ||
if (signArgs.sub_code) { | ||
// 业务系统返回异常时不需要验签(返回成功) | ||
return true; | ||
// page 类接口 | ||
pageExec(method, option = {}) { | ||
const signParams = { alipaySdk: this.sdkVersion }; | ||
const config = this.config; | ||
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null; | ||
option.formData.getFields().forEach((field) => { | ||
signParams[field.name] = field.value; | ||
}); | ||
// 计算签名 | ||
const signData = util_1.sign(method, signParams, config); | ||
// 格式化 url | ||
const { url, execParams } = this.formatUrl(config.gateway, signData); | ||
infoLog && infoLog('[AlipaySdk]start exec url: %s, method: %s, params: %s', url, method, JSON.stringify(signParams)); | ||
if (option.formData.getMethod() === 'get') { | ||
return new Promise((resolve) => { | ||
const query = Object.keys(execParams).map((key) => { | ||
return `${key}=${encodeURIComponent(execParams[key])}`; | ||
}); | ||
resolve(`${url}&${query.join('&')}`); | ||
}); | ||
} | ||
return new Promise((resolve) => { | ||
// 生成表单 | ||
const formName = `alipaySDKSubmit${Date.now()}`; | ||
resolve(` | ||
<form action="${url}" method="post" name="${formName}" id="${formName}"> | ||
${Object.keys(execParams).map((key) => { | ||
const value = String(execParams[key]).replace(/\"/g, '"'); | ||
return `<input type="hidden" name="${key}" value="${value}" />`; | ||
}).join('')} | ||
</form> | ||
<script>document.forms["${formName}"].submit();</script> | ||
`); | ||
}); | ||
} | ||
// 参数存在,并且是正常的结果(不包含 sub_code)时才验签 | ||
const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[signType]); | ||
verifier.update(JSON.stringify(signArgs), 'utf-8'); | ||
return verifier.verify(this.config.alipayPublicKey, signStr, 'base64'); | ||
} | ||
// 通知验签 | ||
checkNotifySign(postData) { | ||
const signStr = postData.sign; | ||
const signType = postData.sign_type || 'RSA2'; | ||
if (!this.config.alipayPublicKey || !signStr || !signType) { | ||
return false; | ||
/** | ||
* | ||
* @param originStr 开放平台返回的原始字符串 | ||
* @param responseKey xx_response 方法名 key | ||
*/ | ||
getSignStr(originStr, responseKey) { | ||
// 待签名的字符串 | ||
let validateStr = originStr.trim(); | ||
// 找到 xxx_response 开始的位置 | ||
const startIndex = originStr.indexOf(`${responseKey}"`); | ||
// 找到最后一个 “"sign"” 字符串的位置(避免) | ||
const lastIndex = originStr.lastIndexOf('"sign"'); | ||
/** | ||
* 删除 xxx_response 及之前的字符串 | ||
* 假设原始字符串为 | ||
* {"xxx_response":{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"} | ||
* 删除后变为 | ||
* :{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"} | ||
*/ | ||
validateStr = validateStr.substr(startIndex + responseKey.length + 1); | ||
/** | ||
* 删除最后一个 "sign" 及之后的字符串 | ||
* 删除后变为 | ||
* :{"code":"10000"}, | ||
* {} 之间就是待验签的字符串 | ||
*/ | ||
validateStr = validateStr.substr(0, lastIndex); | ||
// 删除第一个 { 之前的任何字符 | ||
validateStr = validateStr.replace(/^[^{]*{/g, '{'); | ||
// 删除最后一个 } 之后的任何字符 | ||
validateStr = validateStr.replace(/\}([^}]*)$/g, '}'); | ||
return validateStr; | ||
} | ||
const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[signType]); | ||
const signArgs = Object.assign({}, postData); | ||
// 除去sign、sign_type 皆是待验签的参数。 | ||
delete signArgs.sign; | ||
delete signArgs.sign_type; | ||
const signData = formatParams(signArgs).decode; | ||
verifier.update(signData, 'utf-8'); | ||
return verifier.verify(this.config.alipayPublicKey, signStr, 'base64'); | ||
} | ||
/** | ||
* 执行请求 | ||
* @param {string} method 调用接口方法名,比如 alipay.ebpp.bill.add | ||
* @param {object} params 请求参数 | ||
* @param {object} params.bizContent 业务请求参数 | ||
* @param {Boolean} option 选项 | ||
* @param {Boolean} option.validateSign 是否验签 | ||
* @param {object} args.log 可选日志记录对象 | ||
* @return {Promise} 请求执行结果 | ||
*/ | ||
exec(method, params = {}, option = {}) { | ||
if (option.formData) { | ||
if (option.formData.getFiles().length > 0) { | ||
return this.multipartExec(method, option); | ||
} | ||
/** | ||
* fromData 中不包含文件时,认为是 page 类接口(返回 form 表单) | ||
* 比如 PC 端支付接口 alipay.trade.page.pay | ||
*/ | ||
return this.pageExec(method, option); | ||
} | ||
const config = this.config; | ||
// 计算签名 | ||
const signData = util_1.sign(method, params, config); | ||
const { url, execParams } = this.formatUrl(config.gateway, signData); | ||
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null; | ||
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null; | ||
infoLog && infoLog('[AlipaySdk]start exec, url: %s, method: %s, params: %s', url, method, JSON.stringify(execParams)); | ||
return new Promise((resolve, reject) => { | ||
config.urllib.request(url, { | ||
method: 'POST', | ||
data: execParams, | ||
// 按 text 返回(为了验签) | ||
dataType: 'text', | ||
timeout: config.timeout, | ||
headers: { 'user-agent': this.sdkVersion }, | ||
}) | ||
.then((ret) => { | ||
infoLog && infoLog('[AlipaySdk]exec response: %s', ret); | ||
if (ret.status === 200) { | ||
/** | ||
* 示例响应格式 | ||
* {"alipay_trade_precreate_response": | ||
* {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"}, | ||
* "sign": "abcde=" | ||
* } | ||
*/ | ||
const result = JSON.parse(ret.data); | ||
const responseKey = `${method.replace(/\./g, '_')}_response`; | ||
const data = result[responseKey]; | ||
// 按字符串验签 | ||
const validateSuccess = option.validateSign ? this.checkResponseSign(ret.data, responseKey) : true; | ||
if (validateSuccess) { | ||
resolve(config.camelcase ? camelcaseKeys(data) : data); | ||
} | ||
else { | ||
reject({ serverResult: ret, errorMessage: '[AlipaySdk]验签失败' }); | ||
} | ||
} | ||
reject({ serverResult: ret, errorMessage: '[AlipaySdk]HTTP 请求错误' }); | ||
}) | ||
.catch((err) => { | ||
err.message = '[AlipaySdk]exec error'; | ||
errorLog && errorLog(err); | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
// 结果验签 | ||
checkResponseSign(signStr, responseKey) { | ||
if (!this.config.alipayPublicKey || this.config.alipayPublicKey === '') { | ||
console.warn('config.alipayPublicKey is empty'); | ||
// 支付宝公钥不存在时不做验签 | ||
return true; | ||
} | ||
// 带验签的参数不存在时返回失败 | ||
if (!signStr) { | ||
return false; | ||
} | ||
// 根据服务端返回的结果截取需要验签的目标字符串 | ||
const validateStr = this.getSignStr(signStr, responseKey); | ||
// 服务端返回的签名 | ||
const serverSign = JSON.parse(signStr).sign; | ||
// 参数存在,并且是正常的结果(不包含 sub_code)时才验签 | ||
const verifier = crypto.createVerify(util_1.ALIPAY_ALGORITHM_MAPPING[this.config.signType]); | ||
verifier.update(validateStr, 'utf8'); | ||
return verifier.verify(this.config.alipayPublicKey, serverSign, 'base64'); | ||
} | ||
/** | ||
* 通知验签 | ||
* @param postData {JSON} 服务端的消息内容 | ||
*/ | ||
checkNotifySign(postData) { | ||
const signStr = postData.sign; | ||
const signType = postData.sign_type || 'RSA2'; | ||
if (!this.config.alipayPublicKey || !signStr) { | ||
return false; | ||
} | ||
const signArgs = Object.assign({}, postData); | ||
// 除去sign、sign_type 皆是待验签的参数。 | ||
delete signArgs.sign; | ||
delete signArgs.sign_type; | ||
const decodeSign = Object.keys(signArgs).sort().filter(val => val).map((key) => { | ||
let value = signArgs[key]; | ||
if (Array.prototype.toString.call(value) !== '[object String]') { | ||
value = JSON.stringify(value); | ||
} | ||
return `${key}=${decodeURIComponent(value)}`; | ||
}).join('&'); | ||
const verifier = crypto.createVerify(util_1.ALIPAY_ALGORITHM_MAPPING[signType]); | ||
verifier.update(decodeSign, 'utf8'); | ||
return verifier.verify(this.config.alipayPublicKey, signStr, 'base64'); | ||
} | ||
} | ||
module.exports = AlipaySdk; | ||
exports.default = AlipaySdk; |
144
lib/util.js
@@ -1,100 +0,52 @@ | ||
'use strict'; | ||
const moment = require('moment'); | ||
const decamelize = require('decamelize'); | ||
const camelcase = require('camelcase'); | ||
const isPlainObject = require('is-plain-object'); | ||
const crypto = require('crypto'); | ||
"use strict"; | ||
/** | ||
* @author tudou527 | ||
* @email [tudou527@gmail.com] | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const crypto = require("crypto"); | ||
const moment = require("moment"); | ||
const iconv = require("iconv-lite"); | ||
const snakeCaseKeys = require("snakecase-keys"); | ||
const ALIPAY_ALGORITHM_MAPPING = { | ||
RSA: 'RSA-SHA1', | ||
RSA2: 'RSA-SHA256', | ||
RSA: 'RSA-SHA1', | ||
RSA2: 'RSA-SHA256', | ||
}; | ||
function camelcaseFn(o) { | ||
if (isPlainObject(o)) { | ||
const copy = {}; | ||
for (const key of Object.keys(o)) { | ||
const camelcaseKey = camelcase(key); | ||
copy[camelcaseKey] = camelcaseFn(o[key]); | ||
exports.ALIPAY_ALGORITHM_MAPPING = ALIPAY_ALGORITHM_MAPPING; | ||
/** | ||
* 签名 | ||
* @param {string} method 调用接口方法名,比如 alipay.ebpp.bill.add | ||
* @param {object} bizContent 业务请求参数 | ||
* @param {object} publicArgs 公共请求参数 | ||
* @param {object} config sdk 配置 | ||
*/ | ||
function sign(method, params = {}, config) { | ||
const bizContent = params.bizContent || null; | ||
delete params.bizContent; | ||
const signParams = Object.assign({ | ||
method, | ||
appId: config.appId, | ||
charset: config.charset, | ||
version: config.version, | ||
signType: config.signType, | ||
timestamp: moment().format('YYYY-MM-DD HH:mm:ss'), | ||
}, params); | ||
if (bizContent) { | ||
signParams.bizContent = JSON.stringify(snakeCaseKeys(bizContent)); | ||
} | ||
return copy; | ||
} else if (Array.isArray(o)) { | ||
return o.map(item => camelcaseFn(item)); | ||
} | ||
return o; | ||
// params key 驼峰转下划线 | ||
const decamelizeParams = snakeCaseKeys(signParams); | ||
// 排序 | ||
const signStr = Object.keys(decamelizeParams).sort().map((key) => { | ||
let data = decamelizeParams[key]; | ||
if (Array.prototype.toString.call(data) !== '[object String]') { | ||
data = JSON.stringify(data); | ||
} | ||
return `${key}=${iconv.encode(data, config.charset)}`; | ||
}).join('&'); | ||
// 计算签名 | ||
const sign = crypto.createSign(ALIPAY_ALGORITHM_MAPPING[config.signType]) | ||
.update(signStr, 'utf8').sign(config.privateKey, 'base64'); | ||
return Object.assign(decamelizeParams, { sign }); | ||
} | ||
function decamelizeFn(o) { | ||
const copy = {}; | ||
for (const key of Object.keys(o)) { | ||
const decamelizeKey = decamelize(key, '_'); | ||
copy[decamelizeKey] = o[key]; | ||
} | ||
return copy; | ||
} | ||
function sign(params, privateKey) { | ||
// 驼峰转下划线后对参数排序 | ||
const payload = getOrderedParamString(decamelizeFn(params)); | ||
return signOriginal(payload, privateKey, params.signType); | ||
} | ||
function signOriginal(params, privateKey, signType) { | ||
const sig = crypto.createSign(ALIPAY_ALGORITHM_MAPPING[signType || 'RSA2']); | ||
sig.update(params, 'utf-8'); | ||
return sig.sign(privateKey, 'base64'); | ||
} | ||
function getOrderedParamString(params, needEncode) { | ||
return Object.keys(params).sort().map(key => { | ||
let data = params[key]; | ||
if (Array.prototype.toString.call(data) !== '[object String]') { | ||
data = JSON.stringify(data); | ||
} | ||
const value = needEncode ? encodeURIComponent(data) : data; | ||
return `${key}=${value}`; | ||
}) | ||
.join('&'); | ||
} | ||
module.exports = { | ||
camelcase: camelcaseFn, | ||
decamelize: decamelizeFn, | ||
formatParams(params) { | ||
const decode = []; | ||
const encode = []; | ||
Object.keys(params).sort().forEach(key => { | ||
let value = params[key]; | ||
if (value === '' || value === undefined) { | ||
return; | ||
} | ||
if (Array.prototype.toString.call(value) !== '[object String]') { | ||
value = JSON.stringify(value); | ||
} | ||
decode.push(`${key}=${decodeURIComponent(value)}`); | ||
encode.push(`${key}=${encodeURIComponent(value)}`); | ||
}); | ||
return { decode: decode.join('&'), encode: encode.join('&') }; | ||
}, | ||
formatReqData(method, bizContext, publicArgs, config) { | ||
publicArgs = publicArgs || {}; | ||
const params = Object.assign({}, config.params, { | ||
appId: config.appId, | ||
method, | ||
timestamp: moment().format('YYYY-MM-DD HH:mm:ss'), | ||
}, publicArgs); | ||
if (bizContext) { | ||
params.bizContent = JSON.stringify(decamelizeFn(bizContext)); | ||
} | ||
const signature = sign(params, config.privateKey); | ||
params.sign = signature; | ||
return params; | ||
}, | ||
ALIPAY_ALGORITHM_MAPPING, | ||
}; | ||
exports.sign = sign; |
{ | ||
"name": "alipay-sdk", | ||
"version": "2.0.2", | ||
"version": "3.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"main": "lib/alipay.js", | ||
"scripts": { | ||
"pub": "npm version patch && git push origin && git push origin --tag && npm publish && tnpm sync", | ||
"test": "mocha", | ||
"ci": "istanbul cover _mocha" | ||
"tsc": "./node_modules/.bin/tsc -p ./tsconfig.json", | ||
"pub": "npm run tsc && npm version patch && git push origin && git push origin --tag && npm publish && tnpm sync", | ||
"lint": "tslint -p ./tsconfig.json --fix", | ||
"lint:no-fix": "tslint -p ./tsconfig.json", | ||
"test": "npm run tsc && npm run lint && mocha", | ||
"ci": "npm run tsc && npm run lint:no-fix && istanbul cover _mocha" | ||
}, | ||
"author": "", | ||
"author": "tudou527", | ||
"license": "ISC", | ||
"dependencies": { | ||
"camelcase": "^4.0.0", | ||
"decamelize": "^1.2.0", | ||
"extend2": "^1.0.0", | ||
"is-plain-object": "^2.0.1", | ||
"@types/node": "^9.6.0", | ||
"@types/urllib": "^2.25.0", | ||
"camelcase-keys": "^4.2.0", | ||
"is": "^3.2.1", | ||
"moment": "^2.16.0", | ||
"request": "^2.86.0", | ||
"snakecase-keys": "^1.1.1", | ||
"urllib": "^2.17.0" | ||
@@ -25,5 +30,2 @@ }, | ||
"devDependencies": { | ||
"@ali/ci": "^3.11.0", | ||
"eslint": "^3.10.2", | ||
"eslint-config-egg": "^3.2.0", | ||
"istanbul": "^0.4.5", | ||
@@ -33,4 +35,7 @@ "mocha": "^3.1.2", | ||
"should": "^11.1.1", | ||
"sinon": "^1.17.7" | ||
"sinon": "^1.17.7", | ||
"tslint": "^5.8.0", | ||
"tslint-config-airbnb": "^5.4.2", | ||
"typescript": "^2.6.2" | ||
} | ||
} |
309
README.md
@@ -5,11 +5,257 @@ # Alipay SDK | ||
## 使用步骤 | ||
> 第一次使用,请参考[支付宝开放平台配置](#支付宝开放平台配置)设置公钥 | ||
### 1. 注册支付宝开放平台(https://open.alipay.com/),并创建账号 | ||
# SDK 使用文档 | ||
### 2. 生成密钥 | ||
## 1. 实例化 SDK | ||
``` | ||
// TypeScript | ||
import AlipaySdk from 'alipay-sdk'; | ||
const alipaySdk = new AlipaySdk(AlipaySdkConfig); | ||
``` | ||
`AlipaySdkConfig` 配置项 | ||
* 必选 | ||
* `appId`: `String` 开放平台上创建应用时生成的 appId | ||
* `privateKey`: `String` 应用私钥 | ||
* `alipayPublicKey`: `String` 支付宝公钥 | ||
* 可选 | ||
* `timeout`: `Number` 网关超时时间,单位毫秒,默认 `5000` | ||
* `camelcase`: `Boolean` 是否把服务端返回的数据中的字段名从下划线转为驼峰,默认 `true` | ||
### 完整的例子: | ||
``` | ||
// TypeScript | ||
import AlipaySdk from 'alipay-sdk'; | ||
const alipaySdk = new AlipaySdk({ | ||
appId: '2016123456789012', | ||
privateKey: fs.readFileSync('./private-key.pem', 'ascii'), | ||
alipayPublicKey: fs.readFileSync('./public-key.pem', 'ascii'), | ||
}); | ||
``` | ||
## 2. 通过 `exec` 调用 API | ||
``` | ||
// TypeScript | ||
try { | ||
const result = await alipaySdk.exec(method, params, options); | ||
// console.log(result); | ||
} catch (err) { | ||
// ... | ||
} | ||
``` | ||
* exec 参数列表 | ||
* 必选 | ||
* `method`: `String` 调用的 Api,比如 `koubei.marketing.campaign.tags.query` | ||
* 可选 | ||
* `params`: `Object` Api 的请求参数(包含公共参数和请求参数),其中请求参数(对应支付宝开放平台文档中的“请求参数“)通过 `bizContent` 传递 | ||
* `bizContent`: `Object` 所有的请求参数 | ||
* `options`: `Object` 可选项 | ||
* `validateSign`: `Boolean` 是否对返回值验签(依赖实例化时配置的”支付宝公钥“),默认 `false` | ||
* `formData`: `Object` 文件上传类接口的请求参数,,默认 `null` | ||
* `log`: Log 对象,存在时会调用 `info`、`error` 方法写日志,默认 `null` 即不写日志 | ||
* exec 返回值类型: `Promise` | ||
### 完整的例子 | ||
``` | ||
// TypeScript | ||
try { | ||
const result = await alipaySdk.exec('alipay.security.risk.content.analyze', { | ||
// bizContent 的内容为 alipay.security.risk.content.analyze 的请求参数 | ||
bizContent: { | ||
appName: 'appName', | ||
appScene: 'appMainScene', | ||
publishDate: moment().format('YYYY-MM-DD HH:mm:ss'), | ||
accountId: 'account', | ||
accountType: '0', | ||
appMainScene: 'appMainScene', | ||
appMainSceneId: '12345678', | ||
appSceneDataId: 'appSceneDataId', | ||
text: '好好学习。', | ||
linkUrls: [], | ||
pictureUrls: [ | ||
'http://xxxx.aliyuncs.com/UvfTktYfmcBCshhCdeycbPqlXNRcZvKR.jpg', | ||
], | ||
}, | ||
}, { | ||
// 验签 | ||
validateSign: true, | ||
// 打印执行日志 | ||
log: this.logger, | ||
}); | ||
// result 为 API 介绍内容中 “响应参数” 对应的结果 | ||
console.log(result); | ||
} catch (err) { | ||
//... | ||
} | ||
``` | ||
## 其他 | ||
### 文件上传类接口调用 | ||
``` | ||
// 引入 AlipayFormData 并实例化 | ||
import AlipayFormData from 'alipay-sdk/lib/form'; | ||
const formData = new AlipayFormData(); | ||
``` | ||
AlipayFormData 提供了下面 2 个方法,用于增加字段文件: | ||
* `addField(fieldName, fieldValue)` 增加字段,包含 2 个参数 | ||
* `fieldName`: `String` 字段名 | ||
* `fieldValue`: `String` 字段值 | ||
* `addFile(fieldName, fileName, filePath)` 增加文件,包含 3 个参数 | ||
* `fieldName`: `String` 字段名 | ||
* `fileName`: `String` 文件名 | ||
* `filePath`: `String` 文件绝对路径 | ||
#### 完整的例子 | ||
``` | ||
// TypeScript | ||
import AlipayFormData from 'alipay-sdk/lib/form'; | ||
const formData = new AlipayFormData(); | ||
// 增加字段 | ||
formData.addField('imageType', 'jpg'); | ||
formData.addField('imageName', '图片.jpg'); | ||
// 增加上传的文件 | ||
formData.addFile('imageContent', '图片.jpg', path.join(__dirname, './test.jpg')); | ||
try { | ||
const result = alipaySdk.exec( | ||
'alipay.offline.material.image.upload', | ||
// 文件上传类接口 params 需要设置为 {} | ||
{}, | ||
{ | ||
// 通过 formData 设置请求参数 | ||
formData: formData, | ||
validateSign: true, | ||
}, | ||
); | ||
/** | ||
* result 为 API 介绍内容中 “响应参数” 对应的结果 | ||
* 调用成功的情况下,返回值内容如下: | ||
* { | ||
* "code":"10000", | ||
* "msg":"Success", | ||
* "imageId":"4vjkXpGkRhKRH78ylDPJ4QAAACMAAQED", | ||
* "imageUrl":"http://oalipay-dl-django.alicdn.com/rest/1.0/image?fileIds=4vjkXpGkRhKRH78ylDPJ4QAAACMAAQED&zoom=original" | ||
* } | ||
*/ | ||
console.log(result); | ||
} catch (err) { | ||
//... | ||
} | ||
``` | ||
### 页面类接口调用 | ||
页面类接口默认返回的数据为 html 代码片段,比如 PC 支付接口 `alipay.trade.page.pay` 返回的内容为 Form 表单。 | ||
同文件上传,此类接口也需要通过 `AlipayFormData.addField` 来增加参数。此外,AlipayFormData 还提供了 `setMethod` 方法,用于直接返回 url: | ||
* `setMethod(method)` 设置请求方法 | ||
* `method`: `'post' | 'get'` 默认为 post | ||
#### 完整的例子 | ||
##### 返回 form 表单 | ||
``` | ||
// TypeScript | ||
import AlipayFormData from 'alipay-sdk/lib/form'; | ||
const formData = new AlipayFormData(); | ||
formData.addField('notifyUrl', 'http://www.com/notify'); | ||
formData.addField('bizContent', { | ||
out_trade_no: 'out_trade_no', | ||
product_code: 'FAST_INSTANT_TRADE_PAY', | ||
totalAmount: '0.01', | ||
subject: '商品', | ||
body: '商品详情', | ||
}); | ||
try { | ||
const result = alipaySdk.exec( | ||
'alipay.offline.material.image.upload', {}, { | ||
formData: formData, | ||
}, | ||
); | ||
// result 为 form 表单 | ||
console.log(result); | ||
} catch (err) {} | ||
``` | ||
##### 返回支付链接 | ||
``` | ||
// TypeScript | ||
import AlipayFormData from 'alipay-sdk/lib/form'; | ||
const formData = new AlipayFormData(); | ||
// 调用 setMethod 并传入 get,会返回可以跳转到支付页面的 url | ||
formData.setMethod('get'); | ||
formData.addField('notifyUrl', 'http://www.com/notify'); | ||
formData.addField('bizContent', { | ||
out_trade_no: 'out_trade_no', | ||
product_code: 'FAST_INSTANT_TRADE_PAY', | ||
totalAmount: '0.01', | ||
subject: '支付测试', | ||
body: '支付测试', | ||
}); | ||
try { | ||
const result = alipaySdk.exec( | ||
'alipay.offline.material.image.upload', {}, { | ||
formData: formData, | ||
}, | ||
); | ||
// result 为可以跳转到支付链接的 url | ||
console.log(result); | ||
} catch (err) {} | ||
``` | ||
# 支付宝开放平台配置 | ||
## 1. 注册支付宝开放平台账号 | ||
支付宝开放平台: https://open.alipay.com/ | ||
## 2. 生成密钥 | ||
1. 下载 RSA密钥工具:https://docs.open.alipay.com/291/106097/ | ||
2. 切换到生成秘钥 tab,秘钥格式选择“PKCS1(非JAVA适用)” [Differences between “BEGIN RSA PRIVATE KEY” and “BEGIN PRIVATE KEY”](https://stackoverflow.com/questions/20065304/differences-between-begin-rsa-private-key-and-begin-private-key?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa) | ||
2. 切换到生成秘钥 tab,秘钥格式选择“PKCS1(非JAVA适用)” | ||
> 不需要手动修改秘钥格式,SDK 会自动处理 | ||
![img](https://gw.alipayobjects.com/zos/rmsportal/WYvBOnJBmBzBovsqYePF.png) | ||
@@ -19,5 +265,3 @@ | ||
``` | ||
-----BEGIN RSA PRIVATE KEY----- | ||
// 粘贴上一步生成的私钥到这里(不需要换行) | ||
-----END RSA PRIVATE KEY----- | ||
``` | ||
@@ -28,7 +272,5 @@ | ||
``` | ||
-----BEGIN RSA PRIVATE KEY----- | ||
MIIEpQIBAAKCAQEAqATL9/w/B5Siq/C0mKO8CoUqxX9gv2Fxs2Xz8n0Ce2O3qk+zzQxxwpgG77TMQ5JRaFZ8f1hA2Ax8DQVp9NLi5hqX3cZUpt8snWvc2Jz3hNWv81GqTDZCsMQD/HqgLURZxyw1yVHA2sayFcm8Exn1gqd1bNMSdy/VxhtfwKRxBq/vTNtfKus2E15/bQKB+l/mvroYrR5qYOAInEQt0HoTDFKL2+kvq3TD24gT/VbsfpJoy8FU3lT33LkYAOhHridveugXLbd7eK9e4iC02KV1loQBbL7R+UKdvh+5RZQfRIbQfKhDaSwQqTY0l+2u0b7W80BV8M1iMwQ8errtyUuWKQIDAQABAoIBAEcB5/XA7B5XCbyiuKA9qm3Dw8S9xXR8SSIpN0TG4jKlfOyETJee58D2oQ/TF/SCtNbkni7vbFAiTpbuL85hBbV7ja0TcZkofmF1QVtmUxEXggnR/KfC0sKDxK+CX4lh9pM/MugHHfsXuBGPNWXZNbHm9bBtL8OhOrZDwV7X4FCTgKw8qPBX+hO+RuUQ8iDfZMgUWkrSPCAT68XdLU4K9RrPYMHSmE9HgQhkNbtbLpbHgXxL81H9mIwA4DL3FMoh0IwJ+Yx12m1xC6mVQay2e2DRWfOiGFJWgE2EM1+KY1SR2WgsjNM2/Q1QmSijHbTOx2/gW7xbsuazNQRoptoFVIECgYEA1cslOJmWXa3BVTaV/o47bSSYEjwfg/bV9gDkqA9+33oxLvWehaOYjwsLmY9uid95sWD0mQtJj4pXr63e5SBnTd3vB2p5fJ2Cnt9vnpq/nHvnB0xPHB/DhAMCMYm1bV6gKSzimG1DJVsygjWrbz1lEQ0GMJSBidC0mLx2Jl+jCvECgYEAyTBAPzMvKt1OUTbg1UwqoktKOBCWbaTVNst22/NtLIxi0zAl80NgdbNLhH+zesFGnTmFnP/79SshntvbPNAlUOkL1BPPAhHoIUR7ubFxDeDGFQ+DIUtHKBzYSvbekRO1AEPNkhFA3NiYnRrDOGYC+utuyafCppuBSSXpgMzXjrkCgYEAxaEiaS3hB/vk6gappUSJvpzDTqfxYiW9J8kvlgOs/pyP9p7qyRKvphtJv8wNHLpOXiAIO6lpeJ0j7axGjXvkwuBTY4GTiBR6eK6HGhBm7BrFN8PcpVzfeZrmXjC0W8PLPgTV+p2WImQpTqCaNxyD3r0xaZr+HA2nxEEC3votV6ECgYEAnyQLrfJO3RkxWgyOzCnzj2z+yFpWo2Q/Q5it7E4hjZt+kI8FdedV5cRtd+GLlw5LTRKzHf1e0A/OCFrgkLoUymuNb7Q7iuefNrF1LO2u/8tM5Fvg3fUt1Az9Ck88voVYJ116vo/nPsoV7i+9PF90/AY/HEQXNLLNEY9rpPZjjAECgYEAk9RSfc1w5TJbW8EqqSJ+tgyfJ8zP/D9pnZ0zmXeryVwne7YQmOar6KlLGNBKxbMSF6JV+Yo0lPKKdDmeH8Iqbo6l2grDGyeNLOlTug3fgdtFfgvBzJmQNXd8qpR8smzPqbcYwYHZG39l7lZ+lx/h1+qDiEcMRu/r8h3gv04lMxA= | ||
-----END RSA PRIVATE KEY----- | ||
``` | ||
### 3. 设置应用公钥 | ||
## 3. 设置应用公钥 | ||
@@ -41,7 +283,5 @@ 1. 复制 2.2 中生成的应用公钥 | ||
**注意:** 1.0.x 只支持 RSA(SHA1WithRSA) 方式的签名,请务必设置 RSA 秘钥 | ||
![img](https://gw.alipayobjects.com/zos/rmsportal/CyUzmlKmpCNPAPdNevTd.png) | ||
### 4. 保存支付宝公钥 | ||
## 4. 保存支付宝公钥 | ||
@@ -57,5 +297,3 @@ > “支付宝公钥”用于开放平台返回值的进行验签 | ||
``` | ||
-----BEGIN PUBLIC KEY----- | ||
// 粘贴上一步复制的“支付宝公钥”到这里(不需要换行) | ||
-----END PUBLIC KEY----- | ||
``` | ||
@@ -66,47 +304,4 @@ | ||
``` | ||
-----BEGIN PUBLIC KEY----- | ||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqATL9/w/B5Siq/C0mKO8CoUqxX9gv2Fxs2Xz8n0Ce2O3qk+zzQxxwpgG77TMQ5JRaFZ8f1hA2Ax8DQVp9NLi5hqX3cZUpt8snWvc2Jz3hNWv81GqTDZCsMQD/HqgLURZxyw1yVHA2sayFcm8Exn1gqd1bNMSdy/VxhtfwKRxBq/vTNtfKus2E15/bQKB+l/mvroYrR5qYOAInEQt0HoTDFKL2+kvq3TD24gT/VbsfpJoy8FU3lT33LkYAOhHridveugXLbd7eK9e4iC02KV1loQBbL7R+UKdvh+5RZQfRIbQfKhDaSwQqTY0l+2u0b7W80BV8M1iMwQ8errtyUuWKQIDAQAB | ||
-----END PUBLIC KEY----- | ||
``` | ||
## 调用 SDK | ||
Exp: | ||
``` | ||
const AlipaySdk = require('alipay-sdk'); | ||
const sdk = new AlipaySdk({ | ||
appId: '2016101300678716', | ||
privateKey: fs.readFileSync('./private-key.pem', 'ascii'), | ||
alipayPublicKey: fs.readFileSync('./public-key.pem', 'ascii'), | ||
}); | ||
sdk.execute(method, bizContent) | ||
.then(ret => { | ||
// console.log(ret); | ||
}) | ||
.catch(() => { | ||
// ... | ||
}); | ||
``` | ||
* alipaySDKConfig 配置参数 | ||
* 必选参数 | ||
* `appId`: `String` 开放平台上创建应用时生成的 appId | ||
* `privateKey`: `String` 应用私钥 | ||
* `alipayPublicKey`: `String` 支付宝公钥 | ||
* 可选参数 | ||
* `timeout`: `Number` 网关超时时间 | ||
* `camelcase`: `Boolean` 是否把服务端返回的数据下划线转驼峰 | ||
* execute 方法参数列表 | ||
* 必选参数 | ||
* `method`: `String` 调用的 Api,比如 `koubei.marketing.campaign.tags.query` | ||
* `bizContext`: `Object` Api 的请求参数(文档中的“请求参数“) | ||
> **注:** 某些 Api 可能没有请求参数 | ||
* 可选参数 | ||
* `publicArgs`: `String` Api 的公共请求参数(系统会自动处理公共请求参数,某些 Api 有自己特殊的公共请求参数时,请在这里设置) | ||
* `validateSign`: `String` 是否对返回值验签(依赖3.2中配置的”支付宝公钥“) | ||
* `log`: Log 对象,存在时会调用 `info` 方法写执行日志 |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
1
302
31323
8
14
546
2
1
+ Added@types/node@^9.6.0
+ Added@types/urllib@^2.25.0
+ Addedcamelcase-keys@^4.2.0
+ Addedis@^3.2.1
+ Addedrequest@^2.86.0
+ Addedsnakecase-keys@^1.1.1
+ Added@types/node@9.6.61(transitive)
+ Added@types/urllib@2.33.0(transitive)
+ Addedajv@6.12.6(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.2(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedcamelcase-keys@4.2.0(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedis@3.3.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedmap-obj@2.0.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpsl@1.13.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedquick-lru@1.1.0(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsnakecase-keys@1.2.0(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedto-no-case@0.1.1(transitive)
+ Addedto-snake-case@0.1.2(transitive)
+ Addedto-space-case@0.1.2(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedverror@1.10.0(transitive)
- Removedcamelcase@^4.0.0
- Removeddecamelize@^1.2.0
- Removedextend2@^1.0.0
- Removedis-plain-object@^2.0.1
- Removedcall-bind@1.0.7(transitive)
- Removeddecamelize@1.2.0(transitive)
- Removeddefine-data-property@1.1.4(transitive)
- Removedes-define-property@1.0.0(transitive)
- Removedes-errors@1.3.0(transitive)
- Removedextend2@1.0.1(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedget-intrinsic@1.2.4(transitive)
- Removedgopd@1.0.1(transitive)
- Removedhas-property-descriptors@1.0.2(transitive)
- Removedhas-proto@1.0.3(transitive)
- Removedhas-symbols@1.0.3(transitive)
- Removedhasown@2.0.2(transitive)
- Removedis-plain-object@2.0.4(transitive)
- Removedisobject@3.0.1(transitive)
- Removedobject-inspect@1.13.3(transitive)
- Removedqs@6.13.1(transitive)
- Removedset-function-length@1.2.2(transitive)
- Removedside-channel@1.0.6(transitive)