fix(mitm): prevent stub from loading at runtime via bypass module

Turbopack resolveAlias (@/mitm/manager → manager.stub.ts) was designed
for build-time safety but Next.js applies aliases to ALL imports —
including dynamic ones. This caused await import("@/mitm/manager") at
runtime to load the stub, which silently returned fake {running: true}
without spawning the MITM proxy. The UI showed "MITM proxy started"
but nothing was actually running.

Fix introduces a two-path design:
- @/mitm/manager        → stub (build-time, safe for Turbopack)
- @/mitm/manager.runtime → real manager (runtime, bypasses alias)

Route handlers now dynamic-import from manager.runtime, which
re-exports from ./manager and does NOT match the alias pattern.

Additional fixes:
- Make stub throw explicit errors at runtime so misconfiguration is
  immediately visible instead of silently faking success
- Add server.cjs to outputFileTracingIncludes (NFT trace) and Dockerfile
  COPY so the MITM server binary exists in standalone/Docker output
This commit is contained in:
Muhammad Tamir
2026-05-07 10:33:11 +07:00
parent 1064b85a7d
commit c5dded8992
6 changed files with 31 additions and 18 deletions

View File

@@ -50,6 +50,8 @@ COPY --from=builder /app/node_modules/split2 ./node_modules/split2
# traced by Next.js standalone output — copy them explicitly.
COPY --from=builder /app/src/lib/db/migrations ./migrations
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
# MITM server.cjs is spawned at runtime via child_process — not traced by nft
COPY --from=builder /app/src/mitm/server.cjs ./src/mitm/server.cjs
COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs
COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs

View File

@@ -83,6 +83,7 @@ const nextConfig = {
// runtime and are NOT always auto-traced by webpack/turbopack.
"/*": [
"./src/lib/db/migrations/**/*",
"./src/mitm/server.cjs",
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
"./open-sse/services/compression/rules/**/*.json",
],

View File

@@ -15,7 +15,7 @@ export async function GET(request) {
if (authError) return authError;
try {
const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager");
const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager.runtime");
const status = await getMitmStatus();
return NextResponse.json({
running: status.running,
@@ -59,7 +59,8 @@ export async function POST(request) {
// (#523) Extract keyId BEFORE validation — Zod strips unknown fields!
const apiKeyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
const apiKey = await resolveApiKey(apiKeyId, rawApiKey);
const { startMitm, getCachedPassword, setCachedPassword } = await import("@/mitm/manager");
const { startMitm, getCachedPassword, setCachedPassword } =
await import("@/mitm/manager.runtime");
const isWin = process.platform === "win32";
const isRootUser = !isWin && isRoot();
const pwd = sudoPassword || getCachedPassword() || "";
@@ -114,7 +115,8 @@ 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 { stopMitm, getCachedPassword, setCachedPassword } =
await import("@/mitm/manager.runtime");
const isWin = process.platform === "win32";
const isRootUser = !isWin && isRoot();
const pwd = sudoPassword || getCachedPassword() || "";

View File

@@ -137,7 +137,7 @@ function readStats(): MitmStats {
}
async function buildMitmResponse() {
const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager");
const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager.runtime");
const status = await getMitmStatus();
const config = readConfig();
const stats = readStats();
@@ -204,7 +204,7 @@ export async function PUT(request: Request) {
if (typeof parsed.data.enabled === "boolean") {
const { getCachedPassword, setCachedPassword, startMitm, stopMitm } =
await import("@/mitm/manager");
await import("@/mitm/manager.runtime");
const { isRoot } = await import("@/mitm/systemCommands");
const isWin = process.platform === "win32";
const isRootUser = !isWin && isRoot();
@@ -247,7 +247,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const { getMitmStatus } = await import("@/mitm/manager");
const { getMitmStatus } = await import("@/mitm/manager.runtime");
const status = await getMitmStatus();
if (status.running) {
return NextResponse.json(

View File

@@ -0,0 +1,5 @@
// Runtime bypass for Turbopack resolveAlias.
// Turbopack maps @/mitm/manager → manager.stub.ts so the build doesn't choke
// on native module imports. Dynamic import() of @/mitm/manager.runtime does NOT
// match that alias and loads the real manager at runtime.
export * from "./manager";

View File

@@ -1,22 +1,25 @@
// 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.
// Dynamic import() in route handlers should load the REAL manager at runtime.
// If this stub is reached at runtime, the build alias is incorrectly applied.
const STUB_ERROR =
"MITM manager stub reached at runtime — build alias applied incorrectly. " +
"Use --webpack for production builds or verify Turbopack is not aliasing at runtime.";
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 getMitmStatus = async () => {
throw new Error(STUB_ERROR);
};
export const startMitm = async (
_apiKey: string,
_sudoPassword: string,
_options: { port?: number } = {}
) => ({
running: false,
pid: null,
});
export const stopMitm = async (_sudoPassword: string) => ({ running: false, pid: null });
): Promise<never> => {
throw new Error(STUB_ERROR);
};
export const stopMitm = async (_sudoPassword: string): Promise<never> => {
throw new Error(STUB_ERROR);
};