diff --git a/ui/src/ui/storage.node.test.ts b/ui/src/ui/storage.node.test.ts index 2155758fe97..f2a4ced6857 100644 --- a/ui/src/ui/storage.node.test.ts +++ b/ui/src/ui/storage.node.test.ts @@ -226,6 +226,132 @@ describe("loadSettings default gateway URL derivation", () => { }); }); + it("reuses a session token across loopback host aliases", async () => { + setTestLocation({ + protocol: "http:", + host: "127.0.0.1:18789", + pathname: "/", + }); + + const { loadSettings, saveSettings } = await import("./storage.ts"); + saveSettings({ + gatewayUrl: "ws://127.0.0.1:18789", + token: "loopback-token", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }); + + localStorage.setItem( + "openclaw.control.settings.v1", + JSON.stringify({ + gatewayUrl: "ws://localhost:18789", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }), + ); + + expect(loadSettings()).toMatchObject({ + gatewayUrl: "ws://localhost:18789", + token: "loopback-token", + }); + }); + + it("reuses a session token across IPv6 and IPv4 loopback aliases", async () => { + setTestLocation({ + protocol: "http:", + host: "[::1]:18789", + pathname: "/", + }); + + const { loadSettings, saveSettings } = await import("./storage.ts"); + saveSettings({ + gatewayUrl: "ws://[::1]:18789", + token: "loopback-token", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }); + + localStorage.setItem( + "openclaw.control.settings.v1", + JSON.stringify({ + gatewayUrl: "ws://127.0.0.1:18789", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }), + ); + + expect(loadSettings()).toMatchObject({ + gatewayUrl: "ws://127.0.0.1:18789", + token: "loopback-token", + }); + }); + + it("does not reuse a loopback token across different ports", async () => { + setTestLocation({ + protocol: "http:", + host: "127.0.0.1:18789", + pathname: "/", + }); + + const { loadSettings, saveSettings } = await import("./storage.ts"); + saveSettings({ + gatewayUrl: "ws://127.0.0.1:18789", + token: "loopback-token", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }); + + localStorage.setItem( + "openclaw.control.settings.v1", + JSON.stringify({ + gatewayUrl: "ws://localhost:19999", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "system", + chatFocusMode: false, + chatShowThinking: true, + splitRatio: 0.6, + navCollapsed: false, + navGroupsCollapsed: {}, + }), + ); + + expect(loadSettings()).toMatchObject({ + gatewayUrl: "ws://localhost:19999", + token: "", + }); + }); + it("does not persist gateway tokens when saving settings", async () => { setTestLocation({ protocol: "https:", diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 20997b6fec3..d173bda1b46 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -93,9 +93,15 @@ function normalizeGatewayTokenScope(gatewayUrl: string): string { ? `${location.protocol}//${location.host}${location.pathname || "/"}` : undefined; const parsed = base ? new URL(trimmed, base) : new URL(trimmed); + const host = parsed.hostname.trim().toLowerCase(); + const isLoopbackHost = + host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.startsWith("127."); const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "") || parsed.pathname; - return `${parsed.protocol}//${parsed.host}${pathname}`; + const hostPort = isLoopbackHost + ? `127.0.0.1${parsed.port ? `:${parsed.port}` : ""}` + : parsed.host; + return `${parsed.protocol}//${hostPort}${pathname}`; } catch { return trimmed; }