178 行
6.2 KiB
JavaScript
178 行
6.2 KiB
JavaScript
const {
|
|
WecomCallbackError,
|
|
assertWecomSignature,
|
|
decryptWecomMessage,
|
|
extractEncryptedFromXml,
|
|
maskWecomIdentifier,
|
|
normalizeApprovalCallbackEvent,
|
|
recordCallbackEvidence,
|
|
sanitizeErrorMessage,
|
|
validateCallbackConfig,
|
|
} = require("../services/integrations/wecomCallbackCrypto");
|
|
const { recordApprovalCallbackEvent } = require("../services/integrations/wecomApprovalArchive");
|
|
|
|
function toSafeString(value, maxLength = 500) {
|
|
if (value == null) return "";
|
|
return String(value).trim().slice(0, maxLength);
|
|
}
|
|
|
|
function callbackConfig(fastify) {
|
|
const config = fastify.config || {};
|
|
return {
|
|
receiveId: config.wecomCorpId,
|
|
token: config.wecomCallbackToken,
|
|
encodingAesKey: config.wecomCallbackEncodingAesKey,
|
|
evidenceFile: config.wecomApprovalCallbackEvidenceFile,
|
|
};
|
|
}
|
|
|
|
function sendPlain(reply, statusCode, body) {
|
|
return reply.status(statusCode).type("text/plain; charset=utf-8").send(body);
|
|
}
|
|
|
|
function classifyCallbackError(error) {
|
|
if (error instanceof WecomCallbackError) {
|
|
if (error.code === "invalid_msg_signature") return { statusCode: 403, body: "invalid signature" };
|
|
if (error.code === "receive_id_mismatch") return { statusCode: 403, body: "receive id mismatch" };
|
|
return { statusCode: 400, body: "invalid callback payload" };
|
|
}
|
|
return { statusCode: 500, body: "callback error" };
|
|
}
|
|
|
|
function readSignatureQuery(request) {
|
|
const query = request.query || {};
|
|
return {
|
|
signature: toSafeString(query.msg_signature, 120),
|
|
timestamp: toSafeString(query.timestamp, 80),
|
|
nonce: toSafeString(query.nonce, 120),
|
|
};
|
|
}
|
|
|
|
async function wecomApprovalCallbackRoutes(fastify, options = {}) {
|
|
fastify.addContentTypeParser(
|
|
["text/xml", "application/xml", "application/octet-stream"],
|
|
{ parseAs: "string" },
|
|
(request, body, done) => done(null, body),
|
|
);
|
|
|
|
fastify.addContentTypeParser(
|
|
/^application\/.*\+xml$/i,
|
|
{ parseAs: "string" },
|
|
(request, body, done) => done(null, body),
|
|
);
|
|
|
|
fastify.get("/wecom/approval/callback", async (request, reply) => {
|
|
const runtimeConfig = callbackConfig(fastify);
|
|
const configCheck = validateCallbackConfig({
|
|
wecomCorpId: runtimeConfig.receiveId,
|
|
wecomCallbackToken: runtimeConfig.token,
|
|
wecomCallbackEncodingAesKey: runtimeConfig.encodingAesKey,
|
|
});
|
|
if (!configCheck.ok) {
|
|
fastify.log.warn({ missing: configCheck.missing }, "WeCom approval callback config missing");
|
|
return sendPlain(reply, 503, "callback config missing");
|
|
}
|
|
|
|
const query = readSignatureQuery(request);
|
|
const encrypted = toSafeString(request.query?.echostr, 8192);
|
|
try {
|
|
assertWecomSignature({
|
|
token: runtimeConfig.token,
|
|
timestamp: query.timestamp,
|
|
nonce: query.nonce,
|
|
encrypted,
|
|
signature: query.signature,
|
|
});
|
|
const decrypted = decryptWecomMessage({
|
|
encrypted,
|
|
encodingAesKey: runtimeConfig.encodingAesKey,
|
|
receiveId: runtimeConfig.receiveId,
|
|
});
|
|
return sendPlain(reply, 200, decrypted.plainXml);
|
|
} catch (error) {
|
|
const classified = classifyCallbackError(error);
|
|
fastify.log.warn({
|
|
code: error?.code || "callback_url_verify_failed",
|
|
message: sanitizeErrorMessage(error?.message || error),
|
|
}, "WeCom approval callback URL verification failed");
|
|
return sendPlain(reply, classified.statusCode, classified.body);
|
|
}
|
|
});
|
|
|
|
fastify.post("/wecom/approval/callback", async (request, reply) => {
|
|
const runtimeConfig = callbackConfig(fastify);
|
|
const configCheck = validateCallbackConfig({
|
|
wecomCorpId: runtimeConfig.receiveId,
|
|
wecomCallbackToken: runtimeConfig.token,
|
|
wecomCallbackEncodingAesKey: runtimeConfig.encodingAesKey,
|
|
});
|
|
if (!configCheck.ok) {
|
|
fastify.log.warn({ missing: configCheck.missing }, "WeCom approval callback config missing");
|
|
return sendPlain(reply, 503, "callback config missing");
|
|
}
|
|
|
|
const query = readSignatureQuery(request);
|
|
const body = toSafeString(request.body, 20000);
|
|
try {
|
|
const encrypted = extractEncryptedFromXml(body);
|
|
assertWecomSignature({
|
|
token: runtimeConfig.token,
|
|
timestamp: query.timestamp,
|
|
nonce: query.nonce,
|
|
encrypted,
|
|
signature: query.signature,
|
|
});
|
|
const decrypted = decryptWecomMessage({
|
|
encrypted,
|
|
encodingAesKey: runtimeConfig.encodingAesKey,
|
|
receiveId: runtimeConfig.receiveId,
|
|
});
|
|
const summary = normalizeApprovalCallbackEvent(decrypted.plainXml);
|
|
const evidenceStore = options.evidenceStore || {
|
|
record: async (eventSummary) => {
|
|
let stored = null;
|
|
if (fastify.prisma) {
|
|
stored = await recordApprovalCallbackEvent({
|
|
prisma: fastify.prisma,
|
|
config: {
|
|
wecomCorpId: runtimeConfig.receiveId,
|
|
},
|
|
summary: eventSummary,
|
|
});
|
|
}
|
|
if (runtimeConfig.evidenceFile || !fastify.prisma) {
|
|
await recordCallbackEvidence(eventSummary, { evidenceFile: runtimeConfig.evidenceFile });
|
|
}
|
|
return stored || { recorded: true, duplicate: false };
|
|
},
|
|
};
|
|
const stored = await evidenceStore.record(summary);
|
|
fastify.log.info({
|
|
eventType: summary.eventType,
|
|
callbackKind: summary.callbackKind,
|
|
approvalType: summary.approvalType,
|
|
statusLabel: summary.statusLabel,
|
|
spNoPrefix: summary.spNoPrefix,
|
|
templateIdPrefix: summary.templateIdPrefix,
|
|
idempotencyKey: maskWecomIdentifier(summary.idempotencyKey),
|
|
duplicate: Boolean(stored?.duplicate),
|
|
}, "WeCom approval callback event accepted");
|
|
return sendPlain(reply, 200, "success");
|
|
} catch (error) {
|
|
const classified = classifyCallbackError(error);
|
|
fastify.log.warn({
|
|
code: error?.code || "callback_event_failed",
|
|
message: sanitizeErrorMessage(error?.message || error),
|
|
}, "WeCom approval callback event rejected");
|
|
return sendPlain(reply, classified.statusCode, classified.body);
|
|
}
|
|
});
|
|
}
|
|
|
|
wecomApprovalCallbackRoutes.__testables = {
|
|
classifyCallbackError,
|
|
readSignatureQuery,
|
|
};
|
|
|
|
module.exports = wecomApprovalCallbackRoutes;
|