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