Prevent duplicate Kagi session tokens and redact load error credentials

Canonicalize navigation URLs to strip duplicate token parameters before queueing. Redact Kagi session tokens from loadError.validatedUrl in tab listings to prevent credential leakage in error displays.
This commit is contained in:
Jinjing
2026-08-23 11:50:43 -07:00
parent bc59f38bb8
commit a00736e8fc
4 changed files with 57 additions and 3 deletions
@@ -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],
+3 -1
View File
@@ -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
})
}
@@ -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'
@@ -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)
}