x-9r-real-ip and the Host fallback were trusted from client-controlled
headers whenever custom-server.js was not in the request path (npm run
start, start:bun), letting a remote caller pose as local to skip API key
auth and reach LOCAL_ONLY_PATHS (/api/mcp/*, /api/tunnel/enable,
/api/auth/reset-password).
custom-server.js now generates a per-process secret at boot and stamps it
as x-9r-peer-token on every request it sanitizes. hasTrustedPeerHeaders()
(src/lib/auth/trustedPeer.js) gates trust in x-9r-real-ip on that secret;
otherwise the guard falls back to Host only in development, and fails
closed in production. Same gate on loginLimiter.getClientIp() so a spoofed
header cannot rotate the login lockout bucket.
Also: fix isLoopbackHostname for IPv6 (::1, ::ffff:127.0.0.1) which the
old split(":")[0] reduced to empty string; route npm run start /
start:bun through custom-server.js (postbuild copies it into
.next/standalone, build-cli.js fails without it) so documented deployments
keep passwordless local access.
45 lines
1.9 KiB
JavaScript
45 lines
1.9 KiB
JavaScript
import { cpSync, existsSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
export function copyStandaloneAssets({ projectRoot = process.cwd(), distDir = process.env.NEXT_DIST_DIR || ".next" } = {}) {
|
|
if (process.env.NEXT_TRACING_ROOT_MODE === "workspace") {
|
|
console.log("[standalone-assets] Skipping workspace-traced CLI build; CLI packaging handles assets");
|
|
return;
|
|
}
|
|
|
|
const buildDir = resolve(projectRoot, distDir);
|
|
const standaloneDir = resolve(buildDir, "standalone");
|
|
|
|
if (!existsSync(standaloneDir)) {
|
|
console.log(`[standalone-assets] No standalone build found at ${standaloneDir}`);
|
|
return;
|
|
}
|
|
|
|
const staticSource = resolve(buildDir, "static");
|
|
const staticDestination = resolve(standaloneDir, distDir, "static");
|
|
if (existsSync(staticSource)) {
|
|
cpSync(staticSource, staticDestination, { recursive: true, force: true });
|
|
console.log(`[standalone-assets] Copied static assets to ${staticDestination}`);
|
|
}
|
|
|
|
const publicSource = resolve(projectRoot, "public");
|
|
const publicDestination = resolve(standaloneDir, "public");
|
|
if (existsSync(publicSource)) {
|
|
cpSync(publicSource, publicDestination, { recursive: true, force: true });
|
|
console.log(`[standalone-assets] Copied public assets to ${publicDestination}`);
|
|
}
|
|
|
|
// Without it beside server.js the standalone build serves requests unsanitized.
|
|
const serverWrapperSource = resolve(projectRoot, "custom-server.js");
|
|
const serverWrapperDestination = resolve(standaloneDir, "custom-server.js");
|
|
if (existsSync(serverWrapperSource)) {
|
|
cpSync(serverWrapperSource, serverWrapperDestination, { force: true });
|
|
console.log(`[standalone-assets] Copied custom-server.js to ${serverWrapperDestination}`);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(dirname(fileURLToPath(import.meta.url)), "copy-standalone-assets.mjs")) {
|
|
copyStandaloneAssets();
|
|
}
|