1 import { Redis } from '@upstash/redis';
2 import type { NextRequest } from 'next/server';
3
4-const RATE_LIMIT = 100;
5-const WINDOW_MS = 60_000;
6+interface SlidingWindowConfig {
7+ maxRequests: number;
8+ windowMs: number;
9+ keyPrefix?: string;
10+}
11+
12+const DEFAULT_CONFIG: SlidingWindowConfig = {
13+ maxRequests: 100,
14+ windowMs: 60_000,
15+ keyPrefix: 'rl:sw',
16+};
17
18-export async function rateLimit(req) {
19- const ip = req.headers.get('x-forwarded-for');
20- const count = await redis.incr(ip);
21- if (count === 1) await redis.expire(ip, 60);
22- return count <= RATE_LIMIT;
23-}
24+export async function rateLimit(
25+ req: NextRequest,
26+ config = DEFAULT_CONFIG
27+) {
28+ const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
29+ const now = Date.now();
30+ const windowStart = now - config.windowMs;
31+ const key = `${config.keyPrefix}:${ip}`;
32+
33+ // Atomic sliding window via MULTI
34+ const pipeline = redis.multi();
35+ pipeline.zremrangebyscore(key, 0, windowStart);
36+ pipeline.zadd(key, { score: now, member: crypto.randomUUID() });
37+ pipeline.zcard(key);
38+ pipeline.expire(key, Math.ceil(config.windowMs / 1000));
39+ const results = await pipeline.exec();
40+
41+ const count = results[2] as number;
42+ return {
43+ allowed: count <= config.maxRequests,
44+ remaining: Math.max(0, config.maxRequests - count),
45+ resetAt: now + config.windowMs,
46+ };
47+}