| |
| |
| |
| |
| |
|
|
| interface WindowEntry { |
| count: number |
| resetAt: number |
| } |
|
|
| const store = new Map<string, WindowEntry>() |
|
|
| |
| let lastCleanup = Date.now() |
| const CLEANUP_INTERVAL = 60_000 |
|
|
| function cleanup() { |
| const now = Date.now() |
| if (now - lastCleanup < CLEANUP_INTERVAL) return |
| lastCleanup = now |
|
|
| for (const [key, entry] of store) { |
| if (entry.resetAt <= now) { |
| store.delete(key) |
| } |
| } |
| } |
|
|
| export interface RateLimitResult { |
| allowed: boolean |
| remaining: number |
| resetAt: number |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function checkRateLimit( |
| key: string, |
| maxRequests: number, |
| windowMs: number |
| ): RateLimitResult { |
| cleanup() |
|
|
| const now = Date.now() |
| const entry = store.get(key) |
|
|
| if (!entry || entry.resetAt <= now) { |
| |
| store.set(key, { count: 1, resetAt: now + windowMs }) |
| return { allowed: true, remaining: maxRequests - 1, resetAt: now + windowMs } |
| } |
|
|
| if (entry.count < maxRequests) { |
| entry.count++ |
| return { allowed: true, remaining: maxRequests - entry.count, resetAt: entry.resetAt } |
| } |
|
|
| return { allowed: false, remaining: 0, resetAt: entry.resetAt } |
| } |
|
|
| |
| |
| |
| |
| export function getClientIp(headers: Headers): string { |
| const xff = headers.get('x-forwarded-for') |
| if (xff) { |
| |
| const first = xff.split(',')[0]?.trim() |
| if (first) return first |
| } |
| const realIp = headers.get('x-real-ip') |
| if (realIp) return realIp.trim() |
| return 'unknown' |
| } |
|
|
| |
|
|
| |
| export const API_RATE_LIMIT = { maxRequests: 60, windowMs: 60_000 } |
|
|
| |
| export const WRITE_RATE_LIMIT = { maxRequests: 20, windowMs: 60_000 } |
|
|
| |
| export const AUTH_RATE_LIMIT = { maxRequests: 10, windowMs: 60_000 } |
|
|
| |
| export const HEAVY_RATE_LIMIT = { maxRequests: 10, windowMs: 60_000 } |
|
|