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:
co-authored by
Claude Opus 4.6
parent
6425b2f53b
commit
5c4afe00e5
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user