mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -145,6 +145,7 @@ export type BrowserSessionProfileSource = {
|
||||
| 'safari'
|
||||
| 'comet'
|
||||
| 'helium'
|
||||
| 'vivaldi'
|
||||
| 'manual'
|
||||
profileName?: string
|
||||
importedAt: number
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user