import { expect, test } from "bun:test"; import { RPC_INTERNAL_HEADER, RPC_PATH_PREFIX } from "@wrnexus/rpc"; import { defaultGatewayHostname, forwardAuthFailure, forwardAuthHeaders, gatewayBrowserRpcHeaders, gatewayProxyHeaders, gatewayWebSocketBackendHeaders, stripUntrustedInternalHeaders, gatewayRestartDelay, internalError, isRpcGatewayPath, stripInternalError, } from "../src/gateway.ts"; import { resolveProductionHostname } from "../src/prod.ts"; test("gateway-managed production apps bind to loopback", () => { expect(resolveProductionHostname(undefined, "127.0.0.1")).toBe("127.0.0.1"); expect(resolveProductionHostname("0.0.0.0", "127.0.0.1")).toBe("127.0.0.1"); expect(resolveProductionHostname("10.0.0.5", "")).toBe("10.0.0.5"); expect(resolveProductionHostname(undefined, "")).toBe("0.0.0.0"); }); test("gateway uses platform-safe hostname defaults", () => { expect(defaultGatewayHostname("development")).toBe("127.0.0.1"); expect(defaultGatewayHostname("production")).toBe("0.0.0.0"); }); test("gateway disables compression for its internal proxy hop", () => { const request = new Request("http://localhost:3000/path", { headers: { host: "web.localhost:3000", "accept-encoding": "gzip, deflate" }, }); const headers = gatewayProxyHeaders(request, new URL(request.url), "127.0.0.1", true); expect(headers.get("accept-encoding")).toBe("identity"); expect(headers.get("x-forwarded-host")).toBe("web.localhost:3000"); expect(headers.get("x-forwarded-proto")).toBe("http"); expect(headers.get("x-forwarded-for")).toBe("127.0.0.1"); }); test("gateway WebSocket bridge forwards validated application identity", () => { const request = new Request("http://web.localhost:3000/__wrnexus/hmr", { headers: { host: "web.localhost:3000", origin: "http://web.localhost:3000", cookie: "session=abc", connection: "Upgrade", upgrade: "websocket", "sec-websocket-key": "test-key", }, }); const headers = gatewayWebSocketBackendHeaders( request, new URL(request.url), "127.0.0.1", true, "http://127.0.0.1:3001", ); expect(headers.origin).toBe("http://127.0.0.1:3001"); expect(headers.cookie).toBe("session=abc"); expect(headers["x-forwarded-host"]).toBe("web.localhost:3000"); expect(headers["x-forwarded-proto"]).toBe("http"); expect(headers.host).toBeUndefined(); expect(headers.connection).toBeUndefined(); expect(headers.upgrade).toBeUndefined(); expect(headers["sec-websocket-key"]).toBeUndefined(); }); test("gateway proxy headers do not preserve the RPC internal marker", () => { const request = new Request("http://localhost:3000/path", { headers: { "x-wrnexus-internal": "1" }, }); const headers = stripUntrustedInternalHeaders( gatewayProxyHeaders(request, new URL(request.url), "127.0.0.1", true), ); expect(headers.has("x-wrnexus-internal")).toBe(false); }); test("forward auth preserves intentional verifier redirects", () => { const redirected = forwardAuthFailure( new Response(null, { status: 302, headers: { location: "/login?returnTo=%2Fadmin" } }), "http://sso.localhost:3000/api/verify", ); const denied = forwardAuthFailure(new Response(null, { status: 401 }), "http://sso.localhost"); expect(redirected.status).toBe(302); expect(redirected.headers.get("location")).toBe( "http://sso.localhost:3000/login?returnTo=%2Fadmin", ); expect(denied.status).toBe(401); expect(denied.headers.has("location")).toBe(false); }); test("forward auth exposes the verifier public origin instead of its internal app port", () => { const redirected = forwardAuthFailure( new Response(null, { status: 302, headers: { location: "/sign-in?returnTo=%2Fadmin" }, }), "http://127.0.0.1:85/api/verify", "http://sso.localhost", ); expect(redirected.headers.get("location")).toBe("http://sso.localhost/sign-in?returnTo=%2Fadmin"); }); test("gateway reads and strips internal app diagnostics", async () => { const response = new Response("safe public error", { status: 500, headers: { "x-wrnexus-internal-error": encodeURIComponent('Error: Unknown workspace app "admin"'), }, }); expect(internalError(response)).toBe('Error: Unknown workspace app "admin"'); const stripped = stripInternalError(response); expect(stripped.status).toBe(500); expect(stripped.headers.has("x-wrnexus-internal-error")).toBe(false); expect(await stripped.text()).toBe("safe public error"); }); test("forward auth describes the original gateway request", () => { const headers = forwardAuthHeaders( new Request("https://admin.example.test/settings?tab=security", { headers: { host: "admin.example.test", cookie: "session=abc", authorization: "Bearer token", }, }), ); expect(headers.get("x-forwarded-host")).toBe("admin.example.test"); expect(headers.get("x-forwarded-proto")).toBe("https"); expect(headers.get("x-original-host")).toBe("admin.example.test"); expect(headers.get("x-original-proto")).toBe("https"); expect(headers.get("x-original-method")).toBe("GET"); expect(headers.get("x-original-uri")).toBe("/settings?tab=security"); expect(headers.get("cookie")).toBe("session=abc"); expect(headers.get("authorization")).toBe("Bearer token"); }); test("forward auth uses the protected app public protocol", () => { const headers = forwardAuthHeaders( new Request("http://127.0.0.1:10050/settings", { headers: { host: "admin.wrnx.in" }, }), "https://admin.wrnx.in", ); expect(headers.get("x-original-host")).toBe("admin.wrnx.in"); expect(headers.get("x-original-proto")).toBe("https"); }); test("nested SSO proxy keeps the protected app's original request headers", () => { const authHeaders = forwardAuthHeaders( new Request("http://admin.localhost:3000/settings", { headers: { host: "admin.localhost:3000" }, }), ); authHeaders.set("host", "sso.localhost:3000"); const verifierRequest = new Request("http://sso.localhost:3000/api/verify", { headers: authHeaders, }); const proxied = gatewayProxyHeaders( verifierRequest, new URL(verifierRequest.url), "127.0.0.1", true, ); expect(proxied.get("x-forwarded-host")).toBe("sso.localhost:3000"); expect(proxied.get("x-original-host")).toBe("admin.localhost:3000"); expect(proxied.get("x-original-uri")).toBe("/settings"); }); test("the gateway proxies browser server functions but refuses private RPC routes", () => { expect(isRpcGatewayPath(RPC_PATH_PREFIX)).toBe(false); expect(isRpcGatewayPath(`${RPC_PATH_PREFIX}/billing/createInvoice`)).toBe(true); expect(isRpcGatewayPath("/api/billing")).toBe(false); expect(isRpcGatewayPath("/__wrnexus/rpcfoo")).toBe(false); }); test("browser RPC proxy preserves CSRF credentials and trusts only the internal hop", () => { const request = new Request(`http://web.localhost:3000${RPC_PATH_PREFIX}`, { method: "POST", headers: { host: "web.localhost:3000", origin: "http://web.localhost:3000", cookie: "wrn-csrf=token", "x-csrf-token": "token", [RPC_INTERNAL_HEADER]: "forged", }, }); const headers = gatewayBrowserRpcHeaders( request, new URL(request.url), "127.0.0.1", true, "http://127.0.0.1:3001", ); expect(headers.get("origin")).toBe("http://127.0.0.1:3001"); expect(headers.get("cookie")).toBe("wrn-csrf=token"); expect(headers.get("x-csrf-token")).toBe("token"); expect(headers.get("x-forwarded-host")).toBe("web.localhost:3000"); expect(headers.has(RPC_INTERNAL_HEADER)).toBe(false); expect(headers.has("host")).toBe(false); }); test("an inbound internal-marker header from outside is stripped regardless of casing", () => { for (const name of [ RPC_INTERNAL_HEADER, RPC_INTERNAL_HEADER.toUpperCase(), "X-WrNexus-Internal", ]) { const request = new Request("http://localhost:3000/path", { headers: { [name]: "1" }, }); const headers = stripUntrustedInternalHeaders( gatewayProxyHeaders(request, new URL(request.url), "127.0.0.1", true), ); expect(headers.has(RPC_INTERNAL_HEADER)).toBe(false); } }); test("gateway respawns development apps after an HMR restart exit", () => { expect(gatewayRestartDelay("development", 97, null)).toBe(0); expect(gatewayRestartDelay("development", 1, null)).toBe(1200); expect(gatewayRestartDelay("development", 0, null)).toBeNull(); expect(gatewayRestartDelay("development", 97, "SIGTERM")).toBeNull(); expect(gatewayRestartDelay("production", 97, null)).toBeNull(); });