/**
* Rate Limiting
*
* Simple KV-based rate limiter for Cloudflare Workers.
* Uses sliding window approach with KV expiration.
*/
interface RateLimitConfig {
windowMs: number; // Time window in milliseconds
maxRequests: number; // Max requests per window
}
interface RateLimitResult {
allowed: boolean;
remaining: number;
resetAt: number;
}
/**
* Check if a request is allowed under rate limit.
*
* @param kv - KV namespace for storing rate limit counts
* @param key - Unique key for the rate limit (e.g., `chat:user@example.com`)
* @param config - Rate limit configuration
* @returns Whether the request is allowed and remaining quota
*/
export async function checkRateLimit(
kv: KVNamespace,
key: string,
config: RateLimitConfig
): Promise<RateLimitResult> {
const now = Date.now();
const windowStart = Math.floor(now / config.windowMs);
const windowKey = `ratelimit:${key}:${windowStart}`;
// Get current count in this window
const currentCount = parseInt(await kv.get(windowKey) || '0', 10);
// Check if over limit
if (currentCount >= config.maxRequests) {
return {
allowed: false,
remaining: 0,
resetAt: (windowStart + 1) * config.windowMs,
};
}
// Increment counter with expiration
await kv.put(windowKey, String(currentCount + 1), {
expirationTtl: Math.ceil(config.windowMs / 1000) + 1, // +1s buffer
});
return {
allowed: true,
remaining: config.maxRequests - currentCount - 1,
resetAt: (windowStart + 1) * config.windowMs,
};
}
/**
* Default rate limit configurations
*/
export const RATE_LIMITS = {
// Admin chat: 30 requests per minute
adminChat: {
windowMs: 60_000, // 1 minute
maxRequests: 30,
},
// Token creation: 10 per hour
tokenCreation: {
windowMs: 3600_000, // 1 hour
maxRequests: 10,
},
// General API: 100 per minute
generalApi: {
windowMs: 60_000,
maxRequests: 100,
},
} as const;