From f0847b9e78a37832c0173025d75921858338c0eb Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:38:01 +0100 Subject: [PATCH 1/2] Allow 300 uploads a minute per person (GRYT-1965) Co-Authored-By: Claude Opus 5.5 --- src/middleware/rateLimitHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/rateLimitHttp.ts b/src/middleware/rateLimitHttp.ts index de5d3b86..c7694d47 100644 --- a/src/middleware/rateLimitHttp.ts +++ b/src/middleware/rateLimitHttp.ts @@ -44,7 +44,7 @@ export function httpRateLimit(event: string, rule: RateLimitRule) { export const RL_HTTP_OUTBOUND: RateLimitRule = { limit: 20, windowMs: 60_000, banMs: 60_000 }; /** Writing bytes to disk, which costs storage rather than only time. */ -export const RL_HTTP_UPLOAD: RateLimitRule = { limit: 30, windowMs: 60_000, banMs: 30_000 }; +export const RL_HTTP_UPLOAD: RateLimitRule = { limit: 300, windowMs: 60_000, banMs: 30_000 }; /** An import is one gesture that becomes a request per emoji. No `banMs`: the ban is what took the emoji list down with the writes. */ From e43b0d3aa76aecf7f003b2357d0fdd64676c0930 Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:38:37 +0100 Subject: [PATCH 2/2] Give chat uploads their own limit instead of raising the shared one Co-Authored-By: Claude Opus 5.5 --- src/index.ts | 4 ++-- src/middleware/rateLimitHttp.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index dea6c35f..fd455c2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { startWorkerCapabilityPolling } from "./services/workerCapabilities"; import { join } from "path"; import { config } from "dotenv"; import { applyCors, isOriginAllowed, readAllowedOrigins } from "./config/cors"; -import { RL_HTTP_API, RL_HTTP_EMOJI_WRITE, RL_HTTP_FILE, RL_HTTP_OUTBOUND, RL_HTTP_PUBLIC, RL_HTTP_UPLOAD, httpRateLimit } from "./middleware/rateLimitHttp"; +import { RL_HTTP_API, RL_HTTP_CHAT_UPLOAD, RL_HTTP_EMOJI_WRITE, RL_HTTP_FILE, RL_HTTP_OUTBOUND, RL_HTTP_PUBLIC, RL_HTTP_UPLOAD, httpRateLimit } from "./middleware/rateLimitHttp"; config({ path: "config.env", override: false }); config({ override: false }); import { consola } from "consola"; @@ -342,7 +342,7 @@ app.use("/api/server", httpRateLimit("http:server", RL_HTTP_UPLOAD), serverRoute app.use("/api/messages", httpRateLimit("http:api", RL_HTTP_API), messagesRouter); // Reads and writes share a mount and need different budgets. Express runs every // matching mount, so without the skip a read is charged to the write budget. -const limitUploadWrites = httpRateLimit("http:upload", RL_HTTP_UPLOAD); +const limitUploadWrites = httpRateLimit("http:upload", RL_HTTP_CHAT_UPLOAD); app.use("/api/uploads/files", httpRateLimit("http:file", RL_HTTP_FILE)); app.use( "/api/uploads", diff --git a/src/middleware/rateLimitHttp.ts b/src/middleware/rateLimitHttp.ts index c7694d47..4c8e85c9 100644 --- a/src/middleware/rateLimitHttp.ts +++ b/src/middleware/rateLimitHttp.ts @@ -44,7 +44,10 @@ export function httpRateLimit(event: string, rule: RateLimitRule) { export const RL_HTTP_OUTBOUND: RateLimitRule = { limit: 20, windowMs: 60_000, banMs: 60_000 }; /** Writing bytes to disk, which costs storage rather than only time. */ -export const RL_HTTP_UPLOAD: RateLimitRule = { limit: 300, windowMs: 60_000, banMs: 30_000 }; +export const RL_HTTP_UPLOAD: RateLimitRule = { limit: 30, windowMs: 60_000, banMs: 30_000 }; + +/** Chat files. A Discord import sends hundreds of them in one go, and each is capped by size. */ +export const RL_HTTP_CHAT_UPLOAD: RateLimitRule = { limit: 300, windowMs: 60_000, banMs: 30_000 }; /** An import is one gesture that becomes a request per emoji. No `banMs`: the ban is what took the emoji list down with the writes. */