import cloud from "@lafjs/cloud"; import { createSign, createVerify, createDecipheriv, randomBytes } from "crypto"; import { reject, validatePeriodId } from "@/goldMiner/config"; export function merchantConfig() { const env = process.env.PAYMENT_APP_ENV || "production"; const cfg = { env, mchid: process.env.WX_MCH_ID, appid: process.env.WX_MINIGAME_APP_ID, serial: process.env.WX_MCH_CERT_SERIAL_NO, bucket: process.env.WX_PAY_PRIVATE_KEY_BUCKET, notifyUrl: process.env.GOLD_MINER_MERCHANT_NOTIFY_URL, pageUrl: process.env.GOLD_MINER_PAY_PAGE_URL, checkUrl: process.env.GOLD_MINER_CHECK_IOS_URL, }; if (!["production", "test"].includes(env)) reject("PAYMENT_CONFIG_UNAVAILABLE", "PAYMENT_APP_ENV 必须为 production 或 test"); for (const name of ["WX_MCH_ID", "WX_MINIGAME_APP_ID", "WX_MCH_CERT_SERIAL_NO", "WX_PAY_PRIVATE_KEY_BUCKET"]) { if (!process.env[name]) reject("PAYMENT_CONFIG_UNAVAILABLE", `服务端缺少 ${name} 配置`); } for (const name of ["GOLD_MINER_MERCHANT_NOTIFY_URL", "GOLD_MINER_PAY_PAGE_URL", "GOLD_MINER_CHECK_IOS_URL"]) { let url: URL; try { url = new URL(process.env[name]); } catch { reject("PAYMENT_CONFIG_UNAVAILABLE", `${name} 必须为有效的 HTTPS 地址`); } if (url.protocol !== "https:" || url.username || url.password || url.hash) reject("PAYMENT_CONFIG_UNAVAILABLE", `${name} 必须使用 HTTPS,且不能包含账号、密码或 URL 片段`); if (name === "GOLD_MINER_CHECK_IOS_URL" && url.pathname !== "/wx/checkIos") reject("PAYMENT_CONFIG_UNAVAILABLE", "GOLD_MINER_CHECK_IOS_URL 的路径必须为 /wx/checkIos"); } let keys: any; try { keys = JSON.parse(process.env.GOLD_MINER_WECHATPAY_PUBLIC_KEYS || "{}"); } catch { reject("PAYMENT_CONFIG_UNAVAILABLE", "GOLD_MINER_WECHATPAY_PUBLIC_KEYS 必须为合法的 JSON 公钥映射对象"); } if (!keys || typeof keys !== "object" || Array.isArray(keys) || !Object.keys(keys).length || Object.values(keys).some(value => typeof value !== "string" || !value.trim())) reject("PAYMENT_CONFIG_UNAVAILABLE", "GOLD_MINER_WECHATPAY_PUBLIC_KEYS 必须为非空对象,每个公钥标识对应非空公钥字符串"); if (Buffer.byteLength(process.env.GOLD_MINER_WECHATPAY_API_V3_KEY || "") !== 32) reject("PAYMENT_CONFIG_UNAVAILABLE", "GOLD_MINER_WECHATPAY_API_V3_KEY 必须为 32 字节"); return cfg; } export async function signMerchant(message: string) { const cfg = merchantConfig(); const file = await cloud.storage.bucket(cfg.bucket).readFile("apiclient_key.pem"); const privateKey = await file.Body.transformToString(); return createSign("RSA-SHA256").update(message).sign(privateKey, "base64"); } /** Verify the original bytes, never a JSON re-serialization. Supports key rotation by serial. */ export function verifyMerchant(raw: string, headers: any) { const header = (name: string) => headers?.get ? headers.get(name) : headers?.[name]; const stamp = header("wechatpay-timestamp"), nonce = header("wechatpay-nonce"); const serial = header("wechatpay-serial"), signature = header("wechatpay-signature"); if (typeof stamp !== "string" || !/^\d+$/.test(stamp) || Math.abs(Date.now() / 1000 - Number(stamp)) > 300 || typeof nonce !== "string" || !nonce || typeof serial !== "string" || typeof signature !== "string" || !/^[A-Za-z0-9+/]+=*$/.test(signature)) reject("INVALID_PAYMENT_SIGNATURE"); let keys: any; try { keys = JSON.parse(process.env.GOLD_MINER_WECHATPAY_PUBLIC_KEYS || "{}"); } catch { reject("PAYMENT_CONFIG_UNAVAILABLE", "GOLD_MINER_WECHATPAY_PUBLIC_KEYS 必须为合法的 JSON 公钥映射对象"); } if (!Object.prototype.hasOwnProperty.call(keys, serial) || typeof keys[serial] !== "string") reject("INVALID_PAYMENT_SIGNATURE"); if (!createVerify("RSA-SHA256").update(`${stamp}\n${nonce}\n${raw}\n`).verify(keys[serial], signature, "base64")) reject("INVALID_PAYMENT_SIGNATURE"); } export function notificationTransaction(raw: string, headers: any) { verifyMerchant(raw, headers); const notice = JSON.parse(raw); const r = notice.resource; const secret = process.env.GOLD_MINER_WECHATPAY_API_V3_KEY; if (notice.event_type !== "TRANSACTION.SUCCESS" || r?.algorithm !== "AEAD_AES_256_GCM" || r.original_type !== "transaction" || !secret || Buffer.byteLength(secret) !== 32 || typeof r.nonce !== "string" || typeof r.ciphertext !== "string") reject("INVALID_PAYMENT"); const encrypted = Buffer.from(r.ciphertext, "base64"); if (encrypted.length <= 16) reject("INVALID_PAYMENT"); const decipher = createDecipheriv("aes-256-gcm", Buffer.from(secret), Buffer.from(r.nonce)); decipher.setAuthTag(encrypted.subarray(-16)); decipher.setAAD(Buffer.from(r.associated_data || "")); return JSON.parse(Buffer.concat([decipher.update(encrypted.subarray(0, -16)), decipher.final()]).toString("utf8")); } export async function merchantRequest(method: "GET" | "POST", path: string, body?: any) { const cfg = merchantConfig(); const raw = body === undefined ? "" : JSON.stringify(body); const nonce = randomBytes(16).toString("hex"), stamp = String(Math.floor(Date.now() / 1000)); const signature = await signMerchant(`${method}\n${path}\n${stamp}\n${nonce}\n${raw}\n`); const result = await fetch("https://api.mch.weixin.qq.com" + path, { method, headers: { Authorization: `WECHATPAY2-SHA256-RSA2048 mchid="${cfg.mchid}",nonce_str="${nonce}",timestamp="${stamp}",serial_no="${cfg.serial}",signature="${signature}"`, Accept: "application/json", "Content-Type": "application/json", }, ...(method === "POST" ? { body: raw } : {}), signal: AbortSignal.timeout(5000), }); const text = await result.text(); verifyMerchant(text, result.headers); const data = text ? JSON.parse(text) : {}; if (!result.ok) reject("MERCHANT_" + String(data.code || result.status)); return data; } export function assertMerchantOrder(order: any) { const cfg = merchantConfig(), g = order?.goldMiner; if (g?.paymentChannel !== "legacy_ios" || g.schemaVersion !== 1 || order.paymentAppEnv !== cfg.env || g.merchantSnapshot?.mchid !== cfg.mchid || g.merchantSnapshot?.appid !== cfg.appid) reject("ORDER_ENVIRONMENT_MISMATCH"); validatePeriodId(g.periodId); return cfg; } export async function queryMerchant(order: any) { const cfg = assertMerchantOrder(order); return merchantRequest("GET", `/v3/pay/transactions/out-trade-no/${encodeURIComponent(order.outTradeNo)}?mchid=${encodeURIComponent(cfg.mchid)}`); } export async function closeMerchant(order: any) { const cfg = assertMerchantOrder(order); return merchantRequest("POST", `/v3/pay/transactions/out-trade-no/${encodeURIComponent(order.outTradeNo)}/close`, { mchid: cfg.mchid }); } export async function prepayMerchant(order: any) { const cfg = assertMerchantOrder(order); return merchantRequest("POST", "/v3/pay/transactions/jsapi", { appid: cfg.appid, mchid: cfg.mchid, description: "黄金矿工活动资格", out_trade_no: order.outTradeNo, notify_url: order.goldMiner.merchantSnapshot.notifyUrl, time_expire: new Date(order.goldMiner.endsAt).toISOString(), amount: { total: order.goodsPrice, currency: "CNY" }, payer: { openid: order.openid }, attach: order.goldMiner.periodId, }); } export default async function () { return { code: 0, msg: "internal module" }; }