mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 10:30:37 +09:00
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:
parent
eefe9f39f1
commit
cd19834fab
5 changed files with 109 additions and 15 deletions
|
|
@ -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() ?? "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue