export type DdosCategory = "pages" | "api" | "auth"; /** * Bucket a request path for rate limiting. Everything under `/api/` that is not * an auth route counts as `api` — including `/api/imaging/*` (avatars, badges). * That is intentional: imaging is a deliberate high-volume, cacheable surface, * so it gets the wider api budget rather than the page budget. The bare * `/imaging` renderer path never reaches this gate (it is excluded by the proxy * matcher and, in production, fronted by the edge proxy directly). */ export function classifyDdos(pathname: string): DdosCategory { if ( pathname === "/api/auth" || pathname.startsWith("/api/auth/") || pathname === "/login" || pathname.startsWith("/login/") || pathname === "/register" || pathname.startsWith("/register/") || pathname === "/admin" || pathname.startsWith("/admin/") ) { return "auth"; } if (pathname.startsWith("/api/")) return "api"; return "pages"; } const HOSTILE_PATH_SEGMENTS = new Set([ "wp-admin", "wp-login.php", "wp-includes", "phpmyadmin", "pma", "adminer", "server-status", ".env", ".git", ]); const HOSTILE_PATH_EXTENSIONS = [".php", ".asp", ".aspx", ".jsp", ".cgi"]; /** * Cheap scanner/exploit triage. These paths are never routes in this app, so a * hit is almost certainly an automated attack sweep; dropping it here costs no * Redis work and never affects genuine traffic. */ export function isSuspiciousPath(pathname: string): boolean { const lower = pathname.toLowerCase(); for (const segment of lower.split("/")) { if (HOSTILE_PATH_SEGMENTS.has(segment)) return true; } for (const extension of HOSTILE_PATH_EXTENSIONS) { if ( lower.endsWith(extension) || lower.includes(`${extension}/`) || lower.includes(`${extension}?`) ) { return true; } } return false; }