From e908880834a077d19264c9ad2fdb758fd0c1699e Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Fri, 15 May 2026 00:39:35 +0800 Subject: [PATCH] feat(security): extend rateLimit to support named categories with 5 tiers --- lib/api/app.ts | 2 +- lib/auth/rate-limit.ts | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/api/app.ts b/lib/api/app.ts index ebca55f..36463ba 100644 --- a/lib/api/app.ts +++ b/lib/api/app.ts @@ -142,7 +142,7 @@ export const app = new Elysia({ prefix: "/api" }) error: code === "NOT_FOUND" ? "Not found" : "Internal server error", } }) - .use(rateLimit(60, 100)) + .use(rateLimit("default")) .use(betterAuth) // Health .use(healthRoutes) diff --git a/lib/auth/rate-limit.ts b/lib/auth/rate-limit.ts index 3092bdc..301086f 100644 --- a/lib/auth/rate-limit.ts +++ b/lib/auth/rate-limit.ts @@ -50,14 +50,23 @@ function checkRateLimit( return { allowed: true, remaining: max - entry.count, resetAt: entry.resetAt } } +const CATEGORY_LIMITS: Record = { + default: { window: 60, max: 100 }, + auth: { window: 60, max: 20 }, + read: { window: 60, max: 300 }, + write: { window: 60, max: 10 }, + strict: { window: 60, max: 5 }, +} + export const rateLimit = ( - window: number = 60, - max: number = 100, -) => - new Elysia({ name: "rate-limit" }).onRequest(({ request, set }) => { + category: string = "default", +) => { + const { window, max } = CATEGORY_LIMITS[category] ?? CATEGORY_LIMITS.default + + return new Elysia({ name: `rate-limit-${category}` }).onRequest(({ request, set }) => { const ip = getClientIP(request) const path = new URL(request.url).pathname - const key = `${ip}:${path}` + const key = `${category}:${ip}:${path}` const result = checkRateLimit(key, window, max) @@ -76,3 +85,4 @@ export const rateLimit = ( set.headers["X-RateLimit-Remaining"] = String(result.remaining) set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000)) }) +}