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 <plluviera@users.noreply.github.com>
Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
This commit is contained in:
teknium1
2026-09-15 12:08:58 -07:00
committed by Teknium
parent 66f9f3692a
commit e65ddf1c7b
2 changed files with 28 additions and 0 deletions
@@ -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',
@@ -19,6 +19,7 @@ interface ResolvedPrimaryRemote {
authMode?: 'oauth' | 'token'
baseUrl: string
connectionId?: string
headers?: Record<string, string>
remoteHermesVersion?: string
remoteHost?: string
remoteKind?: 'cloud' | 'ssh' | 'url'
@@ -56,6 +57,9 @@ export function createPrimaryRemoteConnection<State extends object>(
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,