File size: 5,005 Bytes
60dad1c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | #!/usr/bin/env node
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import next from "next";
import { bootstrapEnv } from "../build/bootstrap-env.mjs";
import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mjs";
import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs";
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs";
import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs";
import { randomUUID } from "node:crypto";
// Pre-read DATA_DIR from local .env before bootstrap resolves paths
if (!process.env.DATA_DIR) {
try {
const raw = fs.readFileSync(path.join(process.cwd(), ".env"), "utf8");
const match = raw.match(/^DATA_DIR=(.+)$/m);
if (match?.[1]?.trim()) process.env.DATA_DIR = match[1].trim();
} catch {
/* .env ausente ou ilegível — ok, bootstrap usa o padrão */
}
}
// Add check for conflicting app/ directory (Issue #1206)
const rootAppDir = path.join(process.cwd(), "app");
if (fs.existsSync(rootAppDir) && fs.statSync(rootAppDir).isDirectory()) {
console.error("\x1b[31m[FATAL ERROR]\x1b[0m Next.js App Router conflict detected!");
console.error(`A root-level 'app/' directory was found at: ${rootAppDir}`);
console.error("This conflicts with the 'src/app/' directory on Windows environments.");
console.error("Next.js will serve 404s for all pages because it prefers the root 'app/' folder.");
console.error("Please rename or delete the root 'app/' directory before starting OmniRoute.\n");
process.exit(1);
}
const mode = process.argv[2] === "start" ? "start" : "dev";
const dev = mode === "dev";
// Self-heal a stale better-sqlite3 native binary after a Node version switch
// (nvm 22 <-> 24) before bootstrap touches the DB. No-op when the ABI matches.
if (dev) {
ensureNativeSqlite();
}
const bootstrappedEnv = bootstrapEnv();
const runtimePorts = resolveRuntimePorts(bootstrappedEnv);
const mergedEnv = withRuntimePortEnv(bootstrappedEnv, runtimePorts);
for (const [key, value] of Object.entries(mergedEnv)) {
if (value !== undefined) {
process.env[key] = value;
}
}
const { dashboardPort } = runtimePorts;
const hostname = process.env.HOST || "0.0.0.0";
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK === "1";
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
// Per-process secret used to prove the trusted peer-IP stamp came from this
// server (read by the authz middleware in the same process). See peer-stamp.mjs.
ensurePeerStampToken();
const nextApp = next({
dev,
dir: process.cwd(),
hostname,
port: dashboardPort,
turbopack: useTurbopack,
});
async function start() {
await nextApp.prepare();
const requestHandler = nextApp.getRequestHandler();
const upgradeHandler = nextApp.getUpgradeHandler();
const responsesWsProxy = createResponsesWsProxy({
baseUrl: `http://127.0.0.1:${dashboardPort}`,
bridgeSecret: process.env.OMNIROUTE_WS_BRIDGE_SECRET,
});
const wsBridge = createOmnirouteWsBridge({
baseUrl: `http://127.0.0.1:${dashboardPort}`,
});
const server = http.createServer((req, res) => {
// Stamp the real TCP peer IP before Next sees the request, so the authz
// middleware can decide LOCAL_ONLY locality without trusting the Host header.
stampPeerIp(req);
return requestHandler(req, res);
});
server.on("upgrade", async (req, socket, head) => {
try {
const responsesWsHandled = await responsesWsProxy.handleUpgrade(req, socket, head);
if (responsesWsHandled) return;
const handled = await wsBridge.handleUpgrade(req, socket, head);
if (handled) return;
await upgradeHandler(req, socket, head);
} catch (error) {
if (!socket.destroyed) {
socket.destroy(error instanceof Error ? error : undefined);
}
console.error("[WS] Upgrade handling failed:", error);
}
});
server.on("error", (error) => {
console.error("[FATAL] Next custom server failed:", error);
process.exit(1);
});
const shutdown = async (signal) => {
try {
await new Promise((resolve) => server.close(resolve));
await nextApp.close();
} catch (error) {
console.error("[SHUTDOWN] Failed during signal:", signal, error);
} finally {
process.exit(0);
}
};
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
server.listen(dashboardPort, hostname, () => {
const bundler = dev ? (useTurbopack ? "turbopack" : "webpack") : "production";
console.log(
`[Next] ${mode} server listening on http://${hostname}:${dashboardPort} (${bundler})`
);
});
}
start().catch((error) => {
console.error("[FATAL] Failed to start Next custom server:", error);
process.exit(1);
});
|