diff --git a/src/main/browser/agent-browser-bridge-tab-routing.test.ts b/src/main/browser/agent-browser-bridge-tab-routing.test.ts index 1aa34227d60..60c115e90c5 100644 --- a/src/main/browser/agent-browser-bridge-tab-routing.test.ts +++ b/src/main/browser/agent-browser-bridge-tab-routing.test.ts @@ -192,6 +192,28 @@ describe('AgentBrowserBridge', () => { expect(JSON.stringify(result)).not.toContain('session-secret') }) + it('redacts Kagi session credentials from a listed load error', () => { + const tabs = new Map([['tab-a', 1]]) + const wc = mockWebContents(1, 'chrome-error://chromewebdata/', '') + webContentsFromIdMock.mockReturnValue(wc) + const loadError = { + code: -105, + description: 'Name not resolved', + validatedUrl: 'https://kagi.com/search?token=session-secret&q=private+project' + } + + const result = new AgentBrowserBridge( + mockBrowserManager(tabs, new Map(), { + getBrowserPageLoadError: vi.fn((tabId: string) => (tabId === 'tab-a' ? loadError : null)) + }) + ).tabList() + + expect(result.tabs[0]?.loadError?.validatedUrl).toBe( + 'https://kagi.com/search?q=private+project' + ) + expect(JSON.stringify(result)).not.toContain('session-secret') + }) + it('does not mutate active-tab routing when tab-list infers the first live tab', () => { const tabs = new Map([ ['tab-a', 1], diff --git a/src/main/browser/agent-browser-bridge.ts b/src/main/browser/agent-browser-bridge.ts index 1ed7dc2b5e4..cbd689243a5 100644 --- a/src/main/browser/agent-browser-bridge.ts +++ b/src/main/browser/agent-browser-bridge.ts @@ -751,7 +751,9 @@ export class AgentBrowserBridge { url: redactKagiSessionToken(loadError?.validatedUrl ?? wc.getURL() ?? ''), title: redactKagiSessionToken(wc.getTitle() ?? ''), active: wcId === activeWcId, - loadError, + loadError: loadError + ? { ...loadError, validatedUrl: redactKagiSessionToken(loadError.validatedUrl) } + : loadError, certificateFailure }) } diff --git a/src/renderer/src/lib/kagi-private-initial-navigation.test.ts b/src/renderer/src/lib/kagi-private-initial-navigation.test.ts index 4b0d942c79a..89eda35f059 100644 --- a/src/renderer/src/lib/kagi-private-initial-navigation.test.ts +++ b/src/renderer/src/lib/kagi-private-initial-navigation.test.ts @@ -88,6 +88,20 @@ describe('Kagi private initial navigation', () => { }) }) + it('queues a single bearer token when the link carries duplicates', () => { + const modelUrl = 'https://kagi.com/search?q=private+project' + queueKagiPrivateInitialNavigation( + 'page-duplicate', + 'https://kagi.com/search?token=first-secret&q=private+project&token=second-secret' + ) + + expect(getKagiPrivateInitialNavigation('page-duplicate', modelUrl)).toEqual({ + modelUrl, + navigationUrl: 'https://kagi.com/search?token=first-secret&q=private+project' + }) + discardKagiPrivateInitialNavigation('page-duplicate') + }) + it('redacts a defensive model fallback', () => { const privateUrl = 'https://kagi.com/search?token=session-secret&q=private+project' diff --git a/src/renderer/src/lib/kagi-private-initial-navigation.ts b/src/renderer/src/lib/kagi-private-initial-navigation.ts index 172454afb57..a6195264d54 100644 --- a/src/renderer/src/lib/kagi-private-initial-navigation.ts +++ b/src/renderer/src/lib/kagi-private-initial-navigation.ts @@ -25,13 +25,29 @@ function prunePendingNavigations(now: number): void { } } +// Why: queue the validator's canonical link so duplicate `token` params can't send a second bearer to Kagi. +function canonicalizeKagiNavigationUrl(url: string): string | null { + const normalized = normalizeKagiSessionLink(url) + if (!normalized) { + return null + } + const query = new URL(url).searchParams.get('q') + if (query === null) { + return normalized + } + const canonical = new URL(normalized) + canonical.searchParams.set('q', query) + return canonical.toString() +} + export function queueKagiPrivateInitialNavigation(pageId: string, url: string): void { - if (!normalizeKagiSessionLink(url)) { + const canonicalUrl = canonicalizeKagiNavigationUrl(url) + if (!canonicalUrl) { throw new Error('Expected a Kagi private-session URL.') } const now = Date.now() pendingNavigations.delete(pageId) - pendingNavigations.set(pageId, { queuedAt: now, url }) + pendingNavigations.set(pageId, { queuedAt: now, url: canonicalUrl }) prunePendingNavigations(now) }