@yeepay/yop-typescript-sdk
Version:
TypeScript SDK for interacting with YOP (YeePay Open Platform)
303 lines • 13.8 kB
JavaScript
import { HttpUtils } from "./utils/HttpUtils.js";
import { RsaV3Util } from "./utils/RsaV3Util.js";
import { VerifyUtils } from "./utils/VerifyUtils.js";
/**
* YopClient provides methods for interacting with the repay Open Platform (YOP) API.
* It handles request signing, response verification, and configuration management.
*
* The client can be configured either through an explicit configuration object
* passed to the constructor or via environment variables.
*/
export class YopClient {
/**
* Creates an instance of YopClient.
* ... (constructor docs remain the same) ...
*/
constructor(config) {
this.timeout = 10000; // Default timeout, added explicit type
const loadedConfig = this._loadConfig(config);
this.config = loadedConfig;
// Initialize KeyObject after loading config string/buffer
try {
// Use getPublicKeyObject which now expects string | Buffer
this.yopPublicKeyObject = VerifyUtils.getPublicKeyObject(loadedConfig.yopPublicKey);
}
catch (error) {
// If getPublicKeyObject throws during initialization, re-throw as a critical config error
throw new Error(`[YopClient Constructor] Failed to create YOP public key object during initialization: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Loads and validates the configuration, merging provided config, environment variables, and defaults.
* Returns the configuration containing the public key as string or Buffer.
* @param config Optional configuration object provided during instantiation.
* @returns The validated and merged YopConfig with publicKey as string/Buffer.
* @throws Error if required configuration fields (appKey, appPrivateKey) are missing or if public key loading fails definitively.
*/
_loadConfig(config) {
const defaultBaseUrl = "https://openapi.yeepay.com";
const envBaseUrl = process.env.YOP_API_BASE_URL;
const envAppKey = process.env.YOP_APP_KEY;
const envAppPrivateKey = process.env.YOP_APP_PRIVATE_KEY;
const envYopPublicKey = process.env.YOP_PUBLIC_KEY; // Key as string
let finalConfig = {};
let loadedYopPublicKeyInput; // Store the raw input (string or buffer)
if (config) {
finalConfig = { ...config };
finalConfig.yopApiBaseUrl = config.yopApiBaseUrl ?? envBaseUrl ?? defaultBaseUrl;
// Prioritize public key input from config object if provided
if (config.yopPublicKey) {
loadedYopPublicKeyInput = config.yopPublicKey;
}
}
else {
finalConfig.appKey = envAppKey;
finalConfig.appPrivateKey = envAppPrivateKey;
finalConfig.yopApiBaseUrl = envBaseUrl ?? defaultBaseUrl;
}
// If public key input wasn't loaded from config object, try environment variable
if (!loadedYopPublicKeyInput && envYopPublicKey) {
loadedYopPublicKeyInput = envYopPublicKey;
}
// Assign the loaded public key input to the final config
finalConfig.yopPublicKey = loadedYopPublicKeyInput;
// Validate required fields
if (!finalConfig.appKey) {
const errorMsg = config
? "Missing required configuration: appKey is missing in the provided config object"
: "Missing required configuration: YOP_APP_KEY environment variable is not set";
throw new Error(errorMsg);
}
if (!finalConfig.appPrivateKey) {
const errorMsg = config
? "Missing required configuration: appPrivateKey is missing in the provided config object"
: "Missing required configuration: YOP_APP_PRIVATE_KEY environment variable is not set";
throw new Error(errorMsg);
}
if (!finalConfig.yopPublicKey) {
throw new Error(`[YopClient Config] Missing required yopPublicKey. Please provide it either in the config object or via YOP_PUBLIC_KEY environment variable.`);
}
// Return config with yopPublicKey as string | Buffer
return finalConfig;
}
async request(options) {
const { method, apiUrl, params, body } = options;
const { appKey, appPrivateKey: appPrivateKey, yopApiBaseUrl, } = this.config;
// Use the stored KeyObject for verification
const yopPublicKeyObject = this.yopPublicKeyObject;
const timeout = options.timeout ?? this.timeout;
const contentType = options.contentType ??
(method === "POST"
? "application/x-www-form-urlencoded"
: "application/json");
let requestBodyString;
let fullFetchUrl;
let sdkHeaders = {};
const yopCenterBasePath = `${yopApiBaseUrl.replace(/\/$/, "")}/yop-center`;
const cleanedApiUrl = apiUrl.startsWith("/") ? apiUrl.substring(1) : apiUrl;
const fullUrlString = `${yopCenterBasePath}/${cleanedApiUrl}`;
fullFetchUrl = new URL(fullUrlString);
// --- Header Generation (remains the same, uses RsaV3Util) ---
if (method === "GET" && params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
fullFetchUrl.searchParams.append(key, String(value));
}
});
try {
sdkHeaders = RsaV3Util.getAuthHeaders({
appKey,
appPrivateKey: appPrivateKey,
method: "GET",
url: apiUrl,
params: params,
config: { contentType },
});
}
catch (sdkError) {
const errorMessage = sdkError instanceof Error ? sdkError.message : String(sdkError);
throw new Error(`Failed to generate YOP headers (GET with params): ${errorMessage}`);
}
}
else if (method === "POST" && body) {
try {
sdkHeaders = RsaV3Util.getAuthHeaders({
appKey,
appPrivateKey: appPrivateKey,
method: "POST",
url: apiUrl,
params: body,
config: { contentType },
});
}
catch (sdkError) {
const errorMessage = sdkError instanceof Error ? sdkError.message : String(sdkError);
throw new Error(`Failed to generate YOP headers (POST): ${errorMessage}`);
}
if (contentType === "application/json") {
requestBodyString = JSON.stringify(body);
}
else {
const encodedBodyEntries = Object.entries(body)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => [key, HttpUtils.normalize(String(value))]);
requestBodyString = new URLSearchParams(encodedBodyEntries).toString();
}
}
else if (method === "GET" && !params) {
try {
sdkHeaders = RsaV3Util.getAuthHeaders({
appKey,
appPrivateKey: appPrivateKey,
method: "GET",
url: apiUrl,
config: { contentType },
});
}
catch (sdkError) {
const errorMessage = sdkError instanceof Error ? sdkError.message : String(sdkError);
throw new Error(`Failed to generate YOP headers (GET no params): ${errorMessage}`);
}
}
else {
if (method === "POST" && !body) {
throw new Error("Invalid request configuration: POST method requires a body.");
}
}
const fetchHeaders = new Headers(sdkHeaders);
if (method === "POST" && body) {
fetchHeaders.set("Content-Type", contentType);
}
const fetchOptions = {
method: method,
headers: fetchHeaders,
body: requestBodyString,
};
let timeoutId;
// 只在非测试环境中使用 AbortController
const controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), timeout);
fetchOptions.signal = controller.signal;
let response;
// 在所有环境中,正常发送请求
try {
console.info(`[YopClient] fetch URL: ${fullFetchUrl.toString()}`);
const headersObject = {};
fetchHeaders.forEach((value, key) => { headersObject[key] = value; });
console.info(`[YopClient] fetch Method: ${fetchOptions.method}`);
console.info(`[YopClient] fetch Headers: ${JSON.stringify(headersObject, null, 2)}`);
console.info(`[YopClient] fetch Body: ${requestBodyString}`);
response = await fetch(fullFetchUrl.toString(), fetchOptions);
if (timeoutId) {
clearTimeout(timeoutId);
}
}
catch (fetchError) {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (fetchError instanceof Error && fetchError.name === "AbortError") {
throw new Error(`YeePay API request timed out after ${timeout / 1000} seconds.`);
}
const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
throw new Error(`Network error calling YeePay API: ${errorMessage}`);
}
const responseBodyText = await response.text();
const yopSignHeader = response.headers.get("x-yop-sign");
const yopRequestId = response.headers.get("x-yop-request-id");
let parsedResult = {};
let yopSignBody;
try {
if (responseBodyText.trim() !== "") {
parsedResult = JSON.parse(responseBodyText);
if (typeof parsedResult === 'object' && parsedResult !== null && 'sign' in parsedResult) {
yopSignBody = parsedResult.sign;
}
}
else if (response.ok) {
console.warn("[YopClient] Received empty response body for a successful request.");
}
}
catch (parseError) {
if (responseBodyText.trim() !== "") {
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
console.warn(`[YopClient] Invalid JSON response received: ${responseBodyText}. Parse Error: ${errorMessage}`);
}
else {
console.error("[YopClient] Error parsing empty response body (unexpected).");
}
}
const metadata = {
yopSign: yopSignHeader,
yopRequestId: yopRequestId,
};
const signatureToVerify = yopSignHeader || yopSignBody;
if (signatureToVerify) {
// TODO 修复验签问题
console.info(`${typeof yopPublicKeyObject}`);
// const isValid = VerifyUtils.isValidRsaResult({ // Use isValidRsaResult
// data: responseBodyText,
// sign: signatureToVerify,
// publicKey: yopPublicKeyObject, // Pass the KeyObject
// });
// if (!isValid) {
// throw new Error("Invalid response signature from YeePay");
// } else {
// console.info("[YopClient] Response signature verification successful using signature from", yopSignHeader ? "header." : "body.");
// }
}
else {
if (response.ok) {
console.warn(`[YopClient] Missing signature in response header (x-yop-sign) and body (sign field): ${method} ${apiUrl}`);
}
}
if (!response.ok) {
let errorDetails = responseBodyText;
try {
if (typeof parsedResult === 'object' && parsedResult !== null) {
const errorObj = parsedResult.error || parsedResult;
const code = errorObj?.code || "N/A";
const message = errorObj?.message || responseBodyText;
errorDetails = `Code=${code}, Message=${message}`;
}
}
catch (e) {
// Ignore parsing error
}
throw new Error(`YeePay API HTTP Error: Status=${response.status}, Details=${errorDetails}`);
}
const finalResponse = {
...parsedResult,
stringResult: responseBodyText,
metadata: metadata,
};
if (finalResponse.state && finalResponse.state !== "SUCCESS") {
const error = finalResponse.error;
const errorMessage = `YeePay API Business Error: State=${finalResponse.state}, Code=${error?.code || "N/A"}, Message=${error?.message || "Unknown error"}`;
throw new Error(errorMessage);
}
return finalResponse;
}
async get(apiUrl, params, timeout) {
return this.request({ method: "GET", apiUrl, params, timeout });
}
async post(apiUrl, body, contentType = "application/x-www-form-urlencoded", timeout) {
return this.request({
method: "POST",
apiUrl,
body,
contentType,
timeout,
});
}
async postJson(apiUrl, body, timeout) {
return this.request({
method: "POST",
apiUrl,
body,
contentType: "application/json",
timeout,
});
}
}
//# sourceMappingURL=YopClient.js.map