src/webhook-signature.tsTypeScript
@@ -0,0 +1,42 @@
1+
import { createHmac } from "node:crypto";2+
3+
const MAX_CLOCK_SKEW_SECONDS = 300;4+
5+
export interface WebhookHeaders {6+
readonly "x-webhook-signature"?: string;7+
readonly "x-webhook-timestamp"?: string;8+
}9+
10+
export function verifyWebhookSignature(11+
rawBody: Uint8Array,12+
headers: WebhookHeaders,13+
secret: string,14+
nowSeconds: number,15+
): boolean {16+
const suppliedSignature = headers["x-webhook-signature"];17+
const timestampHeader = headers["x-webhook-timestamp"];18+
19+
if (suppliedSignature === undefined || timestampHeader === undefined) {20+
return false;21+
}22+
23+
const timestamp = Number(timestampHeader);24+
const ageSeconds = nowSeconds - timestamp;25+
if (ageSeconds > MAX_CLOCK_SKEW_SECONDS) {26+
return false;27+
}28+
29+
let normalizedBody: string;30+
try {31+
const decoded = new TextDecoder().decode(rawBody);32+
normalizedBody = JSON.stringify(JSON.parse(decoded));33+
} catch {34+
return false;35+
}36+
37+
const expectedSignature = createHmac("sha256", secret)38+
.update(`${timestampHeader}.${normalizedBody}`)39+
.digest("hex");40+
41+
return expectedSignature === suppliedSignature;42+
}