Commit1c1276dunknown_key
Merge pull request #81 from ccantynz-alt/claude/review-readme-docs-ulqPK
Merge pull request #81 from ccantynz-alt/claude/review-readme-docs-ulqPK fix(rate-limit): dramatic headroom — 1000/min API, 300/min git
2 files changed+18−71c1276d4ff5e34cbc4557fcfd05ede8cacc9f625
2 changed files+18−7
Modifiedsrc/app.tsx+14−6View fileUnifiedSplit
@@ -160,16 +160,24 @@ app.use("*", async (c, next) => {
160160});
161161// Rate-limit API + auth endpoints.
162162//
163// `/api/*`: anonymous IPs are capped at 200/min; authenticated users get 4×
164// (800/min) so an admin clicking around the operator console doesn't
165// exhaust the anonymous budget. Three dashboard-plumbing endpoints are
166// exempt entirely so they never count against the bucket:
167// /api/version — layout polls every 15s to detect new deploys
163// `/api/*`: 1000/min per IP — generous so an admin clicking around the
164// operator console (or a CDN/proxy concentrating multiple users behind one
165// IP) doesn't hit the wall. Bot-resistant headroom comes from the auth
166// rate limits below, not this one.
167//
168// `authedMultiplier` is set but only fires when an upstream middleware has
169// already populated c.get("user") — most app.use() chains apply softAuth
170// per-route, so the multiplier is best-effort. Keep the anonymous base
171// high enough that humans never feel it.
172//
173// Skip-paths: dashboard plumbing endpoints that the layout polls on a
174// fixed cadence and that we don't want consuming any bucket:
175// /api/version — layout polls every 15s
168176// /api/notifications/count — nav bell unread-count fetcher
169177// /pwa/vapid-public-key — fetched once per push-notification opt-in
170178app.use(
171179 "/api/*",
172 rateLimit(200, 60_000, "api", {
180 rateLimit(1000, 60_000, "api", {
173181 authedMultiplier: 4,
174182 skipPaths: ["/api/version", "/api/notifications/count", "/pwa/vapid-public-key"],
175183 })
Modifiedsrc/middleware/rate-limit.ts+4−1View fileUnifiedSplit
@@ -139,5 +139,8 @@ export function clearRateLimitStore() {
139139
140140export const apiRateLimit = rateLimit(100, 60_000, "api"); // 100 req/min
141141export const authRateLimit = rateLimit(10, 60_000, "auth"); // 10 req/min (login/register)
142export const gitRateLimit = rateLimit(60, 60_000, "git"); // 60 req/min
142// 300/min for git Smart-HTTP. A clone of a busy repo fans out into many
143// info/refs + upload-pack requests; 60 was way too tight when the same IP
144// (a developer's laptop) was clicking around the UI in another tab.
145export const gitRateLimit = rateLimit(300, 60_000, "git");
143146export const searchRateLimit = rateLimit(30, 60_000, "search"); // 30 req/min
144147