Ctrl K
ComunidadGuías…

proxy.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { randomBytes } from "crypto";
import { checkAdminRateLimit } from "@/lib/admin-rate-limit";
import { checkCsrfProtection } from "@/lib/csrf";

// /api/logs is authenticated with a per-key API token (not the browser
// session cookie) and is rate-limited separately per key in lib/auth.ts, so
// it's exempt from CSRF checks and the generic admin-mutation rate limit.
// Every other mutating /api/** route is protected by default: this list is
// an explicit exclusion, not an allowlist, so a newly added route can't
// silently skip CSRF/rate-limit protection the way it could before.
const ADMIN_MUTATION_EXEMPT_PATHS = ["/api/logs"];

const MUTATION_METHODS = new Set(["POST", "PATCH", "PUT", "DELETE"]);
function isAdminMutation(request: NextRequest): boolean {
  const { pathname } = request.nextUrl;
  return (
    MUTATION_METHODS.has(request.method) &&
    pathname.startsWith("/api/") &&
    !ADMIN_MUTATION_EXEMPT_PATHS.some(
      (path) => pathname === path || pathname.startsWith(`${path}/`)
    )
  );
}

function buildContentSecurityPolicy(
  request: NextRequest,
  nonce: string
): string {
  const scriptSources = [`'self'`, `'nonce-${nonce}'`];
  if (process.env.NODE_ENV !== "production") {
    scriptSources.push(`'unsafe-eval'`);
  }

  return [
    "default-src 'self'",
    `script-src ${scriptSources.join(" ")}`,
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data:",
    "font-src 'self'",
    `connect-src 'self' ${request.nextUrl.origin}`,
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
  ].join("; ");
}

function continueRequest(request: NextRequest, nonce: string): NextResponse {
  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);

  return NextResponse.next({
    request: {
      headers,
    },
  });
}

function finalizeResponse(
  request: NextRequest,
  response: NextResponse,
  nonce: string
): NextResponse {
  response.headers.set(
    "Content-Security-Policy",
    buildContentSecurityPolicy(request, nonce)
  );
  return response;
}

export function proxy(request: NextRequest) {
  const nonce = randomBytes(16).toString("base64");
  if (!isAdminMutation(request)) {
    return finalizeResponse(request, continueRequest(request, nonce), nonce);
  }

  const csrf = checkCsrfProtection(request);
  if (csrf) {
    return finalizeResponse(request, csrf, nonce);
  }

  const limited = checkAdminRateLimit(request);
  if (limited) {
    return finalizeResponse(request, limited, nonce);
  }

  return finalizeResponse(request, continueRequest(request, nonce), nonce);
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)",
  ],
};