diff --git a/CHANGELOG.md b/CHANGELOG.md index e72e9b7ef..2a698897d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.clawd.bot - Models: default missing custom provider fields so minimal configs are accepted. - Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671) - Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b. +- Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690) - macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman. - Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634) - Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy. diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index 95f61bef7..b97a2d321 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -230,6 +230,21 @@ describe("gateway server auth/connect", () => { ws.close(); }); + test("returns control ui hint when token is missing", async () => { + const ws = await openWs(port); + const res = await connectReq(ws, { + client: { + id: GATEWAY_CLIENT_NAMES.CONTROL_UI, + version: "1.0.0", + platform: "web", + mode: GATEWAY_CLIENT_MODES.WEBCHAT, + }, + }); + expect(res.ok).toBe(false); + expect(res.error?.message ?? "").toContain("Control UI settings"); + ws.close(); + }); + test("rejects control ui without device identity by default", async () => { const ws = await openWs(port); const res = await connectReq(ws, { diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 1d03dbcbf..5bca2e5ed 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -66,19 +66,34 @@ function formatGatewayAuthFailureMessage(params: { authMode: ResolvedGatewayAuth["mode"]; authProvided: AuthProvidedKind; reason?: string; + client?: { id?: string | null; mode?: string | null }; }): string { - const { authMode, authProvided, reason } = params; + const { authMode, authProvided, reason, client } = params; + const isCli = isGatewayCliClient(client); + const isControlUi = client?.id === GATEWAY_CLIENT_IDS.CONTROL_UI; + const isWebchat = isWebchatClient(client); + const uiHint = "open a tokenized dashboard URL or paste token in Control UI settings"; + const tokenHint = isCli + ? "set gateway.remote.token to match gateway.auth.token" + : isControlUi || isWebchat + ? uiHint + : "provide gateway auth token"; + const passwordHint = isCli + ? "set gateway.remote.password to match gateway.auth.password" + : isControlUi || isWebchat + ? "enter the password in Control UI settings" + : "provide gateway auth password"; switch (reason) { case "token_missing": - return "unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)"; + return `unauthorized: gateway token missing (${tokenHint})`; case "token_mismatch": - return "unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)"; + return `unauthorized: gateway token mismatch (${tokenHint})`; case "token_missing_config": return "unauthorized: gateway token not configured on gateway (set gateway.auth.token)"; case "password_missing": - return "unauthorized: gateway password missing (set gateway.remote.password to match gateway.auth.password)"; + return `unauthorized: gateway password missing (${passwordHint})`; case "password_mismatch": - return "unauthorized: gateway password mismatch (set gateway.remote.password to match gateway.auth.password)"; + return `unauthorized: gateway password mismatch (${passwordHint})`; case "password_missing_config": return "unauthorized: gateway password not configured on gateway (set gateway.auth.password)"; case "tailscale_user_missing": @@ -90,10 +105,10 @@ function formatGatewayAuthFailureMessage(params: { } if (authMode === "token" && authProvided === "none") { - return "unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)"; + return `unauthorized: gateway token missing (${tokenHint})`; } if (authMode === "password" && authProvided === "none") { - return "unauthorized: gateway password missing (set gateway.remote.password to match gateway.auth.password)"; + return `unauthorized: gateway password missing (${passwordHint})`; } return "unauthorized"; } @@ -532,6 +547,7 @@ export function attachGatewayWsMessageHandler(params: { authMode: resolvedAuth.mode, authProvided, reason: authResult.reason, + client: connectParams.client, }); setCloseCause("unauthorized", { authMode: resolvedAuth.mode,