import express from "express";
import crypto from "crypto";
const app = express();
// Use raw body for signature verification
app.use("/webhooks/fanfare", express.raw({ type: "application/json" }));
function verifyWebhookSignature(payload: Buffer, signature: string, timestamp: string, secret: string): boolean {
// Check timestamp age (prevent replay attacks)
const MAX_AGE_SECONDS = 300;
const now = Math.floor(Date.now() / 1000);
const timestampAge = now - parseInt(timestamp, 10);
if (timestampAge > MAX_AGE_SECONDS) {
console.error("Webhook timestamp too old:", timestampAge, "seconds");
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
const expectedFull = `sha256=${expectedSignature}`;
// Use timing-safe comparison
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedFull));
} catch {
return false;
}
}
app.post("/webhooks/fanfare", (req, res) => {
const signature = req.headers["x-fanfare-signature"] as string;
const timestamp = req.headers["x-fanfare-timestamp"] as string;
if (!signature || !timestamp) {
return res.status(400).send("Missing signature headers");
}
const isValid = verifyWebhookSignature(req.body, signature, timestamp, process.env.WEBHOOK_SECRET!);
if (!isValid) {
console.error("Invalid webhook signature");
return res.status(401).send("Invalid signature");
}
// Parse and process the event
const event = JSON.parse(req.body.toString());
console.log("Verified webhook event:", event.type);
// Handle the event
handleEvent(event);
res.status(200).send("OK");
});
app.listen(3000);