diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index d38f4eb8ab9..40c821ced9f 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -194,7 +194,23 @@ def _placeholder(channel: str, env_key: str) -> str: if parsed.scheme and parsed.netloc else "http://127.0.0.1:18789" ) - origins = list(dict.fromkeys(["http://127.0.0.1:18789", chat_origin])) + # When onboard injects an internal port (e.g. :18789) into a URL that the + # user provided without an explicit port, the browser origin from a reverse + # proxy (Brev Cloudflare Tunnel, nginx, Caddy, etc.) will not carry that + # port. Include the portless origin so both direct and proxied access work. + # Skip for loopback — no reverse proxy in front of localhost. + try: + _has_explicit_port = parsed.port is not None + except ValueError: + _has_explicit_port = False + if parsed.scheme and parsed.hostname and _has_explicit_port and not is_loopback(parsed.hostname): + host_part = f"[{parsed.hostname}]" if ":" in parsed.hostname else parsed.hostname + portless_origin = f"{parsed.scheme}://{host_part}" + else: + portless_origin = None + origins = list(dict.fromkeys( + filter(None, ["http://127.0.0.1:18789", chat_origin, portless_origin]) + )) # Auto-disable device auth when CHAT_UI_URL is non-loopback — terminal-based # pairing is impossible when the user only has web access (Brev Launchable, diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index b54e8acf594..b11e291524d 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -105,6 +105,9 @@ describe("generate-openclaw-config.py: config generation", () => { expect(config.gateway.controlUi.allowedOrigins).toContain( "https://nemoclaw0-xxx.brevlab.com:18789", ); + expect(config.gateway.controlUi.allowedOrigins).toContain( + "https://nemoclaw0-xxx.brevlab.com", + ); }); it("includes only loopback origin for loopback URL", () => { @@ -112,6 +115,42 @@ describe("generate-openclaw-config.py: config generation", () => { expect(config.gateway.controlUi.allowedOrigins).toEqual(["http://127.0.0.1:18789"]); }); + it("includes portless origin for reverse-proxy access (Fixes #3000)", () => { + const config = runConfigScript({ + CHAT_UI_URL: "https://nemoclaw0-abc123.brevlab.com:18789", + }); + const origins = config.gateway.controlUi.allowedOrigins; + expect(origins).toContain("https://nemoclaw0-abc123.brevlab.com:18789"); + expect(origins).toContain("https://nemoclaw0-abc123.brevlab.com"); + }); + + it("preserves brackets in portless origin for public IPv6 addresses", () => { + const config = runConfigScript({ + CHAT_UI_URL: "https://[2606:4700::1]:18789", + }); + const origins = config.gateway.controlUi.allowedOrigins; + expect(origins).toContain("https://[2606:4700::1]:18789"); + expect(origins).toContain("https://[2606:4700::1]"); + }); + + it("does not add portless origin for IPv6 loopback", () => { + const config = runConfigScript({ + CHAT_UI_URL: "http://[::1]:18789", + }); + const origins = config.gateway.controlUi.allowedOrigins; + expect(origins).toContain("http://[::1]:18789"); + expect(origins).not.toContain("http://[::1]"); + }); + + it("does not crash on malformed port in CHAT_UI_URL", () => { + const config = runConfigScript({ + CHAT_UI_URL: "https://example.com:abc", + }); + const origins = config.gateway.controlUi.allowedOrigins; + expect(origins).toContain("http://127.0.0.1:18789"); + expect(origins).not.toContain("https://example.com"); + }); + it("parses messaging channels from base64", () => { const channels = Buffer.from(JSON.stringify(["telegram"])).toString("base64"); const config = runConfigScript({ NEMOCLAW_MESSAGING_CHANNELS_B64: channels });