From 401a7d087a034b54a7e4e65913d1a0b91f738e09 Mon Sep 17 00:00:00 2001 From: Peyton Lu Date: Wed, 16 Sep 2026 01:57:46 +0800 Subject: [PATCH] fix(desktop): keep cookie partitions colon-free so Windows jars work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron escapes ':' in a session partition name as '%3A' for the on-disk profile folder. On Windows, a profile folder whose name contains '%3A' gets a cookie store the network stack can neither read nor write: a jar placed there reads back zero cookies, `cookies.set()` never reaches disk, and every request goes out with no cookies at all — the gateway answers 401 `no_cookie` and the desktop concludes the user is signed out. Per-connection partitions (#92183) are the only ones carrying a colon beyond the 'persist:' prefix, so every NON-primary cookie-auth remote hits this: the dial mints no ticket, the reauth copy fires ("Remote Hermes gateway uses OAuth, but you are not signed in…"), and the login window — riding the same partition — writes into the same invisible jar. Nothing ever persists, so the prompt returns on every connect. The registry primary and the v1 remote keep the legacy `persist:hermes-remote-oauth` jar, whose folder has no '%3A', which is why a single-gateway setup never shows the symptom. Verified against the app's own Electron runtime (40.10.2, Windows): identical jar bytes seeded into two partition folders read 3 cookies and mint a ws-ticket (200) from `…-conn-…`, and 0 cookies / 401 from `…%3Aconn%3A…`. Keep the partition path component inside [A-Za-z0-9._-]; a regression test pins that it never contains ':' or '%'. --- apps/desktop/electron/oauth-partition.test.ts | 18 ++++++++++++++++++ apps/desktop/electron/oauth-partition.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron/oauth-partition.test.ts b/apps/desktop/electron/oauth-partition.test.ts index 8aabdc6b95..6afb388c98 100644 --- a/apps/desktop/electron/oauth-partition.test.ts +++ b/apps/desktop/electron/oauth-partition.test.ts @@ -131,6 +131,24 @@ describe('resolveOauthPartition (#92183 per-connection cookie jars)', () => { expect(resolveOauthPartition('https://gw-a.example.com', { registry: reg })).toBe(got) }) + it('keeps the on-disk path component colon-free and %-free (Windows cookie-store regression)', () => { + // Electron escapes ':' in a partition name to '%3A' for the folder name. + // A Windows profile folder containing '%3A' gets a cookie store that reads + // empty and never persists, so every cookie-auth connection would 401 and + // re-prompt for sign-in on each dial. The partition path component must + // therefore never need escaping, for ANY connection id. + for (const id of ['10-0-0-88-9119', 'we ird/id:€', 'a:b/c%3Ad']) { + const reg = registry('local', [remote(id, 'https://gw-a.example.com')]) + + const pathComponent = resolveOauthPartition('https://gw-a.example.com/api/auth/ws-ticket', { + registry: reg + }).slice('persist:'.length) + + expect(pathComponent).not.toContain(':') + expect(pathComponent).not.toContain('%') + } + }) + it('breaks same-URL ties deterministically (identical jar for identical gateway)', () => { const reg = registry('local', [ remote('zeta', 'https://gw-a.example.com'), diff --git a/apps/desktop/electron/oauth-partition.ts b/apps/desktop/electron/oauth-partition.ts index d83eece8a1..e014c6a106 100644 --- a/apps/desktop/electron/oauth-partition.ts +++ b/apps/desktop/electron/oauth-partition.ts @@ -36,7 +36,15 @@ export const LEGACY_OAUTH_PARTITION = 'persist:hermes-remote-oauth' -const CONNECTION_PARTITION_PREFIX = `${LEGACY_OAUTH_PARTITION}:conn:` +// Colon-free ON PURPOSE: Electron escapes ':' in a partition name to '%3A' in +// the on-disk profile folder, and a Windows profile folder whose name contains +// '%3A' gets a cookie store the network stack can neither read nor write (a +// jar seeded into it reads back zero cookies, `cookies.set()` never reaches +// disk, and every cookie-authenticated request 401s as `no_cookie`). A jar +// the app cannot see means the dial always looks signed-out and the user is +// asked to sign in again on every connect. Keep this path component to +// [A-Za-z0-9._-] — never a character Electron has to escape. +const CONNECTION_PARTITION_PREFIX = `${LEGACY_OAUTH_PARTITION}-conn-` export interface PartitionRegistrySnapshot { primary?: unknown