mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 13:00:48 +00:00
fix(ui): restore control-ui query token compatibility (#43979)
* fix(ui): restore control-ui query token imports * chore(changelog): add entry for openclaw#43979 thanks @stim64045-spec --------- Co-authored-by: 大禹 <dayu@dayudeMac-mini.local> Co-authored-by: Val Alexander <bunsthedev@gmail.com> Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
This commit is contained in:
@@ -296,6 +296,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
||||
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
|
||||
|
||||
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Security
|
||||
|
||||
@@ -242,7 +242,7 @@ http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-toke
|
||||
Notes:
|
||||
|
||||
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
|
||||
- `token` is imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; it is not stored in localStorage.
|
||||
- `token` is preferably imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; legacy `?token=` query params are also imported once for compatibility and then removed.
|
||||
- `password` is kept in memory only.
|
||||
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
|
||||
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.
|
||||
|
||||
@@ -89,6 +89,49 @@ function createStorageMock(): Storage {
|
||||
};
|
||||
}
|
||||
|
||||
function setTestWindowUrl(urlString: string) {
|
||||
const current = new URL(urlString);
|
||||
const history = {
|
||||
replaceState: vi.fn((_state: unknown, _title: string, nextUrl: string | URL) => {
|
||||
const next = new URL(String(nextUrl), current.toString());
|
||||
current.href = next.toString();
|
||||
current.protocol = next.protocol;
|
||||
current.host = next.host;
|
||||
current.pathname = next.pathname;
|
||||
current.search = next.search;
|
||||
current.hash = next.hash;
|
||||
}),
|
||||
};
|
||||
const locationLike = {
|
||||
get href() {
|
||||
return current.toString();
|
||||
},
|
||||
get protocol() {
|
||||
return current.protocol;
|
||||
},
|
||||
get host() {
|
||||
return current.host;
|
||||
},
|
||||
get pathname() {
|
||||
return current.pathname;
|
||||
},
|
||||
get search() {
|
||||
return current.search;
|
||||
},
|
||||
get hash() {
|
||||
return current.hash;
|
||||
},
|
||||
};
|
||||
vi.stubGlobal("window", {
|
||||
location: locationLike,
|
||||
history,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
} as unknown as Window & typeof globalThis);
|
||||
vi.stubGlobal("location", locationLike as Location);
|
||||
return { history, location: locationLike };
|
||||
}
|
||||
|
||||
const createHost = (tab: Tab): SettingsHost => ({
|
||||
settings: {
|
||||
gatewayUrl: "",
|
||||
@@ -233,15 +276,44 @@ describe("setTabFromRoute", () => {
|
||||
describe("applySettingsFromUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("localStorage", createStorageMock());
|
||||
vi.stubGlobal("sessionStorage", createStorageMock());
|
||||
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
||||
setTestWindowUrl("https://control.example/ui/overview");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
window.history.replaceState({}, "", "/chat");
|
||||
});
|
||||
|
||||
it("hydrates query token params and strips them from the URL", () => {
|
||||
setTestWindowUrl("https://control.example/ui/overview?token=abc123");
|
||||
const host = createHost("overview");
|
||||
host.settings.gatewayUrl = "wss://control.example/openclaw";
|
||||
|
||||
applySettingsFromUrl(host);
|
||||
|
||||
expect(host.settings.token).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
|
||||
it("keeps query token params pending when a gatewayUrl confirmation is required", () => {
|
||||
setTestWindowUrl(
|
||||
"https://control.example/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123",
|
||||
);
|
||||
const host = createHost("overview");
|
||||
host.settings.gatewayUrl = "wss://control.example/openclaw";
|
||||
|
||||
applySettingsFromUrl(host);
|
||||
|
||||
expect(host.settings.token).toBe("");
|
||||
expect(host.pendingGatewayUrl).toBe("wss://other-gateway.example/openclaw");
|
||||
expect(host.pendingGatewayToken).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
|
||||
it("resets stale persisted session selection to main when a token is supplied without a session", () => {
|
||||
setTestWindowUrl("https://control.example/chat#token=test-token");
|
||||
const host = createHost("chat");
|
||||
host.settings = {
|
||||
...host.settings,
|
||||
@@ -252,8 +324,6 @@ describe("applySettingsFromUrl", () => {
|
||||
};
|
||||
host.sessionKey = "agent:test_old:main";
|
||||
|
||||
window.history.replaceState({}, "", "/chat#token=test-token");
|
||||
|
||||
applySettingsFromUrl(host);
|
||||
|
||||
expect(host.sessionKey).toBe("main");
|
||||
@@ -262,6 +332,9 @@ describe("applySettingsFromUrl", () => {
|
||||
});
|
||||
|
||||
it("preserves an explicit session from the URL when token and session are both supplied", () => {
|
||||
setTestWindowUrl(
|
||||
"https://control.example/chat?session=agent%3Atest_new%3Amain#token=test-token",
|
||||
);
|
||||
const host = createHost("chat");
|
||||
host.settings = {
|
||||
...host.settings,
|
||||
@@ -272,8 +345,6 @@ describe("applySettingsFromUrl", () => {
|
||||
};
|
||||
host.sessionKey = "agent:test_old:main";
|
||||
|
||||
window.history.replaceState({}, "", "/chat?session=agent%3Atest_new%3Amain#token=test-token");
|
||||
|
||||
applySettingsFromUrl(host);
|
||||
|
||||
expect(host.sessionKey).toBe("agent:test_new:main");
|
||||
@@ -282,6 +353,9 @@ describe("applySettingsFromUrl", () => {
|
||||
});
|
||||
|
||||
it("does not reset the current gateway session when a different gateway is pending confirmation", () => {
|
||||
setTestWindowUrl(
|
||||
"https://control.example/chat?gatewayUrl=ws%3A%2F%2Fgateway-b.example%3A18789#token=test-token",
|
||||
);
|
||||
const host = createHost("chat");
|
||||
host.settings = {
|
||||
...host.settings,
|
||||
@@ -292,12 +366,6 @@ describe("applySettingsFromUrl", () => {
|
||||
};
|
||||
host.sessionKey = "agent:test_old:main";
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
"/chat?gatewayUrl=ws%3A%2F%2Fgateway-b.example%3A18789#token=test-token",
|
||||
);
|
||||
|
||||
applySettingsFromUrl(host);
|
||||
|
||||
expect(host.sessionKey).toBe("agent:test_old:main");
|
||||
|
||||
@@ -97,7 +97,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl");
|
||||
const nextGatewayUrl = gatewayUrlRaw?.trim() ?? "";
|
||||
const gatewayUrlChanged = Boolean(nextGatewayUrl && nextGatewayUrl !== host.settings.gatewayUrl);
|
||||
const tokenRaw = hashParams.get("token");
|
||||
const tokenRaw = hashParams.get("token") ?? params.get("token");
|
||||
const passwordRaw = params.get("password") ?? hashParams.get("password");
|
||||
const sessionRaw = params.get("session") ?? hashParams.get("session");
|
||||
const shouldResetSessionForToken = Boolean(
|
||||
|
||||
@@ -315,11 +315,11 @@ describe("control UI routing", () => {
|
||||
expect(container.scrollTop).toBe(maxScroll);
|
||||
});
|
||||
|
||||
it("strips query token params without importing them", async () => {
|
||||
it("hydrates token from query params and strips them", async () => {
|
||||
const app = mountApp("/ui/overview?token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
|
||||
undefined,
|
||||
);
|
||||
@@ -405,6 +405,28 @@ describe("control UI routing", () => {
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
it("keeps a query token pending until the gateway URL change is confirmed", async () => {
|
||||
const app = mountApp(
|
||||
"/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123",
|
||||
);
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.gatewayUrl).not.toBe("wss://other-gateway.example/openclaw");
|
||||
expect(app.settings.token).toBe("");
|
||||
|
||||
const confirmButton = Array.from(app.querySelectorAll<HTMLButtonElement>("button")).find(
|
||||
(button) => button.textContent?.trim() === "Confirm",
|
||||
);
|
||||
expect(confirmButton).not.toBeUndefined();
|
||||
confirmButton?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.gatewayUrl).toBe("wss://other-gateway.example/openclaw");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
it("restores the token after a same-tab refresh", async () => {
|
||||
const first = mountApp("/ui/overview#token=abc123");
|
||||
await first.updateComplete;
|
||||
|
||||
Reference in New Issue
Block a user