53 lines · 2.2 KB
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=<hex HMAC of body>`. */
export async function signPayload(secret: string, body: string): Promise<string> {
  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<boolean> {
  if (!header || !secret) return false;
  return timingSafeEqualStrings(await signPayload(secret, body), header.trim());
}

/** Slack request signing: `v0=` HMAC of `v0:<timestamp>:<body>`, rejecting stale timestamps to stop replays. */
export async function verifySlackSignature(input: {
  signingSecret: string;
  body: string;
  timestamp: string | null;
  signature: string | null;
  nowMs?: number;
}): Promise<boolean> {
  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<boolean> {