feat: enforce route-level rate limits for webhook auth and user actions

This commit is contained in:
Codex
2026-02-18 13:02:44 +00:00
parent a9ef1e5e23
commit 989b5cf048
4 changed files with 187 additions and 12 deletions

View File

@@ -6,19 +6,43 @@ const { buildApp } = require("../src/app");
const { hmacSHA256Hex } = require("../src/lib/signature");
function createApp(options = {}) {
return buildApp({
config: {
xWebhookSecret: "x-secret",
polarWebhookSecret: "polar-secret",
credit: {
baseCredits: 1,
includedChars: 25000,
stepChars: 10000,
stepCredits: 1,
maxCharsPerArticle: 120000,
},
const baseConfig = {
xWebhookSecret: "x-secret",
polarWebhookSecret: "polar-secret",
rateLimits: {
webhookPerMinute: 120,
authPerMinute: 30,
actionPerMinute: 60,
},
...options,
credit: {
baseCredits: 1,
includedChars: 25000,
stepChars: 10000,
stepCredits: 1,
maxCharsPerArticle: 120000,
},
};
const overrideConfig = options.config || {};
const mergedConfig = {
...baseConfig,
...overrideConfig,
rateLimits: {
...baseConfig.rateLimits,
...(overrideConfig.rateLimits || {}),
},
credit: {
...baseConfig.credit,
...(overrideConfig.credit || {}),
},
};
const appOptions = { ...options };
delete appOptions.config;
return buildApp({
config: mergedConfig,
...appOptions,
});
}
@@ -224,3 +248,56 @@ test("emits persistence snapshots on mutating actions", () => {
assert.equal(typeof latest.updatedAt, "string");
assert.equal(typeof latest.engine, "object");
});
test("rate limits repeated webhook calls", () => {
const app = createApp({
config: {
rateLimits: {
webhookPerMinute: 1,
},
},
});
const first = call(app, {
method: "POST",
path: "/api/webhooks/x",
headers: { "x-forwarded-for": "1.2.3.4", "x-signature": "sha256=deadbeef" },
body: JSON.stringify({}),
});
const second = call(app, {
method: "POST",
path: "/api/webhooks/x",
headers: { "x-forwarded-for": "1.2.3.4", "x-signature": "sha256=deadbeef" },
body: JSON.stringify({}),
});
assert.equal(first.status, 401);
assert.equal(second.status, 429);
});
test("rate limits repeated login attempts from same IP", () => {
const app = createApp({
config: {
rateLimits: {
authPerMinute: 1,
},
},
});
const first = call(app, {
method: "POST",
path: "/auth/dev-login",
headers: { "x-forwarded-for": "5.5.5.5" },
body: "userId=alice&returnTo=%2Fapp",
});
const second = call(app, {
method: "POST",
path: "/auth/dev-login",
headers: { "x-forwarded-for": "5.5.5.5" },
body: "userId=alice&returnTo=%2Fapp",
});
assert.equal(first.status, 303);
assert.equal(second.status, 303);
assert.match(second.headers.location, /Too%20many%20requests/);
});