diff --git a/next.config.mjs b/next.config.mjs index ac673e5148..376990afbb 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -4,9 +4,30 @@ const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); /** @type {import('next').NextConfig} */ const nextConfig = { - turbopack: {}, + // Turbopack config: redirect native modules to stubs at build time + turbopack: { + resolveAlias: { + // Point mitm/manager to a stub during build (native child_process/fs can't be bundled) + "@/mitm/manager": "./src/mitm/manager.stub.ts", + }, + }, output: "standalone", - serverExternalPackages: ["better-sqlite3", "zod"], + serverExternalPackages: [ + "better-sqlite3", + "zod", + "child_process", + "fs", + "path", + "os", + "crypto", + "net", + "tls", + "http", + "https", + "stream", + "buffer", + "util", + ], transpilePackages: ["@omniroute/open-sse"], allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.*"], typescript: { @@ -16,15 +37,17 @@ const nextConfig = { images: { unoptimized: true, }, - - // NEXT_PUBLIC_CLOUD_URL is set in .env — do NOT hardcode here (it overrides .env) webpack: (config, { isServer }) => { - // Ignore fs/path modules in browser bundle + // Ignore native Node.js modules in browser bundle if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, fs: false, path: false, + child_process: false, + net: false, + tls: false, + crypto: false, }; } return config; diff --git a/scripts/prepublish.mjs b/scripts/prepublish.mjs index 58cd38e673..fc215fe099 100644 --- a/scripts/prepublish.mjs +++ b/scripts/prepublish.mjs @@ -34,7 +34,11 @@ execSync("npm install", { cwd: ROOT, stdio: "inherit" }); // ── Step 3: Build Next.js ────────────────────────────────── console.log(" 🏗️ Building Next.js (standalone)..."); -execSync("npx next build", { cwd: ROOT, stdio: "inherit" }); +execSync("npx next build", { + cwd: ROOT, + stdio: "inherit", + env: { ...process.env, EXPERIMENTAL_TURBOPACK: "0" }, +}); // ── Step 4: Verify standalone output ─────────────────────── const standaloneDir = join(ROOT, ".next", "standalone"); diff --git a/src/app/api/cli-tools/antigravity-mitm/route.ts b/src/app/api/cli-tools/antigravity-mitm/route.ts index 3b7263b96b..c2b6786719 100644 --- a/src/app/api/cli-tools/antigravity-mitm/route.ts +++ b/src/app/api/cli-tools/antigravity-mitm/route.ts @@ -1,19 +1,15 @@ -"use server"; +// Node.js-only route: uses child_process, fs, path via mitm/manager +// Dynamic imports prevent Turbopack from statically resolving native modules +export const runtime = "nodejs"; import { NextResponse } from "next/server"; -import { - getMitmStatus, - startMitm, - stopMitm, - getCachedPassword, - setCachedPassword, -} from "@/mitm/manager"; import { cliMitmStartSchema, cliMitmStopSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; // GET - Check MITM status export async function GET() { try { + const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager"); const status = await getMitmStatus(); return NextResponse.json({ running: status.running, @@ -51,6 +47,7 @@ export async function POST(request) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { apiKey, sudoPassword } = validation.data; + const { startMitm, getCachedPassword, setCachedPassword } = await import("@/mitm/manager"); const isWin = process.platform === "win32"; const pwd = sudoPassword || getCachedPassword() || ""; @@ -101,6 +98,7 @@ export async function DELETE(request) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { sudoPassword } = validation.data; + const { stopMitm, getCachedPassword, setCachedPassword } = await import("@/mitm/manager"); const isWin = process.platform === "win32"; const pwd = sudoPassword || getCachedPassword() || ""; diff --git a/src/instrumentation.ts b/src/instrumentation.ts index a4ceaab464..d42c5c9b27 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -9,13 +9,14 @@ */ function ensureSecrets(): void { - // eslint-disable-next-line no-eval - const crypto = eval("require")("crypto"); - // eslint-disable-next-line no-eval - const Database = eval("require")("better-sqlite3"); - // eslint-disable-next-line no-eval - const path = eval("require")("path"); - const os = eval("require")("os"); // eslint-disable-line no-eval + // Use createRequire to load CJS native modules without bundling + // (eval("require") is banned in Next.js 16 Edge Runtime checks) + const { createRequire } = require("node:module"); + const _require = createRequire(import.meta.url ?? __filename); + const crypto = _require("crypto"); + const Database = _require("better-sqlite3"); + const path = _require("path"); + const os = _require("os"); function getSecretsDb() { const dataDir = process.env.DATA_DIR || path.join(os.homedir(), ".omniroute"); diff --git a/src/mitm/manager.stub.ts b/src/mitm/manager.stub.ts new file mode 100644 index 0000000000..1e0dcb1155 --- /dev/null +++ b/src/mitm/manager.stub.ts @@ -0,0 +1,18 @@ +// Build-time stub for @/mitm/manager +// Used by Turbopack during next build to avoid native module resolution errors. +// The real module is used at runtime via dynamic import in route handlers. + +export const getCachedPassword = () => null; +export const setCachedPassword = (_pwd: string) => {}; +export const clearCachedPassword = () => {}; +export const getMitmStatus = async () => ({ + running: false, + pid: null, + dnsConfigured: false, + certExists: false, +}); +export const startMitm = async (_apiKey: string, _sudoPassword: string) => ({ + running: false, + pid: null, +}); +export const stopMitm = async (_sudoPassword: string) => ({ running: false, pid: null }); diff --git a/src/mitm/manager.ts b/src/mitm/manager.ts index 02ab88cae3..02e69e717d 100644 --- a/src/mitm/manager.ts +++ b/src/mitm/manager.ts @@ -1,11 +1,18 @@ -import { spawn } from "child_process"; -import path from "path"; -import fs from "fs"; +// ESM imports for child_process, fs, path are deferred to function bodies +// to prevent Turbopack from statically bundling native Node.js modules import { resolveDataDir } from "@/lib/dataPaths"; import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig"; import { generateCert } from "./cert/generate"; import { installCert } from "./cert/install"; +// Lazy-loaded native modules (avoids Turbopack static trace) + +const getPath = () => require("path") as typeof import("path"); + +const getFs = () => require("fs") as typeof import("fs"); + +const getSpawn = () => (require("child_process") as typeof import("child_process")).spawn; + // Store server process let serverProcess = null; let serverPid = null; @@ -23,8 +30,10 @@ export function clearCachedPassword() { _cachedPassword = null; } -// server.js is in same directory as this file -const PID_FILE = path.join(resolveDataDir(), "mitm", ".mitm.pid"); +// Lazily compute PID_FILE path to avoid top-level path.join +function getPidFile() { + return getPath().join(resolveDataDir(), "mitm", ".mitm.pid"); +} // Check if a PID is alive function isProcessAlive(pid) { @@ -40,6 +49,10 @@ function isProcessAlive(pid) { * Get MITM status */ export async function getMitmStatus() { + const fs = getFs(); + const path = getPath(); + const PID_FILE = getPidFile(); + // Check in-memory process first, then fallback to PID file let running = serverProcess !== null && !serverProcess.killed; let pid = serverPid; @@ -83,6 +96,11 @@ export async function getMitmStatus() { * @param {string} sudoPassword - Sudo password for DNS/cert operations */ export async function startMitm(apiKey, sudoPassword) { + const fs = getFs(); + const path = getPath(); + const spawn = getSpawn(); + const PID_FILE = getPidFile(); + // Check if already running if (serverProcess && !serverProcess.killed) { throw new Error("MITM proxy is already running"); @@ -104,7 +122,12 @@ export async function startMitm(apiKey, sudoPassword) { // 4. Start MITM server console.log("Starting MITM server..."); - const serverPath = path.join(process.cwd(), "src/mitm/server.js"); + // Use Buffer.from() to make the path opaque to Turbopack static analysis + // (Turbopack can't resolve base64-decoded strings as module paths) + const serverPath = path.join( + process.cwd(), + Buffer.from("c3JjL21pdG0vc2VydmVyLmpz", "base64").toString() // src/mitm/server.js + ); serverProcess = spawn("node", [serverPath], { env: { ...process.env, @@ -188,6 +211,9 @@ export async function startMitm(apiKey, sudoPassword) { * @param {string} sudoPassword - Sudo password for DNS cleanup */ export async function stopMitm(sudoPassword) { + const fs = getFs(); + const PID_FILE = getPidFile(); + // 1. Kill server process (in-memory or from PID file) const proc = serverProcess; if (proc && !proc.killed) {