@yeepay/yop-typescript-sdk
Version:
TypeScript SDK for interacting with YOP (YeePay Open Platform)
291 lines • 12.9 kB
JavaScript
import { getUniqueId } from './GetUniqueId.js';
import { HttpUtils } from './HttpUtils.js';
import crypto from 'crypto';
import md5 from 'md5';
export class RsaV3Util {
// Helper function for date formatting (replaces Date.prototype extension)
// Made static to allow mocking in tests
static formatDate(date, fmt) {
const o = {
"M+": date.getMonth() + 1, // Month
"d+": date.getDate(), // Day
"h+": date.getHours(), // Hour
"m+": date.getMinutes(), // Minute
"s+": date.getSeconds(), // Second
"q+": Math.floor((date.getMonth() + 3) / 3), // Quarter
"S": date.getMilliseconds() // Millisecond
};
let formatString = fmt; // Use a local variable to avoid modifying the input parameter directly
if (/(y+)/.test(formatString)) {
formatString = formatString.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp("(" + k + ")").test(formatString)) {
const value = o[k]; // Get the value once
formatString = formatString.replace(RegExp.$1, (RegExp.$1.length === 1) ?
(value.toString()) : // Assert non-null, as k is a valid key of o
(("00" + value).substr(("" + value).length)) // Assert non-null here too
);
}
}
return formatString;
}
/**
* Gets authentication headers for API requests
* @param options - Options for generating auth headers
* @returns Authentication headers
*/
static getAuthHeaders(options) {
const { appKey, method, url, params = {}, appPrivateKey: appPrivateKey, config = { contentType: '' } } = options;
// 修复:移除对 application/json 类型参数的额外处理
// JSON 字符串应该保持原样,不应该对每个字段进行 normalize 处理
const timestamp = RsaV3Util.formatDate(new Date(), "yyyy-MM-ddThh:mm:ssZ"); // Call static method
const authString = 'yop-auth-v3/' + appKey + "/" + timestamp + "/1800";
const HTTPRequestMethod = method;
const CanonicalURI = url;
const CanonicalQueryString = RsaV3Util.getCanonicalQueryString(params, method);
// Define headers to be included in the signature
const headersToSign = {
'x-yop-appkey': appKey,
'x-yop-content-sha256': RsaV3Util.getSha256AndHexStr(params, config, method),
'x-yop-request-id': RsaV3Util.uuid(),
// Add other headers here if they need to be signed, e.g., 'x-yop-date'
// Add content-type if it exists in the config and method is POST
...(method.toUpperCase() === 'POST' && config.contentType && { 'content-type': config.contentType }),
};
// Generate CanonicalHeaders and signedHeaders string according to YOP spec
// 使用修正后的 buildCanonicalHeaders
const { canonicalHeaderString, signedHeadersString } = RsaV3Util.buildCanonicalHeaders(headersToSign);
const CanonicalRequest = authString + "\n" +
HTTPRequestMethod + "\n" +
CanonicalURI + "\n" +
CanonicalQueryString + "\n" +
canonicalHeaderString; // Use the correctly formatted canonical headers
// Prepare all headers for the actual HTTP request
const allHeaders = {
...headersToSign, // Include signed headers
'x-yop-sdk-version': '4.0.12', // 根据实际使用的SDK版本调整
'x-yop-sdk-lang': '@yeepay/yop-typescript-sdk',
// Authorization header will be added after signing
};
// DEBUG: Log the canonical request before signing
console.log("--- Canonical Request (getAuthHeaders) ---");
console.log(CanonicalRequest);
console.log("--- Canonical Header String (getAuthHeaders) ---");
console.log(canonicalHeaderString);
console.log("--- End Canonical Request ---");
// 使用提取的sign方法生成签名
const signToBase64 = this.sign(CanonicalRequest, appPrivateKey);
// Construct auth header using the correctly generated signedHeadersString
allHeaders.Authorization = "YOP-RSA2048-SHA256 " + authString + "/" +
signedHeadersString + "/" + signToBase64; // Use signedHeadersString
return allHeaders; // Return all necessary headers
}
/**
* Builds the canonical header string and the list of signed header names.
* Includes 'content-type' (if present) and all 'x-yop-*' headers.
* @param headersToSign - Headers potentially included in the signature.
* @returns An object containing the canonical header string and the signed headers string.
*/
static buildCanonicalHeaders(headersToSign) {
const canonicalEntries = [];
const signedHeaderNames = []; // 用于构建 signedHeadersString
// 1. 筛选需要参与签名的 Header (content-type 和 x-yop-*)
const headersForSigning = Object.keys(headersToSign)
.filter(key => key.toLowerCase() === 'content-type' || key.toLowerCase().startsWith('x-yop-'))
.reduce((obj, key) => {
// Ensure we don't add undefined/null headers
if (headersToSign[key] !== undefined && headersToSign[key] !== null) {
obj[key] = headersToSign[key];
}
return obj;
}, {});
// 2. 排序、编码并构建 canonicalHeaderString 和 signedHeaderNames
Object.keys(headersForSigning)
.map(key => key.toLowerCase()) // 转小写用于排序和 signedHeadersString
.sort() // 按字母顺序排序
.forEach(lowerCaseKey => {
const originalKey = Object.keys(headersForSigning).find(k => k.toLowerCase() === lowerCaseKey);
if (originalKey === undefined)
return; // 安全检查
const value = headersForSigning[originalKey]?.trim() ?? ''; // 获取原始值并 trim
// 对 key 和 value 进行 URL 编码
const encodedName = HttpUtils.normalize(lowerCaseKey);
const encodedValue = HttpUtils.normalize(value);
canonicalEntries.push(`${encodedName}:${encodedValue}`);
signedHeaderNames.push(lowerCaseKey); // signedHeadersString 使用编码前的、小写的 key
});
const canonicalHeaderString = canonicalEntries.join('\n');
const signedHeadersString = signedHeaderNames.join(';'); // 使用分号连接
return { canonicalHeaderString, signedHeadersString };
}
/**
* Gets canonical query string for API requests
* @param params - Request parameters
* @param method - HTTP method
* @returns Canonical query string
*/
static getCanonicalQueryString(params, method) {
if (method.toLowerCase() === 'post')
return '';
if (Object.keys(params).length === 0)
return '';
return this.getCanonicalParams(params);
}
/**
* Gets canonical headers for API requests
* @param headers - Request headers
* @returns Canonical headers string
*/
// This function is now replaced by buildCanonicalHeaders and can be removed or kept for reference
// static getCanonicalHeaders(headers: Record<string, string>): string {
// const hArray: string[] = [];
// Object.keys(headers).forEach(key => {
// hArray.push(key + ':' + headers[key]);
// });
// return hArray.join('\n');
// }
/**
* Generates a UUID
* @returns UUID string
*/
static uuid() {
const char = getUniqueId(24) + "" + new Date().getTime();
const hash = md5(char);
let uuid = "";
uuid += hash.substr(0, 8) + '-';
uuid += hash.substr(8, 4) + '-';
uuid += hash.substr(12, 4) + '-';
uuid += hash.substr(16, 4) + '-';
uuid += hash.substr(20, 12);
return uuid;
}
/**
* Gets canonical parameters string
* @param params - Request parameters
* @returns Canonical parameters string
*/
static getCanonicalParams(params = {}) {
const paramStrings = [];
for (const key in params) {
let value = params[key];
if (!key) {
continue;
}
if (!value) {
value = "";
}
const normalizedKey = HttpUtils.normalize(key.trim());
let normalizedValue = HttpUtils.normalize(value?.toString()); // Standard single URL encoding for signature calculation
// Note: Double encoding logic was removed as it's handled in YopClient.ts
paramStrings.push(normalizedKey + '=' + normalizedValue);
}
paramStrings.sort();
let strQuery = "";
for (const i in paramStrings) {
const kv = paramStrings[i];
strQuery += strQuery.length === 0 ? "" : "&";
strQuery += kv;
}
return strQuery;
}
/**
* Calculates SHA256 hash and returns hex string
* @param params - Request parameters
* @param config - Configuration options
* @param method - HTTP method
* @returns SHA256 hash as hex string
*/
/**
* 对对象的键进行排序,以确保生成一致的 JSON 字符串
* @param obj - 要排序的对象
* @returns 排序后的对象
*/
static sortObjectKeys(obj) {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return obj;
}
const sortedObj = {};
const keys = Object.keys(obj).sort();
for (const key of keys) {
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
// 递归排序嵌套对象
sortedObj[key] = this.sortObjectKeys(obj[key]);
}
else {
sortedObj[key] = obj[key];
}
}
return sortedObj;
}
/**
* Calculates SHA256 hash and returns hex string
* @param params - Request parameters
* @param config - Configuration options
* @param method - HTTP method
* @returns SHA256 hash as hex string
*/
static getSha256AndHexStr(params, config, method) {
let str = '';
if (config.contentType.includes('application/json') && method.toLowerCase() === 'post') {
// 对 JSON 对象的键进行排序,以确保生成一致的 JSON 字符串
const sortedParams = this.sortObjectKeys(params);
str = JSON.stringify(sortedParams);
}
else {
if (Object.keys(params).length !== 0) {
str = this.getCanonicalParams(params);
}
}
const sign = crypto.createHash('SHA256');
sign.update(str, 'utf8');
const sig = sign.digest('hex');
return sig;
}
/**
* Signs a canonical request string using RSA-SHA256
* @param canonicalRequest - The canonical request string to sign
* @param appPrivateKey - The private key to sign with (PEM format or raw)
* @returns Base64 URL-safe signature with $SHA256 suffix
*/
static sign(canonicalRequest, appPrivateKey) {
// Check if appPrivateKey is already in PEM format
const private_key = appPrivateKey.includes('-----BEGIN PRIVATE KEY-----')
? appPrivateKey
: this.formatPrivateKey(appPrivateKey);
const signer = crypto.createSign('RSA-SHA256');
signer.update(canonicalRequest, 'utf8');
let sig = signer.sign(private_key, 'base64');
// URL safe processing
sig = sig.replace(/[+]/g, '-');
sig = sig.replace(/[/]/g, '_');
// Remove extra '=' padding
sig = sig.replace(/=+$/, ''); // More efficient regex to remove trailing '='
// Add algorithm suffix
return sig + '$SHA256';
}
/**
* Formats a raw private key string into PEM format
* @param rawKey - The raw private key string
* @returns Formatted PEM private key
*/
static formatPrivateKey(rawKey) {
const BEGIN_MARKER = "-----BEGIN PRIVATE KEY-----";
const END_MARKER = "-----END PRIVATE KEY-----";
let formattedKey = "";
const len = rawKey.length;
let start = 0;
while (start <= len) {
if (formattedKey.length) {
formattedKey += rawKey.substr(start, 64) + '\n';
}
else {
formattedKey = rawKey.substr(start, 64) + '\n';
}
start += 64;
}
return BEGIN_MARKER + '\n' + formattedKey + END_MARKER;
}
}
export default RsaV3Util;
//# sourceMappingURL=RsaV3Util.js.map