import { decryptWecomCallback, verifyWecomCallbackSignature, wecomSignaturesMatch, WecomCallbackCryptoError, } from "../../../lib/wecom-callback-crypto"; import { extractWecomEncryptField, handleWecomCallbackEvent, parseWecomCallbackXml, } from "../../../lib/wecom-callback-events"; import { isEnabled, getRuntimeEnv } from "../../../lib/runtime-env"; type CallbackConfig = { token: string; encodingAesKey: string; corpId: string; }; function resolveCallbackConfig(): CallbackConfig | null { const env = getRuntimeEnv(); if (!isEnabled(env.WECOM_CALLBACK_ENABLED, false)) return null; const token = String(env.WECOM_CALLBACK_TOKEN ?? "").trim(); const encodingAesKey = String( env.WECOM_CALLBACK_ENCODING_AES_KEY ?? "", ).trim(); if (!token || !encodingAesKey) return null; return { token, encodingAesKey, corpId: String(env.WECOM_CORP_ID ?? "").trim(), }; } function readCallbackQuery(request: Request) { const url = new URL(request.url); return { msgSignature: url.searchParams.get("msg_signature") ?? "", timestamp: url.searchParams.get("timestamp") ?? "", nonce: url.searchParams.get("nonce") ?? "", echostr: url.searchParams.get("echostr") ?? "", }; } export async function GET(request: Request) { const config = resolveCallbackConfig(); if (!config) { return new Response("企业微信回调未启用", { status: 403 }); } const { msgSignature, timestamp, nonce, echostr } = readCallbackQuery(request); if (!msgSignature || !timestamp || !nonce || !echostr) { return new Response("缺少回调校验参数", { status: 400 }); } const expected = verifyWecomCallbackSignature( config.token, timestamp, nonce, echostr, ); if (!wecomSignaturesMatch(expected, msgSignature)) { return new Response("签名校验失败", { status: 403 }); } try { const { message } = decryptWecomCallback(echostr, config.encodingAesKey); return new Response(message, { status: 200, headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } catch (error) { if (error instanceof WecomCallbackCryptoError) { console.warn("[KOC LOOP] wecom callback echostr decrypt failed", error); return new Response(error.message, { status: 400 }); } throw error; } } export async function POST(request: Request) { const config = resolveCallbackConfig(); if (!config) { return new Response("企业微信回调未启用", { status: 403 }); } const { msgSignature, timestamp, nonce } = readCallbackQuery(request); try { const body = await request.text(); const encrypt = extractWecomEncryptField(body); if (!encrypt) { return new Response("回调报文缺少 Encrypt 字段", { status: 400 }); } if (!msgSignature || !timestamp || !nonce) { return new Response("缺少回调校验参数", { status: 400 }); } const expected = verifyWecomCallbackSignature( config.token, timestamp, nonce, encrypt, ); if (!wecomSignaturesMatch(expected, msgSignature)) { return new Response("签名校验失败", { status: 403 }); } const { message, receiveId } = decryptWecomCallback( encrypt, config.encodingAesKey, ); if (config.corpId && receiveId !== config.corpId) { console.warn( "[KOC LOOP] wecom callback receiveid mismatch", { expected: config.corpId }, ); return new Response("receiveid 校验失败", { status: 403 }); } const fields = parseWecomCallbackXml(message); handleWecomCallbackEvent(fields); return new Response("", { status: 200 }); } catch (error) { if (error instanceof WecomCallbackCryptoError) { console.warn("[KOC LOOP] wecom callback decrypt failed", error); return new Response(error.message, { status: 400 }); } console.error("[KOC LOOP] wecom callback failed", error); return new Response("", { status: 200 }); } }