diff --git a/src/index.ts b/src/index.ts index 847c052..a3984c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,8 @@ export { DiscordGateway }; const app = createServer(); export default { - async fetch(request: Request, env: Env): Promise { - return app.fetch(request, env); + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + return app.fetch(request, env, ctx); }, async scheduled(_event: ScheduledEvent, env: Env): Promise { diff --git a/src/server.ts b/src/server.ts index 59180ba..d4457fb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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 }); }); diff --git a/src/webhook.ts b/src/webhook.ts index 9a7ecec..312dd1e 100644 --- a/src/webhook.ts +++ b/src/webhook.ts @@ -1,5 +1,21 @@ import type { WebhookEvent, Route, Filter } from "./types"; +const keyCache = new Map(); + +async function getHmacKey(secret: string): Promise { + 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)); }