mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
fix(webhook): dedup duplicate deliveries, cache HMAC key, pass ExecutionContext
Skip re-processing when the same X-GitHub-Delivery id is seen (KV, 300s TTL) to stop double pushes when both an org and an App webhook target the worker. Pass ExecutionContext through so dispatch runs via waitUntil instead of a fire-and-forget that gets frozen. Cache the imported HMAC key per secret.
This commit is contained in:
parent
c75cc058fc
commit
befed0267a
3 changed files with 37 additions and 13 deletions
|
|
@ -9,8 +9,8 @@ export { DiscordGateway };
|
|||
const app = createServer();
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
return app.fetch(request, env);
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
return app.fetch(request, env, ctx);
|
||||
},
|
||||
|
||||
async scheduled(_event: ScheduledEvent, env: Env): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,21 @@ export function createServer(): Hono<{ Bindings: Env }> {
|
|||
return c.json({ error: "Invalid event" }, 400);
|
||||
}
|
||||
|
||||
const delivery = headers["x-github-delivery"];
|
||||
if (delivery) {
|
||||
const seen = await c.env.KV.get(`delivery:${delivery}`);
|
||||
if (seen) {
|
||||
return c.json({ ok: true, duplicate: true });
|
||||
}
|
||||
await c.env.KV.put(`delivery:${delivery}`, "1", { expirationTtl: 300 });
|
||||
}
|
||||
|
||||
const { loadConfig } = await import("./config");
|
||||
const config = await loadConfig(c.env);
|
||||
dispatchEvent(config, event, c.env).catch((err) => log.error(err, "Dispatch failed"));
|
||||
const dispatch = dispatchEvent(config, event, c.env).catch((err) =>
|
||||
log.error(err, "Dispatch failed"),
|
||||
);
|
||||
c.executionCtx.waitUntil(dispatch);
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,21 @@
|
|||
import type { WebhookEvent, Route, Filter } from "./types";
|
||||
|
||||
const keyCache = new Map<string, CryptoKey>();
|
||||
|
||||
async function getHmacKey(secret: string): Promise<CryptoKey> {
|
||||
const cached = keyCache.get(secret);
|
||||
if (cached) return cached;
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
keyCache.set(secret, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function verifySignature(
|
||||
payload: string,
|
||||
signature: string | undefined,
|
||||
|
|
@ -8,13 +24,7 @@ export async function verifySignature(
|
|||
if (!signature) return false;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
||||
const expected = `sha256=${Array.from(new Uint8Array(sig))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
|
|
@ -79,7 +89,7 @@ function extractBranch(event: WebhookEvent): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function matchFilter(filter: Filter, event: WebhookEvent): boolean {
|
||||
function matchFilter(filter: Filter, event: WebhookEvent, keywordBody?: string): boolean {
|
||||
let value: string | undefined;
|
||||
|
||||
switch (filter.type) {
|
||||
|
|
@ -99,7 +109,7 @@ function matchFilter(filter: Filter, event: WebhookEvent): boolean {
|
|||
value = extractBranch(event);
|
||||
break;
|
||||
case "keyword": {
|
||||
const body = JSON.stringify(event.payload).toLowerCase();
|
||||
const body = keywordBody ?? JSON.stringify(event.payload).toLowerCase();
|
||||
const patterns = Array.isArray(filter.match) ? filter.match : [filter.match];
|
||||
return patterns.some((p) => {
|
||||
try {
|
||||
|
|
@ -123,5 +133,7 @@ function matchFilter(filter: Filter, event: WebhookEvent): boolean {
|
|||
|
||||
export function matchRoute(route: Route, event: WebhookEvent): boolean {
|
||||
if (!route.enabled) return false;
|
||||
return route.filters.every((f) => matchFilter(f, event));
|
||||
const hasKeyword = route.filters.some((f) => f.type === "keyword");
|
||||
const keywordBody = hasKeyword ? JSON.stringify(event.payload).toLowerCase() : undefined;
|
||||
return route.filters.every((f) => matchFilter(f, event, keywordBody));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue