From e65ddf1c7bcac6222dd475c8f45980b069a8f555 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:08:58 -0700 Subject: [PATCH] fix(desktop): keep primary remote gateway extra headers on REST calls createPrimaryRemoteConnection() rebuilt the primary remote descriptor field by field and left out `headers`, so every REST call routed through fetchJsonForBackend() (Settings profiles/config, session history) reached the gateway without the configured extra headers while chat, the Test button and the readiness probe -- which read the exact-URL WebSocket header store or the resolved route directly -- kept working. Behind an access proxy (e.g. a service-token gate) those REST calls came back as a 302 to the login page. Carry `headers` through the descriptor like every other rebuild site (buildRemoteConnection, the registry pool path and both ensureRegistryBackend reuse branches already spread it), and pin it with an invariant test on the existing primary-descriptor seam. Fixes #112072 Co-authored-by: plluviera Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com> --- .../electron/primary-backend-startup.test.ts | 24 +++++++++++++++++++ .../electron/primary-backend-startup.ts | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/apps/desktop/electron/primary-backend-startup.test.ts b/apps/desktop/electron/primary-backend-startup.test.ts index de94ddee78..dc5f208245 100644 --- a/apps/desktop/electron/primary-backend-startup.test.ts +++ b/apps/desktop/electron/primary-backend-startup.test.ts @@ -50,6 +50,30 @@ test('primary remote descriptor preserves a resolved registry connection id', () assert.equal(connection.isFullscreen, false) }) +test('primary remote descriptor preserves the gateway extra headers for REST calls', () => { + // Chat and the Test button carry the headers via the exact-URL WS store, but + // fetchJsonForBackend reads descriptor.headers — dropping them here made every + // Settings/session-history call hit an access proxy unauthenticated (#112072). + const headers = { 'CF-Access-Client-Id': 'client-id', 'CF-Access-Client-Secret': 'client-secret' } + + const connection = createPrimaryRemoteConnection( + { + authMode: 'token', + baseUrl: 'https://gateway.example.com', + connectionId: 'gateway', + headers, + remoteKind: 'url', + source: 'settings', + token: 'secret', + wsUrl: 'wss://gateway.example.com/api/ws?token=secret' + }, + [], + {} + ) + + assert.deepEqual(connection.headers, headers) +}) + test('primary remote descriptor preserves the effective SSH dialing identity', () => { const ssh = { effectiveConfigFingerprint: 'effective-config', diff --git a/apps/desktop/electron/primary-backend-startup.ts b/apps/desktop/electron/primary-backend-startup.ts index 37996bcb9d..e319b9b240 100644 --- a/apps/desktop/electron/primary-backend-startup.ts +++ b/apps/desktop/electron/primary-backend-startup.ts @@ -19,6 +19,7 @@ interface ResolvedPrimaryRemote { authMode?: 'oauth' | 'token' baseUrl: string connectionId?: string + headers?: Record remoteHermesVersion?: string remoteHost?: string remoteKind?: 'cloud' | 'ssh' | 'url' @@ -56,6 +57,9 @@ export function createPrimaryRemoteConnection( remoteHermesVersion: remote.remoteHermesVersion, ...(remote.connectionId ? { connectionId: remote.connectionId } : {}), ...(remote.ssh ? { ssh: remote.ssh } : {}), + // fetchJsonForBackend reads descriptor.headers for every REST call; the + // WebSocket header store is keyed by exact URL and cannot stand in for it. + headers: remote.headers, token: remote.token, wsUrl: remote.wsUrl, logs,