mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
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:
commit
9cfa37fce3
5 changed files with 114 additions and 15 deletions
|
|
@ -617,4 +617,72 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
expect(run.status).toBe("issue_created");
|
||||
expect(run.linkedIssueId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("accepts GitHub-style X-Hub-Signature-256 with github_hmac signing mode", async () => {
|
||||
const { routine, svc } = await seedFixture();
|
||||
const { trigger, secretMaterial } = await svc.createTrigger(
|
||||
routine.id,
|
||||
{
|
||||
kind: "webhook",
|
||||
signingMode: "github_hmac",
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const payload = { action: "opened", pull_request: { number: 1 } };
|
||||
const rawBody = Buffer.from(JSON.stringify(payload));
|
||||
const signature = `sha256=${createHmac("sha256", secretMaterial!.webhookSecret)
|
||||
.update(rawBody)
|
||||
.digest("hex")}`;
|
||||
|
||||
const run = await svc.firePublicTrigger(trigger.publicId!, {
|
||||
hubSignatureHeader: signature,
|
||||
rawBody,
|
||||
payload,
|
||||
});
|
||||
|
||||
expect(run.source).toBe("webhook");
|
||||
expect(run.status).toBe("issue_created");
|
||||
});
|
||||
|
||||
it("rejects invalid signature for github_hmac signing mode", async () => {
|
||||
const { routine, svc } = await seedFixture();
|
||||
const { trigger } = await svc.createTrigger(
|
||||
routine.id,
|
||||
{
|
||||
kind: "webhook",
|
||||
signingMode: "github_hmac",
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const rawBody = Buffer.from(JSON.stringify({ ok: true }));
|
||||
|
||||
await expect(
|
||||
svc.firePublicTrigger(trigger.publicId!, {
|
||||
hubSignatureHeader: "sha256=0000000000000000000000000000000000000000000000000000000000000000",
|
||||
rawBody,
|
||||
payload: { ok: true },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("accepts any request with none signing mode", async () => {
|
||||
const { routine, svc } = await seedFixture();
|
||||
const { trigger } = await svc.createTrigger(
|
||||
routine.id,
|
||||
{
|
||||
kind: "webhook",
|
||||
signingMode: "none",
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const run = await svc.firePublicTrigger(trigger.publicId!, {
|
||||
payload: { event: "error.created" },
|
||||
});
|
||||
|
||||
expect(run.source).toBe("webhook");
|
||||
expect(run.status).toBe("issue_created");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export function routineRoutes(db: Db) {
|
|||
const result = await svc.firePublicTrigger(req.params.publicId as string, {
|
||||
authorizationHeader: req.header("authorization"),
|
||||
signatureHeader: req.header("x-paperclip-signature"),
|
||||
hubSignatureHeader: req.header("x-hub-signature-256"),
|
||||
timestampHeader: req.header("x-paperclip-timestamp"),
|
||||
idempotencyKey: req.header("idempotency-key"),
|
||||
rawBody: (req as { rawBody?: Buffer }).rawBody ?? null,
|
||||
|
|
|
|||
|
|
@ -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() ?? "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue