forked from farhoodlabs/paperclip
feat(server): add github_hmac and none webhook signing modes
Adds two new webhook trigger signing modes for external provider compatibility: - github_hmac: accepts X-Hub-Signature-256 header with HMAC-SHA256(secret, rawBody), no timestamp prefix. Compatible with GitHub, Sentry, and services following the same standard. - none: no authentication; the 24-char hex publicId in the URL acts as the shared secret. For services that cannot add auth headers. The replay window UI field is hidden when these modes are selected since neither uses timestamp-based replay protection. Closes #1892 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,24 @@ 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 ?? {}));
|
||||
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 valid =
|
||||
normalizedSignature.length === expectedHmac.length &&
|
||||
crypto.timingSafeEqual(Buffer.from(normalizedSignature), Buffer.from(expectedHmac));
|
||||
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 +1297,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() ?? "";
|
||||
|
||||
Reference in New Issue
Block a user