diff --git a/apps/cli/src/shared.test.ts b/apps/cli/src/shared.test.ts new file mode 100644 index 0000000000..c3056356d8 --- /dev/null +++ b/apps/cli/src/shared.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { buildWebEnv, type PortConfig } from "./shared"; + +const ports: PortConfig = { + backendPort: 38429, + webPort: 37429, + agentctlPort: 36429, + backendUrl: "http://localhost:38429", +}; + +describe("buildWebEnv", () => { + const originalApiPort = process.env.NEXT_PUBLIC_KANDEV_API_PORT; + afterEach(() => { + if (originalApiPort === undefined) { + delete process.env.NEXT_PUBLIC_KANDEV_API_PORT; + } else { + process.env.NEXT_PUBLIC_KANDEV_API_PORT = originalApiPort; + } + }); + + it("sets NEXT_PUBLIC_KANDEV_API_PORT in dev mode so the browser knows the backend port", () => { + const env = buildWebEnv({ ports }); + + expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBe("38429"); + expect(env.NODE_ENV).not.toBe("production"); + }); + + it("does not set NEXT_PUBLIC_KANDEV_API_PORT in production single-port mode", () => { + // The Go backend reverse-proxies Next.js, so the client must use same-origin. + // A non-empty value would cause the client to build cross-origin URLs like + // `https://host:38429/...` that aren't reachable behind a reverse proxy. + const env = buildWebEnv({ ports, production: true }); + + expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBeUndefined(); + expect(env.NODE_ENV).toBe("production"); + }); + + it("strips a host-level NEXT_PUBLIC_KANDEV_API_PORT in production", () => { + // If an operator has the var in their shell/Docker env, the process.env + // spread would otherwise leak it into the Next.js process and reintroduce + // the cross-origin URL problem this fix addresses. + process.env.NEXT_PUBLIC_KANDEV_API_PORT = "99999"; + + const env = buildWebEnv({ ports, production: true }); + + expect(env.NEXT_PUBLIC_KANDEV_API_PORT).toBeUndefined(); + }); + + it("always sets KANDEV_API_BASE_URL for SSR fetches", () => { + expect(buildWebEnv({ ports }).KANDEV_API_BASE_URL).toBe("http://localhost:38429"); + expect(buildWebEnv({ ports, production: true }).KANDEV_API_BASE_URL).toBe( + "http://localhost:38429", + ); + }); + + it("enables debug flag when requested", () => { + expect(buildWebEnv({ ports, debug: true }).NEXT_PUBLIC_KANDEV_DEBUG).toBe("true"); + expect(buildWebEnv({ ports }).NEXT_PUBLIC_KANDEV_DEBUG).toBeUndefined(); + }); +}); diff --git a/apps/cli/src/shared.ts b/apps/cli/src/shared.ts index 71ac99f0a7..42c5a71170 100644 --- a/apps/cli/src/shared.ts +++ b/apps/cli/src/shared.ts @@ -85,9 +85,6 @@ export function buildWebEnv(options: WebEnvOptions): NodeJS.ProcessEnv { ...process.env, // Server-side: full localhost URL for SSR fetches (Next.js SSR → Go backend) KANDEV_API_BASE_URL: ports.backendUrl, - // Client-side: port injection for dev mode (browser on web port, API on backend port). - // In production, the backend reverse-proxies Next.js so the client uses same-origin. - NEXT_PUBLIC_KANDEV_API_PORT: String(ports.backendPort), PORT: String(ports.webPort), // Ensure Next.js standalone server binds to 127.0.0.1 so localhost health checks work. // Without this, HOSTNAME from the host environment can cause binding issues. @@ -96,6 +93,18 @@ export function buildWebEnv(options: WebEnvOptions): NodeJS.ProcessEnv { if (production) { (env as Record).NODE_ENV = "production"; + // Explicitly unset so a host-level NEXT_PUBLIC_KANDEV_API_PORT (from a .env + // file, Docker env, or CI variable) cannot leak through the process.env + // spread above and reintroduce the cross-origin URL problem. + delete env.NEXT_PUBLIC_KANDEV_API_PORT; + } else { + // Dev mode only: browser hits the web port directly, so the client needs to + // know the backend port for API calls. In production the Go backend + // reverse-proxies Next.js on a single port, so the client uses same-origin + // and this var must NOT be set — otherwise the client builds cross-origin + // URLs like `https://host:38429/...` that aren't reachable behind a + // reverse proxy / ingress / Cloudflare tunnel. + env.NEXT_PUBLIC_KANDEV_API_PORT = String(ports.backendPort); } if (debug) {