import { createDecipheriv, createSign, createVerify, randomBytes, } from "node:crypto"; const WECHAT_PAY_API_BASE = "https://api.mch.weixin.qq.com"; type WechatNativeOrderParams = { appId: string; mchId: string; privateKey: string; serialNo: string; description: string; outTradeNo: string; notifyUrl: string; amountFen: number; attach?: string; }; type WechatNativeOrderResponse = { code_url: string; }; type WechatPayNotifyEnvelope = { id: string; create_time: string; event_type: string; resource_type: string; summary: string; resource: { algorithm: string; ciphertext: string; associated_data?: string; nonce: string; }; }; export class WechatPayError extends Error { status: number; constructor(message: string, status = 500) { super(message); this.status = status; } } function normalizePem(pem: string) { return pem.replace(/\\n/g, "\n").trim(); } function buildWechatPaySignature({ method, path, timestamp, nonce, body, privateKey, }: { method: string; path: string; timestamp: string; nonce: string; body: string; privateKey: string; }) { const message = `${method}\n${path}\n${timestamp}\n${nonce}\n${body}\n`; const signer = createSign("RSA-SHA256"); signer.update(message); signer.end(); return signer.sign(normalizePem(privateKey), "base64"); } function buildWechatAuthorization({ mchId, serialNo, timestamp, nonce, signature, }: { mchId: string; serialNo: string; timestamp: string; nonce: string; signature: string; }) { return `WECHATPAY2-SHA256-RSA2048 mchid=\"${mchId}\",nonce_str=\"${nonce}\",timestamp=\"${timestamp}\",serial_no=\"${serialNo}\",signature=\"${signature}\"`; } export function generateWechatOutTradeNo() { const suffix = randomBytes(4).toString("hex"); return `LVT${Date.now()}${suffix}`.slice(0, 32); } export async function createWechatNativeOrder(params: WechatNativeOrderParams) { const path = "/v3/pay/transactions/native"; const timestamp = `${Math.floor(Date.now() / 1000)}`; const nonce = randomBytes(16).toString("hex"); const body = JSON.stringify({ appid: params.appId, mchid: params.mchId, description: params.description, out_trade_no: params.outTradeNo, notify_url: params.notifyUrl, amount: { total: params.amountFen, currency: "CNY", }, attach: params.attach, }); const signature = buildWechatPaySignature({ method: "POST", path, timestamp, nonce, body, privateKey: params.privateKey, }); const authorization = buildWechatAuthorization({ mchId: params.mchId, serialNo: params.serialNo, timestamp, nonce, signature, }); const response = await fetch(`${WECHAT_PAY_API_BASE}${path}`, { method: "POST", headers: { Authorization: authorization, Accept: "application/json", "Content-Type": "application/json", }, body, }); const data = await response.json(); if (!response.ok) { throw new WechatPayError( typeof data?.message === "string" ? data.message : "Failed to create WeChat native order", response.status ); } if (!data?.code_url) { throw new WechatPayError("WeChat order response missing code_url", 502); } return data as WechatNativeOrderResponse; } export function verifyWechatPaySignature({ body, timestamp, nonce, signature, platformPublicKey, }: { body: string; timestamp: string; nonce: string; signature: string; platformPublicKey: string; }) { const message = `${timestamp}\n${nonce}\n${body}\n`; const verifier = createVerify("RSA-SHA256"); verifier.update(message); verifier.end(); return verifier.verify(normalizePem(platformPublicKey), signature, "base64"); } export function decryptWechatPayResource({ ciphertext, nonce, associatedData, apiV3Key, }: { ciphertext: string; nonce: string; associatedData?: string; apiV3Key: string; }) { const key = Buffer.from(apiV3Key, "utf8"); if (key.length !== 32) { throw new WechatPayError("WECHAT_PAY_API_V3_KEY must be 32 bytes", 500); } const data = Buffer.from(ciphertext, "base64"); const authTag = data.subarray(data.length - 16); const encryptedData = data.subarray(0, data.length - 16); const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(nonce, "utf8")); if (associatedData) { decipher.setAAD(Buffer.from(associatedData, "utf8")); } decipher.setAuthTag(authTag); const plaintext = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString("utf8"); return plaintext; } export function parseWechatNotifyPayload({ body, apiV3Key, }: { body: string; apiV3Key: string; }) { const envelope = JSON.parse(body) as WechatPayNotifyEnvelope; const resource = envelope.resource; const plaintext = decryptWechatPayResource({ ciphertext: resource.ciphertext, nonce: resource.nonce, associatedData: resource.associated_data, apiV3Key, }); return { envelope, transaction: JSON.parse(plaintext) as Record, }; }