Files
koc-loop/app/api/wecom-callback/route.ts

132 lines
4.1 KiB
TypeScript

import {
decryptWecomCallback,
verifyWecomCallbackSignature,
wecomSignaturesMatch,
WecomCallbackCryptoError,
} from "../../../lib/wecom-callback-crypto";
import {
extractWecomEncryptField,
handleWecomCallbackEvent,
parseWecomCallbackXml,
} from "../../../lib/wecom-callback-events";
import { ensureSchema, getRawDb } from "../../../lib/mvp-db";
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);
try {
await ensureSchema();
await handleWecomCallbackEvent(getRawDb(), fields);
} catch (eventError) {
console.error("[KOC LOOP] wecom callback event handling failed", eventError);
}
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 });
}
}