import type { Context } from "jsr:@hono/hono@4"; import { getAuthenticatedUser } from "./session.ts"; import { valkey } from "./valkey.ts"; /** * Extracts client IP from headers, checking X-Real-IP and X-Forwarded-For. */ export function getClientIp(c: Context): string { const realIp = c.req.header("x-real-ip"); if (realIp) { return realIp.trim(); } let forwardedFor = c.req.header("x-forwarded-for"); if (forwardedFor) { if (forwardedFor.length > 256) { forwardedFor = forwardedFor.substring(0, 256); } const parts = forwardedFor.split(","); return parts[parts.length - 1].trim(); } return "unknown-ip"; } export async function checkRateLimit( key: string, limit: number, windowMs: number, ): Promise { try { const current = await valkey.incr(key); if (current === 1) { await valkey.pexpire(key, windowMs); } return current <= limit; } catch (err) { console.warn("[Rate Limiter] Valkey error:", err); return true; // Fail open on cache error } } export const rateLimitWrapper = { checkRateLimit, async isRateLimited( key: string, limit: number, windowMs: number, ): Promise { const allowed = await checkRateLimit(key, limit, windowMs); return !allowed; }, }; export const publicRateLimiter = async ( c: Context, next: () => Promise, ) => { const ip = getClientIp(c); const key = `ratelimit:public:${ip}`; const allowed = await rateLimitWrapper.checkRateLimit(key, 10, 60000); if (!allowed) { return c.json({ error: "Too Many Requests" }, 429); } await next(); }; export const adminRateLimiter = async ( c: Context, next: () => Promise, ) => { const auth = await getAuthenticatedUser(c); if (!auth) { return c.json({ error: "Missing or invalid session" }, 401); } const key = `ratelimit:admin:${auth.userId}`; const allowed = await rateLimitWrapper.checkRateLimit(key, 60, 60000); if (!allowed) { return c.json({ error: "Too Many Requests" }, 429); } c.set("userId", auth.userId); await next(); };