mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(reliability): idempotency store, custom webhook replay protection, correlation ids
Add LICENSE (MIT), an IdempotencyStore abstraction with a KV implementation and provider-scoped delivery keys, optional replay protection for custom webhooks (X-WebHooker-Timestamp + X-WebHooker-Nonce), and per-request correlation ids in webhook responses and logs.
This commit is contained in:
parent
92ba2203a5
commit
737dd7af98
15 changed files with 378 additions and 26 deletions
|
|
@ -1,12 +1,55 @@
|
|||
import type { Env, WebhookEvent } from "../../types";
|
||||
import type { Provider } from "../types";
|
||||
import { hmacSha256Hex, timingSafeEqual } from "../hmac";
|
||||
import { verifySignature } from "../github/verify";
|
||||
|
||||
const REPLAY_WINDOW_SECONDS = 300;
|
||||
const NONCE_TTL_SECONDS = 600;
|
||||
|
||||
function parseTimestamp(value: string | undefined): number | null {
|
||||
if (!value) return null;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay protection for `custom` webhooks: the sender adds `X-WebHooker-Timestamp`
|
||||
* (Unix seconds) and `X-WebHooker-Nonce` and signs
|
||||
* `timestamp + "." + nonce + "." + body` instead of the bare body. We reject
|
||||
* timestamps outside a ±5min window and reject any nonce already seen (nonces
|
||||
* live in KV for 10min). Senders without those headers keep the legacy
|
||||
* body-only signature so existing integrations continue to work.
|
||||
*/
|
||||
async function verifyReplayProtected(
|
||||
body: string,
|
||||
headers: Record<string, string>,
|
||||
secret: string,
|
||||
kv: KVNamespace,
|
||||
): Promise<boolean> {
|
||||
const timestamp = parseTimestamp(headers["x-webhooker-timestamp"]);
|
||||
const nonce = headers["x-webhooker-nonce"];
|
||||
const signature = headers["x-webhooker-signature"];
|
||||
if (timestamp == null || !nonce || !secret) return false;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (Math.abs(now - timestamp) > REPLAY_WINDOW_SECONDS) return false;
|
||||
|
||||
const expected = `sha256=${await hmacSha256Hex(secret, `${timestamp}.${nonce}.${body}`)}`;
|
||||
if (!timingSafeEqual(signature ?? "", expected)) return false;
|
||||
|
||||
const nonceKey = `nonce:${nonce}`;
|
||||
const seen = await kv.get(nonceKey);
|
||||
if (seen !== null && seen !== undefined) return false;
|
||||
await kv.put(nonceKey, "1", { expirationTtl: NONCE_TTL_SECONDS });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom webhook provider: accepts arbitrary JSON posts (monitoring, CI,
|
||||
* scripts, ...) that are not signed by a forge. The sender signs the raw body
|
||||
* scripts, ...) that are not signed by a forge. The sender signs the payload
|
||||
* with the tenant's secret using the GitHub-style `sha256=<hex>` HMAC header
|
||||
* `X-WebHooker-Signature`. Payloads become `custom` events that flow through
|
||||
* `X-WebHooker-Signature`, optionally adding replay-protection headers (see
|
||||
* `verifyReplayProtected`). Payloads become `custom` events that flow through
|
||||
* the normal route matching pipeline (a route with `event: custom`).
|
||||
*/
|
||||
export const customProvider: Provider = {
|
||||
|
|
@ -24,7 +67,14 @@ export const customProvider: Provider = {
|
|||
// The tenant webhook handler overrides GITHUB_WEBHOOK_SECRET with the
|
||||
// group's secret; on the legacy global endpoint this falls back to the
|
||||
// operator's global secret.
|
||||
return verifySignature(body, headers["x-webhooker-signature"], env.GITHUB_WEBHOOK_SECRET);
|
||||
const secret = env.GITHUB_WEBHOOK_SECRET;
|
||||
if (
|
||||
headers["x-webhooker-timestamp"] !== undefined &&
|
||||
headers["x-webhooker-nonce"] !== undefined
|
||||
) {
|
||||
return verifyReplayProtected(body, headers, secret, env.KV);
|
||||
}
|
||||
return verifySignature(body, headers["x-webhooker-signature"], secret);
|
||||
},
|
||||
|
||||
parse(body, _headers): WebhookEvent | null {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue