feat(browser): add Vivaldi as a Chromium cookie-import source (#11449)

Detect Vivaldi profiles on macOS, Windows, and Linux and import their
cookies like other Chromium browsers. On Linux, Vivaldi stores its
cookie key under Chrome's libsecret application name, so the browser
definition can override the keyring application used for the lookup.

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
dojineko
2026-10-28 01:28:04 -07:00
committed by GitHub
co-authored by Neil
parent 5529ca6602
commit 85ea1cc4c5
8 changed files with 102 additions and 6 deletions
@@ -685,7 +685,8 @@ describe('detectInstalledBrowsers', () => {
'firefox',
'safari',
'comet',
'helium'
'helium',
'vivaldi'
]
for (const browser of browsers) {
expect(validFamilies).toContain(browser.family)
@@ -0,0 +1,73 @@
import { pbkdf2Sync } from 'node:crypto'
import type * as fsModule from 'node:fs'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { existingPaths, runProcessSyncMock } = vi.hoisted(() => ({
existingPaths: new Set<string>(),
runProcessSyncMock: vi.fn()
}))
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof fsModule>('node:fs')
return {
...actual,
existsSync: (p: string) => existingPaths.has(p.replaceAll('\\', '/'))
}
})
vi.mock('@orca/process-host', () => ({ runProcessSync: runProcessSyncMock }))
import { detectInstalledBrowsers } from './sources/browser-cookie-detection'
import { getEncryptionKey } from './sources/browser-cookie-key'
const originalPlatform = process.platform
const originalEnv = { ...process.env }
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform })
}
beforeEach(() => {
existingPaths.clear()
runProcessSyncMock.mockReset()
process.env.HOME = '/home/test'
process.env.LOCALAPPDATA = 'C:/Users/test/AppData/Local'
process.env.XDG_CONFIG_HOME = '/home/test/.config'
})
afterEach(() => {
setPlatform(originalPlatform)
process.env = { ...originalEnv }
})
describe('Vivaldi cookie import source', () => {
it.each([
['darwin', '/home/test/Library/Application Support/Vivaldi/Default/Cookies'],
['win32', 'C:/Users/test/AppData/Local/Vivaldi/User Data/Default/Network/Cookies'],
['linux', '/home/test/.config/vivaldi/Default/Cookies']
] as const)('detects the %s profile store', (platform, cookiesPath) => {
setPlatform(platform)
existingPaths.add(cookiesPath)
const vivaldi = detectInstalledBrowsers().find((browser) => browser.family === 'vivaldi')
expect(vivaldi?.label).toBe('Vivaldi')
expect(vivaldi?.cookiesPath.replaceAll('\\', '/')).toBe(cookiesPath)
})
it('reads the Linux keyring secret Vivaldi stores under the Chrome application name', () => {
setPlatform('linux')
existingPaths.add('/home/test/.config/vivaldi/Default/Cookies')
runProcessSyncMock.mockImplementation(({ args }: { args: string[] }) =>
args.join(' ') === 'lookup application chrome'
? { code: 0, timedOut: false, stdout: 'vivaldi-secret\n', stderr: '' }
: { code: 1, timedOut: false, stdout: '', stderr: '' }
)
const vivaldi = detectInstalledBrowsers().find((browser) => browser.family === 'vivaldi')
const key = getEncryptionKey(vivaldi!.keychainService!, vivaldi!.keychainAccount!, vivaldi)
expect(key).toMatchObject({
keysByVersion: { v11: pbkdf2Sync('vivaldi-secret', 'saltysalt', 1, 16, 'sha1') }
})
})
})
@@ -22,6 +22,8 @@ export type ChromiumBrowserDef = {
label: string
keychainService: string
keychainAccount: string
// Linux libsecret `application` attribute; defaults to keychainAccount lowercased without spaces.
linuxKeyringApplication?: string
macRoot?: string
winRoot?: string
linuxRoot?: string
@@ -77,6 +79,17 @@ export const CHROMIUM_BROWSERS: ChromiumBrowserDef[] = [
keychainAccount: 'Helium',
macRoot: 'net.imput.helium'
// Only the macOS install is verified.
},
{
family: 'vivaldi',
label: 'Vivaldi',
keychainService: 'Vivaldi Safe Storage',
keychainAccount: 'Vivaldi',
macRoot: 'Vivaldi',
winRoot: 'Vivaldi/User Data',
linuxRoot: 'vivaldi',
// Vivaldi on Linux keeps Chrome's libsecret entry name (matches yt-dlp and browser_cookie3).
linuxKeyringApplication: 'chrome'
}
]
@@ -35,7 +35,10 @@ export function getEncryptionKey(
return getMacEncryptionKey(keychainService, keychainAccount)
}
if (process.platform === 'linux') {
return getLinuxEncryptionKey(keychainService, keychainAccount)
const application = CHROMIUM_BROWSERS.find(
(b) => b.family === browser?.family
)?.linuxKeyringApplication
return getLinuxEncryptionKey(keychainService, keychainAccount, application)
}
if (process.platform === 'win32' && browser) {
return getWindowsEncryptionKey(browser)
@@ -66,17 +69,17 @@ export function getMacEncryptionKey(
export function getLinuxEncryptionKey(
keychainService: string,
keychainAccount: string
keychainAccount: string,
application = keychainAccount.toLowerCase().replaceAll(' ', '')
): EncryptionKeyResult | null {
// Linux v10 uses "peanuts"; v11 requires OS key storage, and CBC cannot authenticate a wrong key.
const v10Key = pbkdf2Sync('peanuts', PBKDF2_SALT, 1, PBKDF2_KEY_LENGTH, 'sha1')
let keyringPassword = ''
let keyringFailure: LinuxCookieKeyFailure = 'key-unavailable'
const app = keychainAccount.toLowerCase().replaceAll(' ', '')
for (const args of [
['lookup', 'service', keychainService, 'account', keychainAccount],
['lookup', 'application', app]
['lookup', 'application', application]
]) {
try {
keyringPassword = runKeychainCommand('secret-tool', args, 5_000)
@@ -10,6 +10,7 @@ describe('getBrowserCookieImportSourceLabels', () => {
'Brave',
'Comet',
'Helium',
'Vivaldi',
'Firefox',
'Safari'
])
@@ -21,6 +22,7 @@ describe('getBrowserCookieImportSourceLabels', () => {
'Microsoft Edge',
'Brave',
'Comet',
'Vivaldi',
'Firefox'
])
})
@@ -30,6 +32,7 @@ describe('getBrowserCookieImportSourceLabels', () => {
'Google Chrome',
'Microsoft Edge',
'Brave',
'Vivaldi',
'Firefox'
])
})
+2 -1
View File
@@ -6,7 +6,8 @@ const CHROMIUM_COOKIE_IMPORT_SOURCES = [
{ label: 'Arc', mac: true, win: false, linux: false },
{ label: 'Brave', mac: true, win: true, linux: true },
{ label: 'Comet', mac: true, win: true, linux: false },
{ label: 'Helium', mac: true, win: false, linux: false }
{ label: 'Helium', mac: true, win: false, linux: false },
{ label: 'Vivaldi', mac: true, win: true, linux: true }
] as const
export function getBrowserCookieImportSourceLabels(
+1
View File
@@ -145,6 +145,7 @@ export type BrowserSessionProfileSource = {
| 'safari'
| 'comet'
| 'helium'
| 'vivaldi'
| 'manual'
profileName?: string
importedAt: number
+1
View File
@@ -54,6 +54,7 @@ export const BROWSER_FAMILY_LABELS: Record<string, string> = {
chromium: 'Chromium',
comet: 'Comet',
helium: 'Helium',
vivaldi: 'Vivaldi',
arc: 'Arc',
edge: 'Microsoft Edge',
brave: 'Brave',