Files
OmniRoute/tests/unit/codex-oauth-remote-host-hint-7523.test.ts
Diego Rodrigues de Sa e Souza 197f726c62 fix(oauth): surface tunnel hint when Codex OAuth runs on a remote host (#7523) (#7527)
The PKCE callback server binds the SERVER's loopback (localhost:PORT). When
the operator drives the OAuth flow from a different machine (OmniRoute on a
remote host/VPS), the provider redirects the browser to the operator's OWN
localhost:PORT — the confirmation screen hangs forever with no explanation.

start-callback-server now inspects the request Host: on a non-loopback host it
returns { remoteHost, tunnelCommand, message } so the UI can show the
'ssh -L PORT:127.0.0.1:PORT' instruction (or steer to the paste/import flow)
instead of a silent hang. Loopback access is unaffected. The Host header is
spoofable, so this drives only a UI hint — never an auth decision.

Logic extracted to remoteOAuthHint.ts (keeps the god-route under its size
budget and makes it unit-testable). TDD: 4 tests covering loopback (no hint),
null host (fail-open), and remote host (correct tunnel command for both the
fixed 1455 and OS-assigned ports).

Closes #7523
2026-07-17 02:39:30 -03:00

43 lines
2.1 KiB
TypeScript

// Regression test for #7523: the Codex (and Windsurf/Devin) PKCE OAuth callback
// server binds the SERVER's loopback (localhost:PORT). When OmniRoute runs on a
// remote host (e.g. the VPS) and the operator drives the browser from a different
// machine, the provider redirects to the operator's OWN localhost:PORT — the
// login confirmation screen hangs forever with no explanation.
//
// buildRemoteOAuthHint() detects a non-loopback Host and surfaces the
// reverse-tunnel instruction so the start-callback-server response carries it
// (the UI shows it instead of a silent hang). Loopback access is unaffected.
import test from "node:test";
import assert from "node:assert/strict";
import { buildRemoteOAuthHint } from "../../src/app/api/oauth/[provider]/[action]/remoteOAuthHint.ts";
test("loopback Host → no remote hint (local access is unaffected)", () => {
for (const host of ["localhost", "localhost:20128", "127.0.0.1:20128", "[::1]:20128", "::1"]) {
const hint = buildRemoteOAuthHint(host, 1455);
assert.equal(hint.remoteHost, false, `expected no hint for loopback host ${host}`);
}
});
test("null Host → no remote hint (fail-open: never block a local flow on a missing header)", () => {
const hint = buildRemoteOAuthHint(null, 1455);
assert.equal(hint.remoteHost, false);
});
test("remote Host → returns the reverse-tunnel hint with the exact callback port", () => {
const hint = buildRemoteOAuthHint("192.168.0.15:20128", 1455);
assert.equal(hint.remoteHost, true);
assert.ok(hint.remoteHost === true); // narrow the union
// The tunnel must forward the SAME port the callback server bound, both sides.
assert.equal(hint.tunnelCommand, "ssh -L 1455:127.0.0.1:1455 <user>@<omniroute-host>");
assert.match(hint.message, /remote host \(192\.168\.0\.15:20128\)/);
assert.match(hint.message, /hang/i);
});
test("remote Host honours a random callback port (Windsurf/Devin OS-assigned port)", () => {
const hint = buildRemoteOAuthHint("omniroute.example.com", 54321);
assert.ok(hint.remoteHost === true);
assert.equal(hint.tunnelCommand, "ssh -L 54321:127.0.0.1:54321 <user>@<omniroute-host>");
});