fix(browser): stop impersonating the source browser's UA on cookie import (STA-3514) (#12849)
* fix(browser): stop impersonating the source browser's UA on cookie import (STA-3514) Delete the import-time UA synthesis entirely instead of patching its version detection. The session now always keeps the engine-derived UA the registry sets at startup (clean or native), for imported and non-imported profiles alike. Why deletion is the fix: - The synthesis read CFBundleShortVersionString, which on forks is a marketing version — Arc imports presented Chrome/1.x and sites rejected the browser as ancient (STA-3514). - Its stated purpose (keep Google-bound cookies valid) is handled by isGoogleSourceBoundCookie excluding those cookies since #736, and #12884 measured that Google rejects re-transplants regardless of UA identity. - #12608 measured the synthesized Chrome-shaped UA reaching /v3/signin/rejected while the engine UA reached account lookup. Persisted userAgent/userAgentByPartition meta is no longer read; legacy keys drop off on the next meta write. The #12811 gate existed only to catch bad synthesized values, so it leaves with the synthesis. * test(browser): catch source UA impersonation regression
This commit is contained in:
parent
7aae88cd21
commit
c03c01ad15
|
|
@ -17,8 +17,7 @@ const {
|
||||||
vi.mock('./browser-session-registry', () => ({
|
vi.mock('./browser-session-registry', () => ({
|
||||||
browserSessionRegistry: {
|
browserSessionRegistry: {
|
||||||
setPendingCookieImport: setPendingCookieImportMock,
|
setPendingCookieImport: setPendingCookieImportMock,
|
||||||
clearPendingCookieImport: clearPendingCookieImportMock,
|
clearPendingCookieImport: clearPendingCookieImportMock
|
||||||
persistUserAgent: vi.fn()
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
vi.mock('node:child_process', () => ({ execFileSync: execFileSyncMock }))
|
vi.mock('node:child_process', () => ({ execFileSync: execFileSyncMock }))
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
||||||
import type * as childProcessModule from 'node:child_process'
|
|
||||||
|
|
||||||
describe('isAdvertisableChromiumEngineVersion', () => {
|
|
||||||
it('accepts real Chromium engine majors and rejects product versions', async () => {
|
|
||||||
const { isAdvertisableChromiumEngineVersion } = await import('./browser-session-ua')
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('120.0.6099.71')).toBe(true)
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('70.0.0.0')).toBe(true)
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('1.158.1')).toBe(false)
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('1.0.0')).toBe(false)
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('not-a-version')).toBe(false)
|
|
||||||
// Malformed components with a valid major must not pass (would become Chrome/70.not-a-version).
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('70.not-a-version')).toBe(false)
|
|
||||||
expect(isAdvertisableChromiumEngineVersion('120.0.invalid.1')).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isUnadvertisableChromeUserAgent', () => {
|
|
||||||
it('flags stored Chrome/1.x UAs and leaves engine-scale ones alone', async () => {
|
|
||||||
const { isUnadvertisableChromeUserAgent } = await import('./browser-session-ua')
|
|
||||||
const ua = (version: string): string =>
|
|
||||||
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`
|
|
||||||
expect(isUnadvertisableChromeUserAgent(ua('1.158.1'))).toBe(true)
|
|
||||||
expect(isUnadvertisableChromeUserAgent(ua('150.0.7871.47'))).toBe(false)
|
|
||||||
expect(isUnadvertisableChromeUserAgent(`${ua('151.0.0.0')} Edg/151.0.0.0`)).toBe(false)
|
|
||||||
// Why: non-Chrome UAs (Firefox/Safari imports) carry no engine claim to invalidate.
|
|
||||||
expect(isUnadvertisableChromeUserAgent('Mozilla/5.0 (Macintosh) Firefox/126.0')).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getUserAgentForBrowser — Arc product version', () => {
|
|
||||||
const originalPlatform = process.platform
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetModules()
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'darwin' })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not persist Chrome/1.x when Arc reports its product version', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: (cmd: string, args: readonly string[]) => {
|
|
||||||
if (cmd === 'defaults' && args[1]?.includes('/Applications/Arc.app/Contents/Info')) {
|
|
||||||
return '1.158.1\n'
|
|
||||||
}
|
|
||||||
return actual.execFileSync(cmd, args as never)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
expect(getUserAgentForBrowser('arc')).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('still builds a Chrome-shaped UA when Arc reports an engine-scale version', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: (cmd: string, args: readonly string[]) => {
|
|
||||||
if (cmd === 'defaults' && args[1]?.includes('/Applications/Arc.app/Contents/Info')) {
|
|
||||||
return '120.0.6099.71\n'
|
|
||||||
}
|
|
||||||
return actual.execFileSync(cmd, args as never)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('arc')
|
|
||||||
expect(ua).toContain('Chrome/120.0.6099.71')
|
|
||||||
expect(ua).not.toContain('Chrome/1.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import type * as childProcessModule from 'node:child_process'
|
|
||||||
import type * as fsModule from 'node:fs'
|
import type * as fsModule from 'node:fs'
|
||||||
|
|
||||||
const { sessionFromPartitionMock, dialogShowOpenDialogMock } = vi.hoisted(() => ({
|
const { sessionFromPartitionMock, dialogShowOpenDialogMock } = vi.hoisted(() => ({
|
||||||
|
|
@ -223,67 +222,6 @@ describe('detectInstalledBrowsers — Comet', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getUserAgentForBrowser — Comet', () => {
|
|
||||||
const originalPlatform = process.platform
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetModules()
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'darwin' })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns a Chrome-shaped UA string when Comet plist version reads successfully', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: (cmd: string, args: readonly string[]) => {
|
|
||||||
if (cmd === 'defaults' && args[1]?.includes('/Applications/Comet.app/Contents/Info')) {
|
|
||||||
return '120.0.6099.71\n'
|
|
||||||
}
|
|
||||||
return actual.execFileSync(cmd, args as never)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('comet')
|
|
||||||
|
|
||||||
expect(ua).not.toBeNull()
|
|
||||||
expect(ua).toContain('Macintosh; Intel Mac OS X 10_15_7')
|
|
||||||
expect(ua).toContain('AppleWebKit/537.36')
|
|
||||||
expect(ua).toContain('Chrome/120.0.6099.71')
|
|
||||||
expect(ua).toContain('Safari/537.36')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns null when reading the Comet plist version throws', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: () => {
|
|
||||||
throw new Error('defaults: domain not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('comet')
|
|
||||||
expect(ua).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns null on non-darwin platforms regardless of family', async () => {
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'linux' })
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('comet')
|
|
||||||
expect(ua).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('BROWSER_FAMILY_LABELS — Comet', () => {
|
describe('BROWSER_FAMILY_LABELS — Comet', () => {
|
||||||
it('maps the comet family key to the user-facing label "Comet"', () => {
|
it('maps the comet family key to the user-facing label "Comet"', () => {
|
||||||
expect(BROWSER_FAMILY_LABELS.comet).toBe('Comet')
|
expect(BROWSER_FAMILY_LABELS.comet).toBe('Comet')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import type * as childProcessModule from 'node:child_process'
|
|
||||||
import type * as fsModule from 'node:fs'
|
import type * as fsModule from 'node:fs'
|
||||||
|
|
||||||
const { sessionFromPartitionMock, dialogShowOpenDialogMock } = vi.hoisted(() => ({
|
const { sessionFromPartitionMock, dialogShowOpenDialogMock } = vi.hoisted(() => ({
|
||||||
|
|
@ -158,60 +157,6 @@ describe('detectInstalledBrowsers — Helium', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getUserAgentForBrowser — Helium', () => {
|
|
||||||
const originalPlatform = process.platform
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetModules()
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'darwin' })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns a Chrome-shaped UA string when Helium plist version reads successfully', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: (cmd: string, args: readonly string[]) => {
|
|
||||||
if (cmd === 'defaults' && args[1]?.includes('/Applications/Helium.app/Contents/Info')) {
|
|
||||||
return '120.0.6099.71\n'
|
|
||||||
}
|
|
||||||
return actual.execFileSync(cmd, args as never)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('helium')
|
|
||||||
|
|
||||||
expect(ua).not.toBeNull()
|
|
||||||
expect(ua).toContain('Macintosh; Intel Mac OS X 10_15_7')
|
|
||||||
expect(ua).toContain('AppleWebKit/537.36')
|
|
||||||
expect(ua).toContain('Chrome/120.0.6099.71')
|
|
||||||
expect(ua).toContain('Safari/537.36')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns null when reading the Helium plist version throws', async () => {
|
|
||||||
vi.doMock('node:child_process', async () => {
|
|
||||||
const actual = await vi.importActual<typeof childProcessModule>('node:child_process')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
execFileSync: () => {
|
|
||||||
throw new Error('defaults: domain not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getUserAgentForBrowser } = await import('./browser-cookie-import')
|
|
||||||
const ua = getUserAgentForBrowser('helium')
|
|
||||||
expect(ua).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('BROWSER_FAMILY_LABELS — Helium', () => {
|
describe('BROWSER_FAMILY_LABELS — Helium', () => {
|
||||||
it('maps the helium family key to the user-facing label "Helium"', () => {
|
it('maps the helium family key to the user-facing label "Helium"', () => {
|
||||||
expect(BROWSER_FAMILY_LABELS.helium).toBe('Helium')
|
expect(BROWSER_FAMILY_LABELS.helium).toBe('Helium')
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ const {
|
||||||
vi.mock('./browser-session-registry', () => ({
|
vi.mock('./browser-session-registry', () => ({
|
||||||
browserSessionRegistry: {
|
browserSessionRegistry: {
|
||||||
setPendingCookieImport: setPendingCookieImportMock,
|
setPendingCookieImport: setPendingCookieImportMock,
|
||||||
clearPendingCookieImport: clearPendingCookieImportMock,
|
clearPendingCookieImport: clearPendingCookieImportMock
|
||||||
persistUserAgent: vi.fn()
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -425,6 +424,7 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
let cookiesRemoveMock: ReturnType<typeof vi.fn>
|
let cookiesRemoveMock: ReturnType<typeof vi.fn>
|
||||||
let cookiesFlushStoreMock: ReturnType<typeof vi.fn>
|
let cookiesFlushStoreMock: ReturnType<typeof vi.fn>
|
||||||
let clearStorageDataMock: ReturnType<typeof vi.fn>
|
let clearStorageDataMock: ReturnType<typeof vi.fn>
|
||||||
|
let setUserAgentMock: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tmpDir = mkdtempSync(join(tmpdir(), 'orca-chromium-cookie-test-'))
|
tmpDir = mkdtempSync(join(tmpdir(), 'orca-chromium-cookie-test-'))
|
||||||
|
|
@ -432,6 +432,7 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
cookiesRemoveMock = vi.fn().mockResolvedValue(undefined)
|
cookiesRemoveMock = vi.fn().mockResolvedValue(undefined)
|
||||||
cookiesFlushStoreMock = vi.fn().mockResolvedValue(undefined)
|
cookiesFlushStoreMock = vi.fn().mockResolvedValue(undefined)
|
||||||
clearStorageDataMock = vi.fn().mockResolvedValue(undefined)
|
clearStorageDataMock = vi.fn().mockResolvedValue(undefined)
|
||||||
|
setUserAgentMock = vi.fn()
|
||||||
appGetPathMock.mockReset()
|
appGetPathMock.mockReset()
|
||||||
appGetPathMock.mockReturnValue(join(tmpDir, 'userData'))
|
appGetPathMock.mockReturnValue(join(tmpDir, 'userData'))
|
||||||
copyFileSyncMock.mockClear()
|
copyFileSyncMock.mockClear()
|
||||||
|
|
@ -448,7 +449,8 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
remove: cookiesRemoveMock,
|
remove: cookiesRemoveMock,
|
||||||
flushStore: cookiesFlushStoreMock
|
flushStore: cookiesFlushStoreMock
|
||||||
},
|
},
|
||||||
clearStorageData: clearStorageDataMock
|
clearStorageData: clearStorageDataMock,
|
||||||
|
setUserAgent: setUserAgentMock
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -471,6 +473,12 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
]).close()
|
]).close()
|
||||||
|
|
||||||
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
||||||
|
execFileSyncMock.mockImplementation((command: string) => {
|
||||||
|
if (command === 'defaults') {
|
||||||
|
return '120.0.6099.71\n'
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected command: ${command}`)
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
expect(existsSync(`${sourceCookiesPath}-wal`)).toBe(true)
|
expect(existsSync(`${sourceCookiesPath}-wal`)).toBe(true)
|
||||||
const sourceFilesBefore = ['', '-wal', '-shm'].map((suffix) =>
|
const sourceFilesBefore = ['', '-wal', '-shm'].map((suffix) =>
|
||||||
|
|
@ -491,6 +499,7 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
expect(execFileSyncMock.mock.calls.some(([command]) => command === 'security')).toBe(false)
|
expect(execFileSyncMock.mock.calls.some(([command]) => command === 'security')).toBe(false)
|
||||||
|
expect(execFileSyncMock.mock.calls.some(([command]) => command === 'defaults')).toBe(false)
|
||||||
expect(copyFileSyncMock.mock.calls.some(([source]) => source === sourceCookiesPath)).toBe(
|
expect(copyFileSyncMock.mock.calls.some(([source]) => source === sourceCookiesPath)).toBe(
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
@ -502,6 +511,9 @@ describe('importCookiesFromBrowser Chromium', () => {
|
||||||
).toEqual(sourceFilesBefore)
|
).toEqual(sourceFilesBefore)
|
||||||
expect(cookiesRemoveMock).not.toHaveBeenCalled()
|
expect(cookiesRemoveMock).not.toHaveBeenCalled()
|
||||||
expect(clearStorageDataMock).toHaveBeenCalledWith({ storages: ['cookies'] })
|
expect(clearStorageDataMock).toHaveBeenCalledWith({ storages: ['cookies'] })
|
||||||
|
// Why: STA-3514 — imports must never impersonate the source browser; the
|
||||||
|
// session keeps the engine UA the registry set at startup.
|
||||||
|
expect(setUserAgentMock).not.toHaveBeenCalled()
|
||||||
} finally {
|
} finally {
|
||||||
platformSpy.mockRestore()
|
platformSpy.mockRestore()
|
||||||
sourceDb.close()
|
sourceDb.close()
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,6 @@ import type {
|
||||||
BrowserSessionProfileSource
|
BrowserSessionProfileSource
|
||||||
} from '../../shared/types'
|
} from '../../shared/types'
|
||||||
import { browserSessionRegistry } from './browser-session-registry'
|
import { browserSessionRegistry } from './browser-session-registry'
|
||||||
import { getBrowserSessionUserAgentMode } from './browser-session-user-agent-mode'
|
|
||||||
import { isAdvertisableChromiumEngineVersion, setupClientHintsOverride } from './browser-session-ua'
|
|
||||||
import {
|
import {
|
||||||
isGoogleSourceBoundCookie,
|
isGoogleSourceBoundCookie,
|
||||||
normalizeCookieDomain,
|
normalizeCookieDomain,
|
||||||
|
|
@ -772,74 +770,6 @@ export async function importCookiesFromFile(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Direct import from installed Chromium browser
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Why: services bind auth cookies to the creating User-Agent, so build a UA matching the source browser's real version.
|
|
||||||
export function getUserAgentForBrowser(
|
|
||||||
family: BrowserSessionProfileSource['browserFamily']
|
|
||||||
): string | null {
|
|
||||||
// Why: UA version comes from macOS-only plist reading; elsewhere the default Electron UA is acceptable.
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = 'Macintosh; Intel Mac OS X 10_15_7'
|
|
||||||
const chromeBase = 'AppleWebKit/537.36 (KHTML, like Gecko)'
|
|
||||||
|
|
||||||
function readBrowserVersion(
|
|
||||||
appPath: string,
|
|
||||||
plistKey = 'CFBundleShortVersionString'
|
|
||||||
): string | null {
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
execFileSync('defaults', ['read', `${appPath}/Contents/Info`, plistKey], {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
timeout: 5_000
|
|
||||||
}).trim() || null
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function chromeShapedUa(version: string | null, edgeSuffix = false): string | null {
|
|
||||||
if (!version || !isAdvertisableChromiumEngineVersion(version)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const base = `Mozilla/5.0 (${platform}) ${chromeBase} Chrome/${version} Safari/537.36`
|
|
||||||
return edgeSuffix ? `${base} Edg/${version}` : base
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (family) {
|
|
||||||
case 'chrome': {
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Google Chrome.app'))
|
|
||||||
}
|
|
||||||
case 'edge': {
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Microsoft Edge.app'), true)
|
|
||||||
}
|
|
||||||
case 'arc': {
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Arc.app'))
|
|
||||||
}
|
|
||||||
case 'chromium': {
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Brave Browser.app'))
|
|
||||||
}
|
|
||||||
case 'comet': {
|
|
||||||
// Why: Comet is Chromium-based; use Chrome's UA shape so Google-bound auth cookies survive import.
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Comet.app'))
|
|
||||||
}
|
|
||||||
case 'helium': {
|
|
||||||
// Why: Helium is Chromium-based; use Chrome's UA shape so Google-bound auth cookies survive import.
|
|
||||||
return chromeShapedUa(readBrowserVersion('/Applications/Helium.app'))
|
|
||||||
}
|
|
||||||
case 'firefox':
|
|
||||||
case 'safari':
|
|
||||||
case 'manual':
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PBKDF2_ITERATIONS = 1003
|
const PBKDF2_ITERATIONS = 1003
|
||||||
const PBKDF2_KEY_LENGTH = 16
|
const PBKDF2_KEY_LENGTH = 16
|
||||||
const PBKDF2_SALT = 'saltysalt'
|
const PBKDF2_SALT = 'saltysalt'
|
||||||
|
|
@ -1876,15 +1806,12 @@ export async function importCookiesFromBrowser(
|
||||||
diag(` all cookies loaded in-memory — no restart needed`)
|
diag(` all cookies loaded in-memory — no restart needed`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ua = getUserAgentForBrowser(browser.family)
|
// Why: the session keeps the UA the registry set at startup (clean or native).
|
||||||
if (ua) {
|
// Imports must not impersonate the source browser — the synthesized UA read a
|
||||||
targetSession.setUserAgent(ua)
|
// fork's marketing version as a Chromium version (STA-3514), and Google binds
|
||||||
setupClientHintsOverride(targetSession, ua, {
|
// sessions to the re-import, not the UA (#12884), so it bought nothing.
|
||||||
googleAuthOverride: getBrowserSessionUserAgentMode(targetSession) !== 'native'
|
// Google-bound integrity cookies are already excluded by
|
||||||
})
|
// isGoogleSourceBoundCookie, which is what actually prevents CookieMismatch.
|
||||||
browserSessionRegistry.persistUserAgent(targetPartition, ua)
|
|
||||||
diag(` set UA for partition: ${ua.substring(0, 80)}...`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary: BrowserCookieImportSummary = {
|
const summary: BrowserCookieImportSummary = {
|
||||||
totalCookies: sourceRows.length,
|
totalCookies: sourceRows.length,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import type * as browserSessionUaModule from './browser-session-ua'
|
|
||||||
|
|
||||||
const USER_DATA = '/user-data'
|
const USER_DATA = '/user-data'
|
||||||
const META_PATH = `${USER_DATA}/browser-session-meta.json`
|
const META_PATH = `${USER_DATA}/browser-session-meta.json`
|
||||||
|
|
@ -119,15 +118,10 @@ function installModuleMocks(
|
||||||
hasSystemMediaAccess: vi.fn(() => true),
|
hasSystemMediaAccess: vi.fn(() => true),
|
||||||
requestSystemMediaAccess: requestSystemMediaAccessMock
|
requestSystemMediaAccess: requestSystemMediaAccessMock
|
||||||
}))
|
}))
|
||||||
vi.doMock('./browser-session-ua', async () => {
|
vi.doMock('./browser-session-ua', () => ({
|
||||||
// Why: the version gate is the behavior under test, so use the real predicate here.
|
cleanElectronUserAgent: vi.fn((ua: string) => ua.replace(/\s*Electron\/\S+/, '')),
|
||||||
const actual = await vi.importActual<typeof browserSessionUaModule>('./browser-session-ua')
|
setupClientHintsOverride: setupClientHintsOverrideMock
|
||||||
return {
|
}))
|
||||||
cleanElectronUserAgent: vi.fn((ua: string) => ua.replace(/\s*Electron\/\S+/, '')),
|
|
||||||
isUnadvertisableChromeUserAgent: actual.isUnadvertisableChromeUserAgent,
|
|
||||||
setupClientHintsOverride: setupClientHintsOverrideMock
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionFromPartitionMock,
|
sessionFromPartitionMock,
|
||||||
|
|
@ -355,18 +349,63 @@ describe('BrowserSessionRegistry persistence', () => {
|
||||||
expect(fsState.present.has('/staged/default')).toBe(true)
|
expect(fsState.present.has('/staged/default')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('restores a persisted source UA even for native-mode profiles', async () => {
|
// Why: imports before Aug 2026 persisted a synthesized source-browser UA
|
||||||
|
// (fork imports as a broken Chrome/1.x, Chrome imports as a valid version).
|
||||||
|
// Neither may ever be applied again — the engine-derived UA is the only one.
|
||||||
|
it('ignores legacy persisted UAs, valid or broken, and applies the engine UA', async () => {
|
||||||
|
const importedPartition = 'persist:orca-browser-session-11111111-1111-4111-8111-111111111111'
|
||||||
|
const brokenUa =
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.158.1 Safari/537.36'
|
||||||
|
const validUa = 'Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
const fsState = createFsState()
|
||||||
|
seedMeta(fsState, {
|
||||||
|
defaultSource: { browserFamily: 'arc', importedAt: 1 },
|
||||||
|
userAgent: brokenUa,
|
||||||
|
userAgentByPartition: {
|
||||||
|
'persist:orca-browser': brokenUa,
|
||||||
|
[importedPartition]: validUa
|
||||||
|
},
|
||||||
|
pendingCookieDbPath: null,
|
||||||
|
pendingCookieImports: {},
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
id: '11111111-1111-4111-8111-111111111111',
|
||||||
|
scope: 'imported',
|
||||||
|
partition: importedPartition,
|
||||||
|
label: 'Imported',
|
||||||
|
source: { browserFamily: 'chrome', importedAt: 1 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { sessionFromPartitionMock, setupClientHintsOverrideMock } = installModuleMocks(fsState)
|
||||||
|
const { browserSessionRegistry } = await import('./browser-session-registry')
|
||||||
|
|
||||||
|
browserSessionRegistry.initializeBrowserSessionsFromPersistedState()
|
||||||
|
|
||||||
|
const appliedUas = sessionFromPartitionMock.mock.results.flatMap((r) =>
|
||||||
|
r.value.setUserAgent.mock.calls.map((c: unknown[]) => c[0])
|
||||||
|
)
|
||||||
|
expect(appliedUas).not.toContain(brokenUa)
|
||||||
|
expect(appliedUas).not.toContain(validUa)
|
||||||
|
// Why: every non-native profile falls to Orca's own cleaned engine UA.
|
||||||
|
expect(appliedUas.length).toBeGreaterThan(0)
|
||||||
|
expect(appliedUas.every((ua) => ua === 'Mozilla/5.0 Orca')).toBe(true)
|
||||||
|
expect(
|
||||||
|
setupClientHintsOverrideMock.mock.calls.every(
|
||||||
|
(c: unknown[]) => c[1] !== brokenUa && c[1] !== validUa
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('never applies a legacy persisted UA to a native-mode profile', async () => {
|
||||||
const importedPartition = 'persist:orca-browser-session-11111111-1111-4111-8111-111111111111'
|
const importedPartition = 'persist:orca-browser-session-11111111-1111-4111-8111-111111111111'
|
||||||
const importedUa = 'Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36'
|
const importedUa = 'Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36'
|
||||||
const defaultUa = 'Mozilla/5.0 Chrome/119.0.0.0 Safari/537.36'
|
|
||||||
const fsState = createFsState()
|
const fsState = createFsState()
|
||||||
seedMeta(fsState, {
|
seedMeta(fsState, {
|
||||||
defaultSource: null,
|
defaultSource: null,
|
||||||
userAgent: defaultUa,
|
userAgent: null,
|
||||||
userAgentByPartition: {
|
userAgentByPartition: { [importedPartition]: importedUa },
|
||||||
'persist:orca-browser': defaultUa,
|
|
||||||
[importedPartition]: importedUa
|
|
||||||
},
|
|
||||||
pendingCookieDbPath: null,
|
pendingCookieDbPath: null,
|
||||||
pendingCookieImports: {},
|
pendingCookieImports: {},
|
||||||
profiles: [
|
profiles: [
|
||||||
|
|
@ -381,7 +420,7 @@ describe('BrowserSessionRegistry persistence', () => {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { sessionFromPartitionMock, setupClientHintsOverrideMock } = installModuleMocks(fsState)
|
const { sessionFromPartitionMock } = installModuleMocks(fsState)
|
||||||
const { browserSessionRegistry } = await import('./browser-session-registry')
|
const { browserSessionRegistry } = await import('./browser-session-registry')
|
||||||
|
|
||||||
browserSessionRegistry.initializeBrowserSessionsFromPersistedState()
|
browserSessionRegistry.initializeBrowserSessionsFromPersistedState()
|
||||||
|
|
@ -390,19 +429,8 @@ describe('BrowserSessionRegistry persistence', () => {
|
||||||
.filter((_, idx) => sessionFromPartitionMock.mock.calls[idx]?.[0] === importedPartition)
|
.filter((_, idx) => sessionFromPartitionMock.mock.calls[idx]?.[0] === importedPartition)
|
||||||
.map((r) => r.value)
|
.map((r) => r.value)
|
||||||
expect(importedSessions.length).toBeGreaterThan(0)
|
expect(importedSessions.length).toBeGreaterThan(0)
|
||||||
expect(
|
// Why: native mode means the engine UA stands untouched — no setUserAgent at all.
|
||||||
importedSessions.some((s) =>
|
expect(importedSessions.every((s) => s.setUserAgent.mock.calls.length === 0)).toBe(true)
|
||||||
s.setUserAgent.mock.calls.some((c: unknown[]) => c[0] === importedUa)
|
|
||||||
)
|
|
||||||
).toBe(true)
|
|
||||||
expect(
|
|
||||||
setupClientHintsOverrideMock.mock.calls.some(
|
|
||||||
(c: unknown[]) =>
|
|
||||||
(c[0] as { partition?: string } | undefined)?.partition === importedPartition &&
|
|
||||||
c[1] === importedUa &&
|
|
||||||
(c[2] as { googleAuthOverride?: boolean } | undefined)?.googleAuthOverride === false
|
|
||||||
)
|
|
||||||
).toBe(true)
|
|
||||||
const { getBrowserSessionUserAgentMode } = await import('./browser-session-user-agent-mode')
|
const { getBrowserSessionUserAgentMode } = await import('./browser-session-user-agent-mode')
|
||||||
expect(
|
expect(
|
||||||
importedSessions.every(
|
importedSessions.every(
|
||||||
|
|
@ -411,39 +439,6 @@ describe('BrowserSessionRegistry persistence', () => {
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('drops a persisted fork product-version UA instead of replaying Chrome/1.x', async () => {
|
|
||||||
const brokenUa =
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.158.1 Safari/537.36'
|
|
||||||
const fsState = createFsState()
|
|
||||||
seedMeta(fsState, {
|
|
||||||
defaultSource: { browserFamily: 'arc', importedAt: 1 },
|
|
||||||
userAgent: brokenUa,
|
|
||||||
userAgentByPartition: { 'persist:orca-browser': brokenUa },
|
|
||||||
pendingCookieDbPath: null,
|
|
||||||
pendingCookieImports: {},
|
|
||||||
profiles: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const { sessionFromPartitionMock, setupClientHintsOverrideMock } = installModuleMocks(fsState)
|
|
||||||
const { browserSessionRegistry } = await import('./browser-session-registry')
|
|
||||||
|
|
||||||
browserSessionRegistry.initializeBrowserSessionsFromPersistedState()
|
|
||||||
|
|
||||||
const appliedUas = sessionFromPartitionMock.mock.results.flatMap((r) =>
|
|
||||||
r.value.setUserAgent.mock.calls.map((c: unknown[]) => c[0])
|
|
||||||
)
|
|
||||||
expect(appliedUas).not.toContain(brokenUa)
|
|
||||||
// Why: with the broken UA gone the profile must fall back to Orca's own cleaned engine UA.
|
|
||||||
expect(appliedUas).toContain('Mozilla/5.0 Orca')
|
|
||||||
expect(setupClientHintsOverrideMock.mock.calls.some((c: unknown[]) => c[1] === brokenUa)).toBe(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
const persisted = JSON.parse(fsState.files.get(META_PATH) ?? '{}')
|
|
||||||
expect(persisted.userAgentByPartition).toEqual({})
|
|
||||||
expect(persisted.userAgent).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('preserves native mode across hydration when no source UA was imported', async () => {
|
it('preserves native mode across hydration when no source UA was imported', async () => {
|
||||||
const importedPartition = 'persist:orca-browser-session-12121212-1212-4121-8121-121212121212'
|
const importedPartition = 'persist:orca-browser-session-12121212-1212-4121-8121-121212121212'
|
||||||
const fsState = createFsState()
|
const fsState = createFsState()
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,7 @@ import type {
|
||||||
} from '../../shared/types'
|
} from '../../shared/types'
|
||||||
import { browserManager } from './browser-manager'
|
import { browserManager } from './browser-manager'
|
||||||
import { hasSystemMediaAccess, requestSystemMediaAccess } from './browser-media-access'
|
import { hasSystemMediaAccess, requestSystemMediaAccess } from './browser-media-access'
|
||||||
import {
|
import { cleanElectronUserAgent, setupClientHintsOverride } from './browser-session-ua'
|
||||||
cleanElectronUserAgent,
|
|
||||||
isUnadvertisableChromeUserAgent,
|
|
||||||
setupClientHintsOverride
|
|
||||||
} from './browser-session-ua'
|
|
||||||
import {
|
import {
|
||||||
clearBrowserSessionUserAgentMode,
|
clearBrowserSessionUserAgentMode,
|
||||||
setBrowserSessionUserAgentMode
|
setBrowserSessionUserAgentMode
|
||||||
|
|
@ -42,10 +38,12 @@ import {
|
||||||
installBrowserWebAuthnAccessHandlers
|
installBrowserWebAuthnAccessHandlers
|
||||||
} from './browser-webauthn-access'
|
} from './browser-webauthn-access'
|
||||||
|
|
||||||
|
// Why: no userAgent fields — the session UA is always derived from the running
|
||||||
|
// engine at startup (clean or native), never persisted. Imports before Aug 2026
|
||||||
|
// stored a synthesized source-browser UA here; persistMeta drops those legacy
|
||||||
|
// keys on the next write because this loader no longer carries them.
|
||||||
type BrowserSessionMeta = {
|
type BrowserSessionMeta = {
|
||||||
defaultSource: BrowserSessionProfile['source']
|
defaultSource: BrowserSessionProfile['source']
|
||||||
userAgent: string | null
|
|
||||||
userAgentByPartition: Record<string, string>
|
|
||||||
pendingCookieDbPath: string | null
|
pendingCookieDbPath: string | null
|
||||||
pendingCookieImports: Record<string, string>
|
pendingCookieImports: Record<string, string>
|
||||||
profiles: BrowserSessionProfile[]
|
profiles: BrowserSessionProfile[]
|
||||||
|
|
@ -122,11 +120,8 @@ class BrowserSessionRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private persistSource(source: BrowserSessionProfile['source'], userAgent?: string | null): void {
|
private persistSource(source: BrowserSessionProfile['source']): void {
|
||||||
this.persistMeta({
|
this.persistMeta({ defaultSource: source })
|
||||||
defaultSource: source,
|
|
||||||
...(userAgent !== undefined ? { userAgent } : {})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Why: non-default profiles are in-memory only; without this they vanish on restart.
|
// Why: non-default profiles are in-memory only; without this they vanish on restart.
|
||||||
|
|
@ -139,15 +134,6 @@ class BrowserSessionRegistry {
|
||||||
try {
|
try {
|
||||||
const raw = readFileSync(this.metadataPath, 'utf-8')
|
const raw = readFileSync(this.metadataPath, 'utf-8')
|
||||||
const data = JSON.parse(raw)
|
const data = JSON.parse(raw)
|
||||||
const legacyUserAgent = typeof data?.userAgent === 'string' ? data.userAgent : null
|
|
||||||
const userAgentByPartition: Record<string, string> =
|
|
||||||
data && typeof data.userAgentByPartition === 'object' && data.userAgentByPartition
|
|
||||||
? { ...data.userAgentByPartition }
|
|
||||||
: {}
|
|
||||||
if (legacyUserAgent && !userAgentByPartition[this.defaultPartition]) {
|
|
||||||
userAgentByPartition[this.defaultPartition] = legacyUserAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyPendingCookieDbPath =
|
const legacyPendingCookieDbPath =
|
||||||
typeof data?.pendingCookieDbPath === 'string' ? data.pendingCookieDbPath : null
|
typeof data?.pendingCookieDbPath === 'string' ? data.pendingCookieDbPath : null
|
||||||
const pendingCookieImports: Record<string, string> =
|
const pendingCookieImports: Record<string, string> =
|
||||||
|
|
@ -159,8 +145,6 @@ class BrowserSessionRegistry {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
defaultSource: data?.defaultSource ?? null,
|
defaultSource: data?.defaultSource ?? null,
|
||||||
userAgent: legacyUserAgent,
|
|
||||||
userAgentByPartition,
|
|
||||||
pendingCookieDbPath: legacyPendingCookieDbPath,
|
pendingCookieDbPath: legacyPendingCookieDbPath,
|
||||||
pendingCookieImports,
|
pendingCookieImports,
|
||||||
profiles: Array.isArray(data?.profiles) ? data.profiles : []
|
profiles: Array.isArray(data?.profiles) ? data.profiles : []
|
||||||
|
|
@ -168,8 +152,6 @@ class BrowserSessionRegistry {
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
defaultSource: null,
|
defaultSource: null,
|
||||||
userAgent: null,
|
|
||||||
userAgentByPartition: {},
|
|
||||||
pendingCookieDbPath: null,
|
pendingCookieDbPath: null,
|
||||||
pendingCookieImports: {},
|
pendingCookieImports: {},
|
||||||
profiles: []
|
profiles: []
|
||||||
|
|
@ -200,20 +182,6 @@ class BrowserSessionRegistry {
|
||||||
const sess = session.fromPartition(partition)
|
const sess = session.fromPartition(partition)
|
||||||
const userAgentMode = profile.userAgentMode ?? 'clean'
|
const userAgentMode = profile.userAgentMode ?? 'clean'
|
||||||
setBrowserSessionUserAgentMode(sess, userAgentMode)
|
setBrowserSessionUserAgentMode(sess, userAgentMode)
|
||||||
const persistedUa = meta.userAgentByPartition[partition]
|
|
||||||
if (persistedUa) {
|
|
||||||
// Why: imports before the engine-version gate stored a fork's product version (Chrome/1.x);
|
|
||||||
// it is reapplied every launch, so drop it here or the profile stays blocked forever.
|
|
||||||
if (isUnadvertisableChromeUserAgent(persistedUa)) {
|
|
||||||
this.persistUserAgent(partition, null)
|
|
||||||
} else {
|
|
||||||
sess.setUserAgent(persistedUa)
|
|
||||||
setupClientHintsOverride(sess, persistedUa, {
|
|
||||||
googleAuthOverride: userAgentMode !== 'native'
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile.userAgentMode === 'native') {
|
if (profile.userAgentMode === 'native') {
|
||||||
continue
|
continue
|
||||||
|
|
@ -334,20 +302,6 @@ class BrowserSessionRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
persistUserAgent(partition: string, userAgent: string | null): void {
|
|
||||||
const meta = this.loadPersistedMeta()
|
|
||||||
const userAgentByPartition = { ...meta.userAgentByPartition }
|
|
||||||
if (userAgent) {
|
|
||||||
userAgentByPartition[partition] = userAgent
|
|
||||||
} else {
|
|
||||||
delete userAgentByPartition[partition]
|
|
||||||
}
|
|
||||||
this.persistMeta({
|
|
||||||
userAgentByPartition,
|
|
||||||
userAgent: userAgentByPartition[this.defaultPartition] ?? null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultProfile(): BrowserSessionProfile {
|
getDefaultProfile(): BrowserSessionProfile {
|
||||||
return this.profiles.get('default')!
|
return this.profiles.get('default')!
|
||||||
}
|
}
|
||||||
|
|
@ -441,13 +395,9 @@ class BrowserSessionRegistry {
|
||||||
const meta = this.loadPersistedMeta()
|
const meta = this.loadPersistedMeta()
|
||||||
const pendingCookieImports = { ...meta.pendingCookieImports }
|
const pendingCookieImports = { ...meta.pendingCookieImports }
|
||||||
delete pendingCookieImports[profile.partition]
|
delete pendingCookieImports[profile.partition]
|
||||||
const userAgentByPartition = { ...meta.userAgentByPartition }
|
|
||||||
delete userAgentByPartition[profile.partition]
|
|
||||||
this.persistMeta({
|
this.persistMeta({
|
||||||
pendingCookieImports,
|
pendingCookieImports,
|
||||||
pendingCookieDbPath: pendingCookieImports[this.defaultPartition] ?? null,
|
pendingCookieDbPath: pendingCookieImports[this.defaultPartition] ?? null
|
||||||
userAgentByPartition,
|
|
||||||
userAgent: userAgentByPartition[this.defaultPartition] ?? null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Why: clear the partition's storage so deleting a profile doesn't leave orphaned cookies/cache behind.
|
// Why: clear the partition's storage so deleting a profile doesn't leave orphaned cookies/cache behind.
|
||||||
|
|
@ -474,12 +424,8 @@ class BrowserSessionRegistry {
|
||||||
const meta = this.loadPersistedMeta()
|
const meta = this.loadPersistedMeta()
|
||||||
const pendingCookieImports = { ...meta.pendingCookieImports }
|
const pendingCookieImports = { ...meta.pendingCookieImports }
|
||||||
delete pendingCookieImports[this.defaultPartition]
|
delete pendingCookieImports[this.defaultPartition]
|
||||||
const userAgentByPartition = { ...meta.userAgentByPartition }
|
|
||||||
delete userAgentByPartition[this.defaultPartition]
|
|
||||||
this.persistMeta({
|
this.persistMeta({
|
||||||
defaultSource: null,
|
defaultSource: null,
|
||||||
userAgent: null,
|
|
||||||
userAgentByPartition,
|
|
||||||
pendingCookieDbPath: null,
|
pendingCookieDbPath: null,
|
||||||
pendingCookieImports
|
pendingCookieImports
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -21,31 +21,11 @@ export function cleanElectronUserAgent(ua: string): string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Why: Chromium forks (Arc, Brave, …) report a product version (1.x) in
|
// Why: Electron emits sec-ch-ua brands like "Not A(Brand" without a
|
||||||
// CFBundleShortVersionString. Advertising that as Chrome/1.x makes version-gating
|
// "Google Chrome" entry, which disagrees with the Chrome-shaped UA the session
|
||||||
// sites treat the session as ancient Chromium, so only engine-scale majors are safe.
|
// presents. Rewrite the hint headers to the brand set Chrome ships for the same
|
||||||
export function isAdvertisableChromiumEngineVersion(version: string): boolean {
|
// engine version so the two surfaces tell one story. Also owns the Google
|
||||||
const normalizedVersion = version.trim()
|
// auth-host Firefox switch, which must install even for a non-Chrome-shaped UA.
|
||||||
// Reject malformed tokens (e.g. 70.not-a-version) so they never become Chrome/… in the UA.
|
|
||||||
if (!/^\d+(?:\.\d+)*$/.test(normalizedVersion)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Chrome 70+ covers every Chromium engine we still support; product versions stay below.
|
|
||||||
return Number(normalizedVersion.split('.')[0]) >= 70
|
|
||||||
}
|
|
||||||
|
|
||||||
// Why: builds without the version gate persisted Chrome/1.x for fork imports, and a stored
|
|
||||||
// UA is reapplied on every launch — so the profile stays blocked until the value is dropped.
|
|
||||||
export function isUnadvertisableChromeUserAgent(ua: string): boolean {
|
|
||||||
const chromeVersion = /Chrome\/(\S+)/.exec(ua)?.[1]
|
|
||||||
return chromeVersion !== undefined && !isAdvertisableChromiumEngineVersion(chromeVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Why: Electron's actual Chromium version (e.g. 134) differs from the source
|
|
||||||
// browser's version (e.g. Edge 147). The sec-ch-ua Client Hints headers
|
|
||||||
// reveal the real version, creating a mismatch that Google's anti-fraud
|
|
||||||
// detection flags as CookieMismatch on accounts.google.com. Override Client
|
|
||||||
// Hints on outgoing requests to match the source browser's UA.
|
|
||||||
export function setupClientHintsOverride(
|
export function setupClientHintsOverride(
|
||||||
sess: Session,
|
sess: Session,
|
||||||
ua: string,
|
ua: string,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue