// lib/hooks.js — webhook signature verification and push-payload extraction. import { createHmac } from 'node:crypto'; import { safeEqual } from './auth.js'; // Priority: GitHub HMAC header, Gitea HMAC header, GitLab token header, // then the generic URL-secret fallback (Bitbucket, curl, ...). export function verifyWebhook(headers, rawBody, urlSecret, secret) { const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody || ''), 'utf8'); const ghSig = headers['x-hub-signature-256']; if (ghSig) { const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); return safeEqual(String(ghSig).trim(), expected); } const giteaSig = headers['x-gitea-signature']; if (giteaSig) { const expected = createHmac('sha256', secret).update(body).digest('hex'); return safeEqual(String(giteaSig).trim(), expected); } const glToken = headers['x-gitlab-token']; if (glToken) { return safeEqual(String(glToken).trim(), secret); } return safeEqual(String(urlSecret || ''), secret); } const ZERO_SHA_RE = /^0+$/; // GitHub / GitLab / Gitea share `ref` ("refs/heads/"); // Bitbucket uses body.push.changes[0].new.{name,target.hash}. export function extractPush(body) { if (!body || typeof body !== 'object') return { branch: null, commit: null }; let branch = null; let commit = null; if (typeof body.ref === 'string' && body.ref.startsWith('refs/heads/')) { branch = body.ref.slice('refs/heads/'.length); } commit = body.after || (body.head_commit && body.head_commit.id) || body.checkout_sha || null; if (!branch && body.push && Array.isArray(body.push.changes) && body.push.changes[0] && body.push.changes[0].new) { const change = body.push.changes[0].new; if (!change.type || change.type === 'branch') branch = change.name || null; if (change.target && change.target.hash) commit = change.target.hash; } // Branch deletions must never trigger a deploy. if (body.deleted === true) branch = null; if (commit && ZERO_SHA_RE.test(String(commit))) { commit = null; branch = null; } return { branch, commit }; }