desloppify: code health overhaul plus abuse hardening (140 green, strict 87.2)
Honest baseline after removing 45 gaming suppressions. Structural: lib/game plus root hooks extraction, CORS origin echo, DEV_ORIGINS layering. Fixes: error-cause binding, discord 5xx split, null-cache guard, co-located tests, rejecting-fetch coverage, naming and typing cleanup, socket controller, cache bound, cascade helpers, latch resets. Hardening: per-IP rate limit on API routes, deterministic-miss UX, retry cooldown plus cap. Suite 140 green, build plus lint clean.
This commit is contained in:
78 files changed
+4908
-1430
No files matched your search
@@ -1,8 +1,85 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { LOCALE_COOKIE, matchLocale } from "./lib/i18n/locales";
|
||||
import { corsHeaders, forbidden, isAllowedOrigin } from "@/lib/api-route";
|
||||
import { LOCALE_COOKIE, matchLocale } from "@/lib/i18n/locales";
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
function withCorsHeaders(response: NextResponse, origin: string | null): NextResponse {
|
||||
const headers = corsHeaders(origin);
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
response.headers.set(key, value);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
const RATE_LIMIT_MAX_REQUESTS = 30;
|
||||
|
||||
const rateLimitHits = new Map<string, number[]>();
|
||||
|
||||
export function _resetRateLimit(): void {
|
||||
rateLimitHits.clear();
|
||||
}
|
||||
|
||||
export function clientIp(request: NextRequest): string {
|
||||
const cloudflareIp = request.headers.get("cf-connecting-ip")?.trim();
|
||||
if (cloudflareIp) {
|
||||
return cloudflareIp;
|
||||
}
|
||||
const forwardedFor = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
||||
if (forwardedFor) {
|
||||
return forwardedFor;
|
||||
}
|
||||
return "direct";
|
||||
}
|
||||
|
||||
function isRateLimited(ip: string, now: number = Date.now()): boolean {
|
||||
const cutoff = now - RATE_LIMIT_WINDOW_MS;
|
||||
const hits = (rateLimitHits.get(ip) ?? []).filter((timestamp) => timestamp > cutoff);
|
||||
if (hits.length >= RATE_LIMIT_MAX_REQUESTS) {
|
||||
rateLimitHits.set(ip, hits);
|
||||
return true;
|
||||
}
|
||||
hits.push(now);
|
||||
rateLimitHits.set(ip, hits);
|
||||
return false;
|
||||
}
|
||||
|
||||
function rateLimited(origin: string | null): NextResponse {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests" },
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Retry-After": String(Math.ceil(RATE_LIMIT_WINDOW_MS / 1000)),
|
||||
...corsHeaders(origin),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handleApi(request: NextRequest): NextResponse {
|
||||
const origin = request.headers.get("origin");
|
||||
if (isRateLimited(clientIp(request))) {
|
||||
return rateLimited(origin);
|
||||
}
|
||||
if (!isAllowedOrigin(request)) {
|
||||
return forbidden(origin);
|
||||
}
|
||||
if (request.method === "OPTIONS") {
|
||||
const response = new NextResponse(null, { status: 204 });
|
||||
withCorsHeaders(response, origin);
|
||||
response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
response.headers.set("Access-Control-Max-Age", "86400");
|
||||
return response;
|
||||
}
|
||||
return withCorsHeaders(NextResponse.next(), origin);
|
||||
}
|
||||
|
||||
export function proxy(request: NextRequest): NextResponse {
|
||||
if (request.nextUrl.pathname.startsWith("/api/")) {
|
||||
return handleApi(request);
|
||||
}
|
||||
if (request.cookies.has(LOCALE_COOKIE)) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user