import { hmacSha256Hex, timingSafeEqualStrings } from "../crypto"; const SLACK_MAX_SKEW_SECONDS = 5 * 60; const LINEAR_MAX_SKEW_MS = 60_000; const MS_PER_SECOND = 1000; /** Signature header sent with every outbound integration payload: `sha256=`. */ export async function signPayload(secret: string, body: string): Promise { return `sha256=${await hmacSha256Hex(secret, body)}`; } /** GitHub style `X-Hub-Signature-256` / our own `X-Gitfor-Signature-256`. */ export async function verifySha256Signature(secret: string, body: string, header: string | null): Promise { if (!header || !secret) return false; return timingSafeEqualStrings(await signPayload(secret, body), header.trim()); } /** Slack request signing: `v0=` HMAC of `v0::`, rejecting stale timestamps to stop replays. */ export async function verifySlackSignature(input: { signingSecret: string; body: string; timestamp: string | null; signature: string | null; nowMs?: number; }): Promise { if (!input.signingSecret || !input.timestamp || !input.signature) return false; if (!/^\d{1,12}$/.test(input.timestamp)) return false; const now = Math.floor((input.nowMs ?? Date.now()) / MS_PER_SECOND); if (Math.abs(now - Number(input.timestamp)) > SLACK_MAX_SKEW_SECONDS) return false; const expected = `v0=${await hmacSha256Hex(input.signingSecret, `v0:${input.timestamp}:${input.body}`)}`; return timingSafeEqualStrings(expected, input.signature.trim()); } /** Linear webhooks: hex HMAC of the raw body in `Linear-Signature`, plus a `webhookTimestamp` within a minute. */ export async function verifyLinearSignature(input: { secret: string; body: string; signature: string | null; nowMs?: number; }): Promise { if (!input.secret || !input.signature) return false; const expected = await hmacSha256Hex(input.secret, input.body); if (!timingSafeEqualStrings(expected, input.signature.trim().toLowerCase())) return false; let timestamp: unknown; try { timestamp = (JSON.parse(input.body) as { webhookTimestamp?: unknown }).webhookTimestamp; } catch { return false; } if (typeof timestamp !== "number") return false; return Math.abs((input.nowMs ?? Date.now()) - timestamp) <= LINEAR_MAX_SKEW_MS; }