From da4f519745edbb705c20c01dc152ac2f10a9cb49 Mon Sep 17 00:00:00 2001 From: gsknnft Date: Sat, 18 Apr 2026 20:47:45 -0400 Subject: [PATCH] fix(security): preserve stored token through empty-token UI state; handle non-JSON health responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GatewayClient.ts — autosave effects no longer overwrite persisted gateway tokens with empty strings. When the in-memory token is empty (proxy handles auth server-side), the patch omits the token field (undefined) so mergeGatewaySettings/mergeGatewayProfiles treats it as "leave unchanged". adapterProfiles updater also preserves the existing stored token when the new token is empty, preventing floor-switch from erasing tokens. runtime/custom/http.ts — requestCustomRuntime() checks the response Content-Type before calling response.json(). Non-JSON responses (e.g. plain-text /health "OK") are returned as-is instead of throwing a JSON parse error, making the custom/local/claw3d health probe path reliable for runtimes that return plain text. Authored By: GSKNNFT --- src/lib/gateway/GatewayClient.ts | 17 +++++++++++------ src/lib/runtime/custom/http.ts | 5 +++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/gateway/GatewayClient.ts b/src/lib/gateway/GatewayClient.ts index 0709a7f..f241a9c 100644 --- a/src/lib/gateway/GatewayClient.ts +++ b/src/lib/gateway/GatewayClient.ts @@ -1048,11 +1048,12 @@ export const useGatewayConnection = ( useEffect(() => { if (!settingsLoaded) return; setAdapterProfiles((current) => { - const nextProfile = { - url: gatewayUrl.trim(), - token, - }; const existing = current[selectedAdapterType]; + // Never overwrite a stored token with an empty string — the Studio proxy + // injects the real token server-side; an empty token in UI state means + // "let the proxy handle it", not "clear the saved token". + const nextToken = token || existing?.token || ""; + const nextProfile = { url: gatewayUrl.trim(), token: nextToken }; if ( existing && existing.url === nextProfile.url && @@ -1072,11 +1073,15 @@ export const useGatewayConnection = ( const baseline = loadedGatewaySettings.current; if (!baseline) return; const nextGatewayUrl = gatewayUrl.trim(); + // Use undefined for the token in the patch when the in-memory token is empty so + // that mergeGatewaySettings / mergeGatewayProfiles treats it as "leave unchanged" + // rather than overwriting the persisted token with an empty string. + const persistToken = token || undefined; const nextProfiles = { ...adapterProfiles, [selectedAdapterType]: { url: nextGatewayUrl, - token, + token: persistToken, }, }; if ( @@ -1091,7 +1096,7 @@ export const useGatewayConnection = ( { gateway: { url: nextGatewayUrl, - token, + token: persistToken, adapterType: selectedAdapterType, profiles: nextProfiles, }, diff --git a/src/lib/runtime/custom/http.ts b/src/lib/runtime/custom/http.ts index 1969687..b6d40fa 100644 --- a/src/lib/runtime/custom/http.ts +++ b/src/lib/runtime/custom/http.ts @@ -54,6 +54,11 @@ export async function requestCustomRuntime({ text.trim() || `Custom runtime request failed (${response.status}) for ${pathname}.` ); } + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("application/json")) { + // Non-JSON success response (e.g. plain-text /health "OK") — return as-is. + return (await response.text()) as unknown as T; + } return (await response.json()) as T; }