refactor(security): unify webhook guardrails across channels

This commit is contained in:
Peter Steinberger
2026-03-02 00:31:31 +00:00
parent 58659b931b
commit 3a68c56264
7 changed files with 450 additions and 108 deletions

View File

@@ -22,6 +22,32 @@ export type BoundedCounter = {
clear: () => void;
};
export const WEBHOOK_RATE_LIMIT_DEFAULTS = Object.freeze({
windowMs: 60_000,
maxRequests: 120,
maxTrackedKeys: 4_096,
});
export const WEBHOOK_ANOMALY_COUNTER_DEFAULTS = Object.freeze({
maxTrackedKeys: 4_096,
ttlMs: 6 * 60 * 60_000,
logEvery: 25,
});
export const WEBHOOK_ANOMALY_STATUS_CODES = Object.freeze([400, 401, 408, 413, 415, 429]);
export type WebhookAnomalyTracker = {
record: (params: {
key: string;
statusCode: number;
message: (count: number) => string;
log?: (message: string) => void;
nowMs?: number;
}) => number;
size: () => number;
clear: () => void;
};
export function createFixedWindowRateLimiter(options: {
windowMs: number;
maxRequests: number;
@@ -134,3 +160,37 @@ export function createBoundedCounter(options: {
},
};
}
export function createWebhookAnomalyTracker(options?: {
maxTrackedKeys?: number;
ttlMs?: number;
logEvery?: number;
trackedStatusCodes?: readonly number[];
}): WebhookAnomalyTracker {
const maxTrackedKeys = Math.max(
1,
Math.floor(options?.maxTrackedKeys ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys),
);
const ttlMs = Math.max(0, Math.floor(options?.ttlMs ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs));
const logEvery = Math.max(
1,
Math.floor(options?.logEvery ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.logEvery),
);
const trackedStatusCodes = new Set(options?.trackedStatusCodes ?? WEBHOOK_ANOMALY_STATUS_CODES);
const counter = createBoundedCounter({ maxTrackedKeys, ttlMs });
return {
record: ({ key, statusCode, message, log, nowMs }) => {
if (!trackedStatusCodes.has(statusCode)) {
return 0;
}
const next = counter.increment(key, nowMs);
if (log && (next === 1 || next % logEvery === 0)) {
log(message(next));
}
return next;
},
size: () => counter.size(),
clear: () => counter.clear(),
};
}