Skip to content

Commit 59950f7

Browse files
fix(ui): preserve gateway token during safe websocket url edits (#73923)
* fix(ui): preserve gateway token during safe websocket url edits * fix(ui): preserve gateway token during safe websocket url edits --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
1 parent ccf83ac commit 59950f7

5 files changed

Lines changed: 111 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
2929
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.
3030
- Memory, state, diagnostics, and config: split header-too-large embedding batches, keep QMD memory search enabled in transient mode, avoid SQLite WAL on NFS volumes, preserve recovery scheduling outside stuck-session warning backoff, and keep shell environment fallbacks contained in config write tests. (#92650, #92618, #92639, #91247, #92752) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, and @gnanam1990.
3131
- UI/mobile/TUI: preserve dashboard session parent lineage, WebChat backscroll, reset soft command args, sidebar session picker interactivity, collapsed workspace files, resolved `/model` confirmation refs, and stale foreground iOS Gateway reconnects. (#90658, #92622, #91353, #92705, #92779, #92773, #92552) Thanks @luoyanglang, @TurboTheTurtle, @zhouhe-xydt, @NianJiuZst, @shakkernerd, @NarahariRaghava, and @Solvely-Colin.
32+
- Control UI: preserve Gateway Access tokens during same-normalized WebSocket URL edits and reload gateway-scoped tokens when switching endpoints. Fixes #41545; repairs #42001 with additional source PRs #41546, #41552, and #41718. Thanks @wsyjh8, @llagy0020, @llagy007, @pingfanfan, and @zheliu2.
3233
- Release and test reliability: extend slow Gateway/full-suite watchdogs, split local full-suite shards when throttled, stabilize plugin auth marker fixtures, avoid brittle provider-ref error text, and keep QA Lab bootstrap selection assertions aligned with flow-only scenarios. (#92652)
3334
- macOS Peekaboo bridge: update the embedded Peekaboo package to 3.5.2 and route bundled-skill CLI commands through the OpenClaw app bridge so they inherit its Screen Recording and Accessibility grants.
3435
- Agent routing: route subagent RPC callbacks addressed to an agent-shaped `--to` target to the correct session key instead of falling back to the main session, so WeChat (and other channel) session-key callbacks reach the intended subagent session. (#90231) Thanks @zhangguiping-xydt.

ui/src/ui/navigation.browser.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ describe("control UI routing", () => {
774774
expect(thread.scrollTop).toBe(targetScrollTop);
775775
});
776776

777-
it("hydrates hash tokens, restores same-tab refreshes, and clears after gateway changes", async () => {
777+
it("hydrates hash tokens, preserves same-scope URL edits, and reloads after gateway changes", async () => {
778778
const app = mountApp("/ui/overview#token=abc123");
779779
await app.updateComplete;
780780

@@ -799,12 +799,32 @@ describe("control UI routing", () => {
799799
'input[placeholder="ws://100.x.y.z:18789"]',
800800
HTMLInputElement,
801801
);
802+
803+
const sameScopeUrl = `${refreshed.settings.gatewayUrl}/`;
804+
gatewayUrlInput.value = sameScopeUrl;
805+
gatewayUrlInput.dispatchEvent(new Event("input", { bubbles: true }));
806+
await refreshed.updateComplete;
807+
808+
expect(refreshed.settings.gatewayUrl).toBe(sameScopeUrl);
809+
expect(refreshed.settings.token).toBe("abc123");
810+
811+
gatewayUrlInput.value = "wss://missing-token.example/openclaw";
812+
gatewayUrlInput.dispatchEvent(new Event("input", { bubbles: true }));
813+
await refreshed.updateComplete;
814+
815+
expect(refreshed.settings.gatewayUrl).toBe("wss://missing-token.example/openclaw");
816+
expect(refreshed.settings.token).toBe("");
817+
818+
sessionStorage.setItem(
819+
"openclaw.control.token.v1:wss://other-gateway.example/openclaw",
820+
"other-token",
821+
);
802822
gatewayUrlInput.value = "wss://other-gateway.example/openclaw";
803823
gatewayUrlInput.dispatchEvent(new Event("input", { bubbles: true }));
804824
await refreshed.updateComplete;
805825

806826
expect(refreshed.settings.gatewayUrl).toBe("wss://other-gateway.example/openclaw");
807-
expect(refreshed.settings.token).toBe("");
827+
expect(refreshed.settings.token).toBe("other-token");
808828
});
809829

810830
it("keeps a hash token pending until the gateway URL change is confirmed", async () => {

ui/src/ui/storage.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ function loadSessionToken(gatewayUrl: string): string {
199199
}
200200
}
201201

202+
export function resolveGatewayTokenForUrlEdit(
203+
currentGatewayUrl: string,
204+
nextGatewayUrl: string,
205+
currentToken: string,
206+
): string {
207+
if (
208+
normalizeGatewayTokenScope(currentGatewayUrl) === normalizeGatewayTokenScope(nextGatewayUrl)
209+
) {
210+
return currentToken;
211+
}
212+
// Gateway tokens stay session-scoped across endpoint edits.
213+
// Durable settings may contain scrubbed legacy tokens, but must not restore them here.
214+
return loadSessionToken(nextGatewayUrl);
215+
}
216+
202217
function persistSessionToken(gatewayUrl: string, token: string) {
203218
try {
204219
const storage = getSessionStorage();

ui/src/ui/views/overview.node.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,79 @@
11
// @vitest-environment node
2-
import { describe, expect, it } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33
import { ConnectErrorDetailCodes } from "../../../../packages/gateway-protocol/src/connect-error-details.js";
4+
import { createStorageMock } from "../../test-helpers/storage.ts";
5+
import { resolveGatewayTokenForUrlEdit } from "../storage.ts";
46
import {
57
resolveAuthHintKind,
68
resolvePairingHint,
79
shouldShowInsecureContextHint,
810
shouldShowPairingHint,
911
} from "./overview-hints.ts";
1012

13+
afterEach(() => {
14+
vi.unstubAllGlobals();
15+
});
16+
17+
describe("resolveGatewayTokenForUrlEdit", () => {
18+
it("preserves the current token for same normalized gateway endpoint edits", () => {
19+
expect(
20+
resolveGatewayTokenForUrlEdit(
21+
"wss://gateway.example/openclaw",
22+
" wss://gateway.example/openclaw/ ",
23+
"abc123",
24+
),
25+
).toBe("abc123");
26+
});
27+
28+
it("loads a scoped token when the normalized gateway endpoint changes", () => {
29+
vi.stubGlobal("sessionStorage", createStorageMock());
30+
sessionStorage.setItem(
31+
"openclaw.control.token.v1:wss://other-gateway.example/openclaw",
32+
"other-token",
33+
);
34+
35+
expect(
36+
resolveGatewayTokenForUrlEdit(
37+
"wss://gateway.example/openclaw",
38+
"wss://other-gateway.example/openclaw/",
39+
"abc123",
40+
),
41+
).toBe("other-token");
42+
});
43+
44+
it("clears the token when the changed gateway endpoint has no scoped token", () => {
45+
vi.stubGlobal("sessionStorage", createStorageMock());
46+
47+
expect(
48+
resolveGatewayTokenForUrlEdit(
49+
"wss://gateway.example/openclaw",
50+
"wss://other-gateway.example/openclaw",
51+
"abc123",
52+
),
53+
).toBe("");
54+
});
55+
56+
it("does not restore legacy durable tokens when the gateway endpoint changes", () => {
57+
vi.stubGlobal("localStorage", createStorageMock());
58+
vi.stubGlobal("sessionStorage", createStorageMock());
59+
localStorage.setItem(
60+
"openclaw.control.settings.v1",
61+
JSON.stringify({
62+
gatewayUrl: "wss://other-gateway.example/openclaw",
63+
token: "legacy-durable-token",
64+
}),
65+
);
66+
67+
expect(
68+
resolveGatewayTokenForUrlEdit(
69+
"wss://gateway.example/openclaw",
70+
"wss://other-gateway.example/openclaw",
71+
"abc123",
72+
),
73+
).toBe("");
74+
});
75+
});
76+
1177
describe("shouldShowPairingHint", () => {
1278
it("returns true for 'pairing required' close reason", () => {
1379
expect(shouldShowPairingHint(false, "disconnected (1008): pairing required")).toBe(true);

ui/src/ui/views/overview.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts"
66
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
77
import type { GatewayHelloOk } from "../gateway.ts";
88
import { icons } from "../icons.ts";
9-
import type { UiSettings } from "../storage.ts";
9+
import { resolveGatewayTokenForUrlEdit, type UiSettings } from "../storage.ts";
1010
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
1111
import type {
1212
AttentionItem,
@@ -269,7 +269,11 @@ export function renderOverview(props: OverviewProps) {
269269
props.onSettingsChange({
270270
...props.settings,
271271
gatewayUrl: v,
272-
token: v.trim() === props.settings.gatewayUrl.trim() ? props.settings.token : "",
272+
token: resolveGatewayTokenForUrlEdit(
273+
props.settings.gatewayUrl,
274+
v,
275+
props.settings.token,
276+
),
273277
});
274278
}}
275279
placeholder="ws://100.x.y.z:18789"

0 commit comments

Comments
 (0)