diff --git a/electron/api/routes/gateway.ts b/electron/api/routes/gateway.ts index 530e030d8..4f00dbcec 100644 --- a/electron/api/routes/gateway.ts +++ b/electron/api/routes/gateway.ts @@ -1,5 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { PORTS } from '../../utils/config'; +import { buildOpenClawControlUiUrl } from '../../utils/openclaw-control-ui'; import { getSetting } from '../../utils/store'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; @@ -68,7 +69,7 @@ export async function handleGatewayRoutes( const status = ctx.gatewayManager.getStatus(); const token = await getSetting('gatewayToken'); const port = status.port || PORTS.OPENCLAW_GATEWAY; - const urlValue = `http://127.0.0.1:${port}/?token=${encodeURIComponent(token)}`; + const urlValue = buildOpenClawControlUiUrl(port, token); sendJson(res, 200, { success: true, url: urlValue, token, port }); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index dc855c583..49664a049 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -19,6 +19,7 @@ import { saveProviderKeyToOpenClaw, removeProviderFromOpenClaw, } from '../utils/openclaw-auth'; +import { buildOpenClawControlUiUrl } from '../utils/openclaw-control-ui'; import { logger } from '../utils/logger'; import { saveChannelConfig, @@ -1283,8 +1284,7 @@ function registerGatewayHandlers( const status = gatewayManager.getStatus(); const token = await getSetting('gatewayToken'); const port = status.port || 18789; - // Pass token as query param - Control UI will store it in localStorage - const url = `http://127.0.0.1:${port}/?token=${encodeURIComponent(token)}`; + const url = buildOpenClawControlUiUrl(port, token); return { success: true, url, port, token }; } catch (error) { return { success: false, error: String(error) }; diff --git a/electron/utils/openclaw-control-ui.ts b/electron/utils/openclaw-control-ui.ts new file mode 100644 index 000000000..11cffdbf8 --- /dev/null +++ b/electron/utils/openclaw-control-ui.ts @@ -0,0 +1,17 @@ +/** + * Build the external OpenClaw Control UI URL. + * + * OpenClaw 2026.3.13 imports one-time auth tokens from the URL fragment + * (`#token=...`) and strips them after load. Query-string tokens are removed + * by the UI bootstrap but are not imported for auth. + */ +export function buildOpenClawControlUiUrl(port: number, token: string): string { + const url = new URL(`http://127.0.0.1:${port}/`); + const trimmedToken = token.trim(); + + if (trimmedToken) { + url.hash = new URLSearchParams({ token: trimmedToken }).toString(); + } + + return url.toString(); +} diff --git a/package.json b/package.json index 35cf2c1ce..a9e0333eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawx", - "version": "0.2.3-beta.2", + "version": "0.2.3-beta.3", "pnpm": { "onlyBuiltDependencies": [ "@discordjs/opus", @@ -120,4 +120,4 @@ "zx": "^8.8.5" }, "packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268" -} \ No newline at end of file +} diff --git a/tests/unit/openclaw-control-ui.test.ts b/tests/unit/openclaw-control-ui.test.ts new file mode 100644 index 000000000..2a641ca78 --- /dev/null +++ b/tests/unit/openclaw-control-ui.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { buildOpenClawControlUiUrl } from '@electron/utils/openclaw-control-ui'; + +describe('buildOpenClawControlUiUrl', () => { + it('uses the URL fragment for one-time token bootstrap', () => { + expect(buildOpenClawControlUiUrl(18789, 'clawx-test-token')).toBe( + 'http://127.0.0.1:18789/#token=clawx-test-token', + ); + }); + + it('omits the fragment when the token is blank', () => { + expect(buildOpenClawControlUiUrl(18789, ' ')).toBe('http://127.0.0.1:18789/'); + }); +});