import express from "express"; import crypto from "node:crypto"; import { postPRComment } from "./gitea.js"; import { reviewPullRequest } from "./runner.js"; import { startMatrixBot } from "./matrixBot.js"; const app = express(); app.use( express.json({ verify: (req, _res, buf) => { req.rawBody = buf; }, }) ); const PORT = process.env.PORT || 3001; const WEBHOOK_SECRET = process.env.GITEA_WEBHOOK_SECRET; function verifySignature(req) { if (!WEBHOOK_SECRET) return false; const sig = req.get("X-Gitea-Signature"); if (!sig) return false; const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.rawBody).digest("hex"); const sigBuf = Buffer.from(sig, "hex"); const expBuf = Buffer.from(expected, "hex"); return sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf); } app.get("/healthz", (_req, res) => res.send("ok")); app.post("/webhooks/gitea", async (req, res) => { if (!verifySignature(req)) { return res.status(401).send("bad signature"); } // Ack immediately — Gitea has a short webhook timeout and the review itself takes a while. res.status(202).send("accepted"); const event = req.get("X-Gitea-Event"); const body = req.body; try { if (event === "pull_request" && ["opened", "synchronize"].includes(body.action)) { const { repository, pull_request: pr } = body; const [owner, repo] = repository.full_name.split("/"); const review = await reviewPullRequest({ owner, repo, ref: pr.head.sha, cloneUrl: repository.clone_url, prTitle: pr.title, prBody: pr.body || "", }); await postPRComment(owner, repo, pr.number, review); } } catch (err) { console.error("webhook handling failed:", err); } }); app.listen(PORT, () => { console.log(`claude-agent listening on :${PORT}`); }); startMatrixBot().catch((err) => { console.error("matrix bot failed to start:", err); });