Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion scripts/generate-openclaw-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 39 additions & 0 deletions test/generate-openclaw-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,52 @@ 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", () => {
const config = runConfigScript({ CHAT_UI_URL: "http://127.0.0.1:18789" });
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 });
Expand Down
Loading