fix(app): add rate limiting to all API endpoints

Add sliding-window rate limiter in server/dev-auth.ts (60 req/min reads,
10 req/min writes per IP). Apply checkRateLimit() in all Vite plugins
and claude-proxy.ts after auth validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 01:34:41 +00:00
co-authored by Claude Opus 4.6
parent 6425b2f53b
commit 5c4afe00e5
8 changed files with 69 additions and 9 deletions
+2 -1
View File
@@ -3,7 +3,7 @@ import { createServer } from 'http'
import { readFileSync, existsSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { validateDevAuth, handleCorsOptions } from './dev-auth.js'
import { validateDevAuth, handleCorsOptions, checkRateLimit } from './dev-auth.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -291,6 +291,7 @@ const server = createServer((req, res) => {
}
if (!validateDevAuth(req, res)) return
if (!checkRateLimit(req, res, true)) return
let body = ''
req.on('data', (chunk) => { body += chunk })
+51 -2
View File
@@ -1,5 +1,5 @@
/**
* Shared dev server authentication middleware.
* Shared dev server authentication and rate limiting middleware.
* Validates Bearer token on all /api/* requests.
* Token is auto-generated in scripts/dev.sh and injected via VITE_DEV_API_TOKEN.
*/
@@ -7,7 +7,6 @@ import type { IncomingMessage, ServerResponse } from 'http'
const DEV_TOKEN = process.env.VITE_DEV_API_TOKEN ?? ''
/** Validate Authorization header. Returns true if authorized, false if rejected (response already sent). */
/** Validate Authorization header. Returns true if authorized, false if rejected (response already sent). */
export function validateDevAuth(req: IncomingMessage, res: ServerResponse): boolean {
if (!DEV_TOKEN) return true // No token configured, skip auth
@@ -34,3 +33,53 @@ export function handleCorsOptions(res: ServerResponse): void {
})
res.end()
}
// ─── Rate Limiting ──────────────────────────────────────────
const WINDOW_MS = 60_000 // 1 minute window
const READ_LIMIT = 60 // 60 requests per minute for reads
const WRITE_LIMIT = 10 // 10 requests per minute for writes
interface RateBucket {
count: number
resetAt: number
}
const rateBuckets = new Map<string, RateBucket>()
// Clean up stale buckets every 5 minutes
setInterval(() => {
const now = Date.now()
for (const [key, bucket] of rateBuckets) {
if (now > bucket.resetAt) rateBuckets.delete(key)
}
}, 5 * 60_000)
function getClientIp(req: IncomingMessage): string {
return req.socket.remoteAddress ?? 'unknown'
}
/**
* Check rate limit for a request. Returns true if allowed, false if rejected (response already sent).
* @param isWrite - Set to true for write operations (POST/PUT/DELETE) which have a lower limit.
*/
export function checkRateLimit(req: IncomingMessage, res: ServerResponse, isWrite = false): boolean {
const ip = getClientIp(req)
const limit = isWrite ? WRITE_LIMIT : READ_LIMIT
const key = `${ip}:${isWrite ? 'w' : 'r'}`
const now = Date.now()
let bucket = rateBuckets.get(key)
if (!bucket || now > bucket.resetAt) {
bucket = { count: 0, resetAt: now + WINDOW_MS }
rateBuckets.set(key, bucket)
}
bucket.count++
if (bucket.count > limit) {
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '60' })
res.end(JSON.stringify({ error: 'Too many requests' }))
return false
}
return true
}