import { createHmac, timingSafeEqual } from "node:crypto"; import { stdin } from "node:process"; import { pathToFileURL } from "node:url"; export function verifyRupaySignature({ header, body, secret, toleranceSeconds = 300, now = Math.floor(Date.now() / 1000), }) { const parts = parseSignatureHeader(header); const timestamp = parts.t; const signature = parts.v1; if (!timestamp || !signature || !/^\d+$/.test(timestamp)) { return false; } if (Math.abs(now - Number(timestamp)) > toleranceSeconds) { return false; } const expected = createHmac("sha256", secret) .update(`${timestamp}.${body}`) .digest("hex"); const expectedBuffer = Buffer.from(expected, "hex"); const signatureBuffer = Buffer.from(signature, "hex"); if (expectedBuffer.length !== signatureBuffer.length) { return false; } return timingSafeEqual(signatureBuffer, expectedBuffer); } function parseSignatureHeader(header) { return Object.fromEntries( header.split(",").map((part) => { const [key, ...value] = part.trim().split("="); return [key, value.join("=")]; }), ); } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { const secret = process.env.RUPAY_WEBHOOK_SECRET; const header = process.env.RUPAY_SIGNATURE; const body = await readStdin(); if (!secret || !header) { console.error( "Set RUPAY_WEBHOOK_SECRET and RUPAY_SIGNATURE, then pipe the raw JSON body on stdin.", ); process.exit(2); } process.exit( verifyRupaySignature({ header, body, secret, }) ? 0 : 1, ); } async function readStdin() { const chunks = []; for await (const chunk of stdin) { chunks.push(Buffer.from(chunk)); } return Buffer.concat(chunks).toString("utf8"); }