132 lines
4.1 KiB
TypeScript
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 });
|
|
}
|
|
}
|