Merge pull request #1961 from antonio-mello-ai/fix/webhook-github-sentry-signing-modes

feat(server): add github_hmac and none webhook signing modes
This commit is contained in:
Dotta 2026-04-07 22:58:14 -05:00 committed by GitHub
commit 9cfa37fce3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 114 additions and 15 deletions

View file

@ -1251,6 +1251,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
firePublicTrigger: async (publicId: string, input: {
authorizationHeader?: string | null;
signatureHeader?: string | null;
hubSignatureHeader?: string | null;
timestampHeader?: string | null;
idempotencyKey?: string | null;
rawBody?: Buffer | null;
@ -1266,8 +1267,29 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
if (!routine) throw notFound("Routine not found");
if (!trigger.enabled || routine.status !== "active") throw conflict("Routine trigger is not active");
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
if (trigger.signingMode === "bearer") {
if (trigger.signingMode === "none") {
// No authentication — the publicId in the URL acts as a shared secret.
} else if (trigger.signingMode === "github_hmac") {
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
const rawBody = input.rawBody ?? Buffer.from(JSON.stringify(input.payload ?? {}));
// Accept X-Hub-Signature-256 (GitHub/Sentry) or fall back to the
// generic X-Paperclip-Signature header so operators can use github_hmac
// mode with either header convention.
const providedSignature = (input.hubSignatureHeader ?? input.signatureHeader)?.trim() ?? "";
if (!providedSignature) throw unauthorized();
const expectedHmac = crypto
.createHmac("sha256", secretValue)
.update(rawBody)
.digest("hex");
const normalizedSignature = providedSignature.replace(/^sha256=/, "");
const normalizedBuf = Buffer.from(normalizedSignature);
const expectedBuf = Buffer.from(expectedHmac);
const valid =
normalizedBuf.length === expectedBuf.length &&
crypto.timingSafeEqual(normalizedBuf, expectedBuf);
if (!valid) throw unauthorized();
} else if (trigger.signingMode === "bearer") {
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
const expected = `Bearer ${secretValue}`;
const provided = input.authorizationHeader?.trim() ?? "";
const expectedBuf = Buffer.from(expected);
@ -1280,6 +1302,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
throw unauthorized();
}
} else {
const secretValue = await resolveTriggerSecret(trigger, routine.companyId);
const rawBody = input.rawBody ?? Buffer.from(JSON.stringify(input.payload ?? {}));
const providedSignature = input.signatureHeader?.trim() ?? "";
const providedTimestamp = input.timestampHeader?.trim() ?? "";