fix: support Linux Chromium cookie import

This commit is contained in:
AliFozooni
2026-03-20 23:14:43 -07:00
parent 1f4b6fd7a2
commit 97c8084df2
8 changed files with 325 additions and 77 deletions
+1 -1
View File
@@ -416,7 +416,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `click <sel>` | Click element |
| `cookie <name>=<value>` | Set cookie on current page domain |
| `cookie-import <json>` | Import cookies from JSON file |
| `cookie-import-browser [browser] [--domain d]` | Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import) |
| `cookie-import-browser [browser] [--domain d]` | Import cookies from installed Chromium browsers (opens picker, or use --domain for direct import) |
| `dialog-accept [text]` | Auto-accept next alert/confirm/prompt. Optional text is sent as the prompt response |
| `dialog-dismiss` | Auto-dismiss next dialog |
| `fill <sel> <val>` | Fill input |
+1 -1
View File
@@ -73,7 +73,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, { category: string; descriptio
'viewport':{ category: 'Interaction', description: 'Set viewport size', usage: 'viewport <WxH>' },
'cookie': { category: 'Interaction', description: 'Set cookie on current page domain', usage: 'cookie <name>=<value>' },
'cookie-import': { category: 'Interaction', description: 'Import cookies from JSON file', usage: 'cookie-import <json>' },
'cookie-import-browser': { category: 'Interaction', description: 'Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import)', usage: 'cookie-import-browser [browser] [--domain d]' },
'cookie-import-browser': { category: 'Interaction', description: 'Import cookies from installed Chromium browsers (opens picker, or use --domain for direct import)', usage: 'cookie-import-browser [browser] [--domain d]' },
'header': { category: 'Interaction', description: 'Set custom request header (colon-separated, sensitive values auto-redacted)', usage: 'header <name>:<value>' },
'useragent': { category: 'Interaction', description: 'Set user agent', usage: 'useragent <string>' },
'dialog-accept': { category: 'Interaction', description: 'Auto-accept next alert/confirm/prompt. Optional text is sent as the prompt response', usage: 'dialog-accept [text]' },
+172 -48
View File
@@ -1,25 +1,28 @@
/**
* Chromium browser cookie import read and decrypt cookies from real browsers
*
* Supports macOS Chromium-based browsers: Comet, Chrome, Arc, Brave, Edge.
* Supports macOS and Linux Chromium-based browsers.
* Pure logic module no Playwright dependency, no HTTP concerns.
*
* Decryption pipeline (Chromium macOS "v10" format):
* Decryption pipeline:
*
*
* 1. Keychain: `security find-generic-password -s "<svc>" -w`
* base64 password string
* 1. Resolve the cookie DB from the browser profile dir
* - macOS: ~/Library/Application Support/<browser>/<profile>
* - Linux: ~/.config/<browser>/<profile>
*
* 2. Key derivation:
* PBKDF2(password, salt="saltysalt", iter=1003, len=16, sha1)
* 16-byte AES key
* 2. Derive the AES key
* - macOS v10: Keychain password, PBKDF2(..., iter=1003)
* - Linux v10: "peanuts", PBKDF2(..., iter=1)
* - Linux v11: libsecret/secret-tool password, iter=1
*
* 3. For each cookie with encrypted_value starting with "v10":
* 3. For each cookie with encrypted_value starting with "v10"/
* "v11":
* - Ciphertext = encrypted_value[3:]
* - IV = 16 bytes of 0x20 (space character)
* - Plaintext = AES-128-CBC-decrypt(key, iv, ciphertext)
* - Remove PKCS7 padding
* - Skip first 32 bytes (HMAC-SHA256 authentication tag)
* - Skip first 32 bytes of Chromium cookie metadata
* - Remaining bytes = cookie value (UTF-8)
*
* 4. If encrypted_value is empty but `value` field is set,
@@ -42,9 +45,11 @@ import * as os from 'os';
export interface BrowserInfo {
name: string;
dataDir: string; // relative to ~/Library/Application Support/
dataDir: string; // primary storage dir (retained for compatibility with existing callers/tests)
keychainService: string;
aliases: string[];
linuxDataDir?: string;
linuxApplication?: string;
}
export interface DomainEntry {
@@ -81,15 +86,24 @@ export class CookieImportError extends Error {
}
}
type BrowserPlatform = 'darwin' | 'linux';
interface BrowserMatch {
browser: BrowserInfo;
platform: BrowserPlatform;
dbPath: string;
}
// ─── Browser Registry ───────────────────────────────────────────
// Hardcoded — NEVER interpolate user input into shell commands.
const BROWSER_REGISTRY: BrowserInfo[] = [
{ name: 'Comet', dataDir: 'Comet/', keychainService: 'Comet Safe Storage', aliases: ['comet', 'perplexity'] },
{ name: 'Chrome', dataDir: 'Google/Chrome/', keychainService: 'Chrome Safe Storage', aliases: ['chrome', 'google-chrome'] },
{ name: 'Arc', dataDir: 'Arc/User Data/', keychainService: 'Arc Safe Storage', aliases: ['arc'] },
{ name: 'Brave', dataDir: 'BraveSoftware/Brave-Browser/', keychainService: 'Brave Safe Storage', aliases: ['brave'] },
{ name: 'Edge', dataDir: 'Microsoft Edge/', keychainService: 'Microsoft Edge Safe Storage', aliases: ['edge'] },
{ name: 'Comet', dataDir: 'Comet/', keychainService: 'Comet Safe Storage', aliases: ['comet', 'perplexity'] },
{ name: 'Chrome', dataDir: 'Google/Chrome/', keychainService: 'Chrome Safe Storage', aliases: ['chrome', 'google-chrome', 'google-chrome-stable'], linuxDataDir: 'google-chrome/', linuxApplication: 'chrome' },
{ name: 'Chromium', dataDir: 'chromium/', keychainService: 'Chromium Safe Storage', aliases: ['chromium'], linuxDataDir: 'chromium/', linuxApplication: 'chromium' },
{ name: 'Arc', dataDir: 'Arc/User Data/', keychainService: 'Arc Safe Storage', aliases: ['arc'] },
{ name: 'Brave', dataDir: 'BraveSoftware/Brave-Browser/', keychainService: 'Brave Safe Storage', aliases: ['brave'], linuxDataDir: 'BraveSoftware/Brave-Browser/', linuxApplication: 'brave' },
{ name: 'Edge', dataDir: 'Microsoft Edge/', keychainService: 'Microsoft Edge Safe Storage', aliases: ['edge'], linuxDataDir: 'microsoft-edge/', linuxApplication: 'microsoft-edge' },
];
// ─── Key Cache ──────────────────────────────────────────────────
@@ -104,11 +118,14 @@ const keyCache = new Map<string, Buffer>();
* Find which browsers are installed (have a cookie DB on disk).
*/
export function findInstalledBrowsers(): BrowserInfo[] {
const appSupport = path.join(os.homedir(), 'Library', 'Application Support');
return BROWSER_REGISTRY.filter(b => {
const dbPath = path.join(appSupport, b.dataDir, 'Default', 'Cookies');
try { return fs.existsSync(dbPath); } catch { return false; }
});
return BROWSER_REGISTRY.filter(browser => findBrowserMatch(browser, 'Default') !== null);
}
export function listSupportedBrowserNames(): string[] {
const hostPlatform = getHostPlatform();
return BROWSER_REGISTRY
.filter(browser => hostPlatform ? getDataDirForPlatform(browser, hostPlatform) !== null : true)
.map(browser => browser.name);
}
/**
@@ -116,8 +133,8 @@ export function findInstalledBrowsers(): BrowserInfo[] {
*/
export function listDomains(browserName: string, profile = 'Default'): { domains: DomainEntry[]; browser: string } {
const browser = resolveBrowser(browserName);
const dbPath = getCookieDbPath(browser, profile);
const db = openDb(dbPath, browser.name);
const match = getBrowserMatch(browser, profile);
const db = openDb(match.dbPath, browser.name);
try {
const now = chromiumNow();
const rows = db.query(
@@ -144,9 +161,9 @@ export async function importCookies(
if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
const browser = resolveBrowser(browserName);
const derivedKey = await getDerivedKey(browser);
const dbPath = getCookieDbPath(browser, profile);
const db = openDb(dbPath, browser.name);
const match = getBrowserMatch(browser, profile);
const derivedKeys = await getDerivedKeys(match);
const db = openDb(match.dbPath, browser.name);
try {
const now = chromiumNow();
@@ -167,7 +184,7 @@ export async function importCookies(
for (const row of rows) {
try {
const value = decryptCookieValue(row, derivedKey);
const value = decryptCookieValue(row, derivedKeys);
const cookie = toPlaywrightCookie(row, value);
cookies.push(cookie);
domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
@@ -208,17 +225,61 @@ function validateProfile(profile: string): void {
}
}
function getCookieDbPath(browser: BrowserInfo, profile: string): string {
validateProfile(profile);
const appSupport = path.join(os.homedir(), 'Library', 'Application Support');
const dbPath = path.join(appSupport, browser.dataDir, profile, 'Cookies');
if (!fs.existsSync(dbPath)) {
throw new CookieImportError(
`${browser.name} is not installed (no cookie database at ${dbPath})`,
'not_installed',
);
function getHostPlatform(): BrowserPlatform | null {
if (process.platform === 'darwin' || process.platform === 'linux') return process.platform;
return null;
}
function getSearchPlatforms(): BrowserPlatform[] {
const current = getHostPlatform();
const order: BrowserPlatform[] = [];
if (current) order.push(current);
for (const platform of ['darwin', 'linux'] as BrowserPlatform[]) {
if (!order.includes(platform)) order.push(platform);
}
return dbPath;
return order;
}
function getDataDirForPlatform(browser: BrowserInfo, platform: BrowserPlatform): string | null {
return platform === 'darwin' ? browser.dataDir : browser.linuxDataDir || null;
}
function getBaseDir(platform: BrowserPlatform): string {
return platform === 'darwin'
? path.join(os.homedir(), 'Library', 'Application Support')
: path.join(os.homedir(), '.config');
}
function findBrowserMatch(browser: BrowserInfo, profile: string): BrowserMatch | null {
validateProfile(profile);
for (const platform of getSearchPlatforms()) {
const dataDir = getDataDirForPlatform(browser, platform);
if (!dataDir) continue;
const dbPath = path.join(getBaseDir(platform), dataDir, profile, 'Cookies');
try {
if (fs.existsSync(dbPath)) {
return { browser, platform, dbPath };
}
} catch {}
}
return null;
}
function getBrowserMatch(browser: BrowserInfo, profile: string): BrowserMatch {
const match = findBrowserMatch(browser, profile);
if (match) return match;
const attempted = getSearchPlatforms()
.map(platform => {
const dataDir = getDataDirForPlatform(browser, platform);
return dataDir ? path.join(getBaseDir(platform), dataDir, profile, 'Cookies') : null;
})
.filter((entry): entry is string => entry !== null);
throw new CookieImportError(
`${browser.name} is not installed (no cookie database at ${attempted.join(' or ')})`,
'not_installed',
);
}
// ─── Internal: SQLite Access ────────────────────────────────────
@@ -273,17 +334,40 @@ function openDbFromCopy(dbPath: string, browserName: string): Database {
// ─── Internal: Keychain Access (async, 10s timeout) ─────────────
async function getDerivedKey(browser: BrowserInfo): Promise<Buffer> {
const cached = keyCache.get(browser.keychainService);
if (cached) return cached;
function deriveKey(password: string, iterations: number): Buffer {
return crypto.pbkdf2Sync(password, 'saltysalt', iterations, 16, 'sha1');
}
const password = await getKeychainPassword(browser.keychainService);
const derived = crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1');
keyCache.set(browser.keychainService, derived);
function getCachedDerivedKey(cacheKey: string, password: string, iterations: number): Buffer {
const cached = keyCache.get(cacheKey);
if (cached) return cached;
const derived = deriveKey(password, iterations);
keyCache.set(cacheKey, derived);
return derived;
}
async function getKeychainPassword(service: string): Promise<string> {
async function getDerivedKeys(match: BrowserMatch): Promise<Map<string, Buffer>> {
if (match.platform === 'darwin') {
const password = await getMacKeychainPassword(match.browser.keychainService);
return new Map([
['v10', getCachedDerivedKey(`darwin:${match.browser.keychainService}:v10`, password, 1003)],
]);
}
const keys = new Map<string, Buffer>();
keys.set('v10', getCachedDerivedKey('linux:v10', 'peanuts', 1));
const linuxPassword = await getLinuxSecretPassword(match.browser);
if (linuxPassword) {
keys.set(
'v11',
getCachedDerivedKey(`linux:${match.browser.keychainService}:v11`, linuxPassword, 1),
);
}
return keys;
}
async function getMacKeychainPassword(service: string): Promise<string> {
// Use async Bun.spawn with timeout to avoid blocking the event loop.
// macOS may show an Allow/Deny dialog that blocks until the user responds.
const proc = Bun.spawn(
@@ -341,6 +425,47 @@ async function getKeychainPassword(service: string): Promise<string> {
}
}
async function getLinuxSecretPassword(browser: BrowserInfo): Promise<string | null> {
const attempts: string[][] = [
['secret-tool', 'lookup', 'Title', browser.keychainService],
];
if (browser.linuxApplication) {
attempts.push(
['secret-tool', 'lookup', 'xdg:schema', 'chrome_libsecret_os_crypt_password_v2', 'application', browser.linuxApplication],
['secret-tool', 'lookup', 'xdg:schema', 'chrome_libsecret_os_crypt_password', 'application', browser.linuxApplication],
);
}
for (const cmd of attempts) {
const password = await runPasswordLookup(cmd, 3_000);
if (password) return password;
}
return null;
}
async function runPasswordLookup(cmd: string[], timeoutMs: number): Promise<string | null> {
try {
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' });
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => {
proc.kill();
reject(new Error('timeout'));
}, timeoutMs),
);
const exitCode = await Promise.race([proc.exited, timeout]);
const stdout = await new Response(proc.stdout).text();
if (exitCode !== 0) return null;
const password = stdout.trim();
return password.length > 0 ? password : null;
} catch {
return null;
}
}
// ─── Internal: Cookie Decryption ────────────────────────────────
interface RawCookie {
@@ -356,7 +481,7 @@ interface RawCookie {
samesite: number;
}
function decryptCookieValue(row: RawCookie, key: Buffer): string {
function decryptCookieValue(row: RawCookie, keys: Map<string, Buffer>): string {
// Prefer unencrypted value if present
if (row.value && row.value.length > 0) return row.value;
@@ -364,16 +489,15 @@ function decryptCookieValue(row: RawCookie, key: Buffer): string {
if (ev.length === 0) return '';
const prefix = ev.slice(0, 3).toString('utf-8');
if (prefix !== 'v10') {
throw new Error(`Unknown encryption prefix: ${prefix}`);
}
const key = keys.get(prefix);
if (!key) throw new Error(`No decryption key available for ${prefix} cookies`);
const ciphertext = ev.slice(3);
const iv = Buffer.alloc(16, 0x20); // 16 space characters
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
// First 32 bytes are HMAC-SHA256 authentication tag; actual value follows
// Chromium prefixes encrypted cookie payloads with 32 bytes of metadata.
if (plaintext.length <= 32) return '';
return plaintext.slice(32).toString('utf-8');
}
+2 -2
View File
@@ -6,7 +6,7 @@
*/
import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, importCookies } from './cookie-import-browser';
import { findInstalledBrowsers, importCookies, listSupportedBrowserNames } from './cookie-import-browser';
import { validateNavigationUrl } from './url-validation';
import * as fs from 'fs';
import * as path from 'path';
@@ -333,7 +333,7 @@ export async function handleWriteCommand(
const browsers = findInstalledBrowsers();
if (browsers.length === 0) {
throw new Error('No Chromium browsers found. Supported: Comet, Chrome, Arc, Brave, Edge');
throw new Error(`No Chromium browsers found. Supported: ${listSupportedBrowserNames().join(', ')}`);
}
const pickerUrl = `http://127.0.0.1:${port}/cookie-picker`;
+134 -12
View File
@@ -13,7 +13,7 @@
* Remaining bytes = actual cookie value
*/
import { describe, test, expect, beforeAll, afterAll, mock } from 'bun:test';
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
import * as crypto from 'crypto';
import * as fs from 'fs';
@@ -24,16 +24,26 @@ import * as os from 'os';
const TEST_PASSWORD = 'test-keychain-password';
const TEST_KEY = crypto.pbkdf2Sync(TEST_PASSWORD, 'saltysalt', 1003, 16, 'sha1');
const LINUX_V10_PASSWORD = 'peanuts';
const LINUX_V10_KEY = crypto.pbkdf2Sync(LINUX_V10_PASSWORD, 'saltysalt', 1, 16, 'sha1');
const LINUX_V11_PASSWORD = 'test-linux-secret';
const LINUX_V11_KEY = crypto.pbkdf2Sync(LINUX_V11_PASSWORD, 'saltysalt', 1, 16, 'sha1');
const IV = Buffer.alloc(16, 0x20);
const CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
// Fixture DB path
const FIXTURE_DIR = path.join(import.meta.dir, 'fixtures');
const FIXTURE_DB = path.join(FIXTURE_DIR, 'test-cookies.db');
const LINUX_FIXTURE_DB = path.join(FIXTURE_DIR, 'test-cookies-linux.db');
// ─── Encryption Helper ──────────────────────────────────────────
function encryptCookieValue(value: string): Buffer {
function encryptCookieValue(
value: string,
options?: { key?: Buffer; prefix?: 'v10' | 'v11' },
): Buffer {
const key = options?.key ?? TEST_KEY;
const prefix = options?.prefix ?? 'v10';
// 32-byte HMAC tag (random for test) + actual value
const hmacTag = crypto.randomBytes(32);
const plaintext = Buffer.concat([hmacTag, Buffer.from(value, 'utf-8')]);
@@ -43,12 +53,11 @@ function encryptCookieValue(value: string): Buffer {
const padLen = blockSize - (plaintext.length % blockSize);
const padded = Buffer.concat([plaintext, Buffer.alloc(padLen, padLen)]);
const cipher = crypto.createCipheriv('aes-128-cbc', TEST_KEY, IV);
const cipher = crypto.createCipheriv('aes-128-cbc', key, IV);
cipher.setAutoPadding(false); // We padded manually
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
// Prefix with "v10"
return Buffer.concat([Buffer.from('v10'), encrypted]);
return Buffer.concat([Buffer.from(prefix), encrypted]);
}
function chromiumEpoch(unixSeconds: number): bigint {
@@ -57,11 +66,11 @@ function chromiumEpoch(unixSeconds: number): bigint {
// ─── Create Fixture Database ────────────────────────────────────
function createFixtureDb() {
function createFixtureDb(dbPath: string): Database {
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
if (fs.existsSync(FIXTURE_DB)) fs.unlinkSync(FIXTURE_DB);
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
const db = new Database(FIXTURE_DB);
const db = new Database(dbPath);
db.run(`CREATE TABLE cookies (
host_key TEXT NOT NULL,
name TEXT NOT NULL,
@@ -74,7 +83,11 @@ function createFixtureDb() {
has_expires INTEGER NOT NULL DEFAULT 0,
samesite INTEGER NOT NULL DEFAULT 1
)`);
return db;
}
function createMacFixtureDb() {
const db = createFixtureDb(FIXTURE_DB);
const insert = db.prepare(`INSERT INTO cookies
(host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires, samesite)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
@@ -110,6 +123,21 @@ function createFixtureDb() {
db.close();
}
function createLinuxFixtureDb() {
const db = createFixtureDb(LINUX_FIXTURE_DB);
const insert = db.prepare(`INSERT INTO cookies
(host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires, samesite)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const futureExpiry = Number(chromiumEpoch(Math.floor(Date.now() / 1000) + 86400 * 365));
insert.run('.linux-v10.com', 'sid', '', encryptCookieValue('linux-v10-value', { key: LINUX_V10_KEY, prefix: 'v10' }), '/', futureExpiry, 1, 1, 1, 1);
insert.run('.linux-v11.com', 'auth', '', encryptCookieValue('linux-v11-value', { key: LINUX_V11_KEY, prefix: 'v11' }), '/', futureExpiry, 1, 1, 1, 1);
insert.run('.linux-plain.com', 'plain', 'plain-linux', Buffer.alloc(0), '/', futureExpiry, 0, 0, 1, 1);
db.close();
}
// ─── Mock Setup ─────────────────────────────────────────────────
// We need to mock:
// 1. The Keychain access (getKeychainPassword) to return TEST_PASSWORD
@@ -120,17 +148,18 @@ let findInstalledBrowsers: any;
let listDomains: any;
let importCookies: any;
let CookieImportError: any;
let originalSpawn: typeof Bun.spawn;
beforeAll(async () => {
createFixtureDb();
createMacFixtureDb();
createLinuxFixtureDb();
// Mock Bun.spawn to return test password for keychain access
const origSpawn = Bun.spawn;
originalSpawn = Bun.spawn;
// @ts-ignore - monkey-patching for test
Bun.spawn = function(cmd: any, opts: any) {
// Intercept security find-generic-password calls
if (Array.isArray(cmd) && cmd[0] === 'security' && cmd[1] === 'find-generic-password') {
const service = cmd[3]; // -s <service>
// Return test password for any known test service
return {
stdout: new ReadableStream({
@@ -146,8 +175,23 @@ beforeAll(async () => {
kill: () => {},
};
}
if (Array.isArray(cmd) && cmd[0] === 'secret-tool' && cmd[1] === 'lookup') {
return {
stdout: new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(LINUX_V11_PASSWORD + '\n'));
controller.close();
}
}),
stderr: new ReadableStream({
start(controller) { controller.close(); }
}),
exited: Promise.resolve(0),
kill: () => {},
};
}
// Pass through other spawn calls
return origSpawn(cmd, opts);
return originalSpawn(cmd, opts);
};
// Import the module (uses our mocked Bun.spawn)
@@ -159,8 +203,12 @@ beforeAll(async () => {
});
afterAll(() => {
// Restore Bun.spawn
// @ts-ignore - monkey-patching for test
Bun.spawn = originalSpawn;
// Clean up fixture DB
try { fs.unlinkSync(FIXTURE_DB); } catch {}
try { fs.unlinkSync(LINUX_FIXTURE_DB); } catch {}
try { fs.rmdirSync(FIXTURE_DIR); } catch {}
});
@@ -176,6 +224,35 @@ afterAll(() => {
// 2. Decrypting them with the module's decryption logic
// The actual DB path resolution is tested separately.
async function withInstalledProfile<T>(
relativeBrowserDir: string,
sourceDb: string,
run: () => Promise<T>,
profile = 'Default',
): Promise<T> {
const homeDir = os.homedir();
const profileDir = path.join(homeDir, relativeBrowserDir, profile);
const cookiesPath = path.join(profileDir, 'Cookies');
const backupPath = path.join(profileDir, `Cookies.backup-${crypto.randomUUID()}`);
const hadOriginal = fs.existsSync(cookiesPath);
fs.mkdirSync(profileDir, { recursive: true });
if (hadOriginal) fs.copyFileSync(cookiesPath, backupPath);
fs.copyFileSync(sourceDb, cookiesPath);
try {
return await run();
} finally {
if (hadOriginal) {
fs.copyFileSync(backupPath, cookiesPath);
fs.unlinkSync(backupPath);
} else {
try { fs.unlinkSync(cookiesPath); } catch {}
try { fs.rmdirSync(profileDir); } catch {}
}
}
}
// ─── Tests ──────────────────────────────────────────────────────
describe('Cookie Import Browser', () => {
@@ -351,6 +428,51 @@ describe('Cookie Import Browser', () => {
expect(b).toHaveProperty('aliases');
}
});
test('detects linux-style Chromium profiles under ~/.config', async () => {
await withInstalledProfile('.config/chromium', LINUX_FIXTURE_DB, async () => {
const browsers = findInstalledBrowsers();
const names = browsers.map((browser: any) => browser.name);
expect(names).toContain('Chromium');
});
});
});
describe('Real Profile Imports', () => {
test('imports Linux v10 cookies from ~/.config/chromium', async () => {
await withInstalledProfile('.config/chromium', LINUX_FIXTURE_DB, async () => {
const result = await importCookies('chromium', ['.linux-v10.com'], 'GstackLinuxV10');
expect(result.count).toBe(1);
expect(result.failed).toBe(0);
expect(result.cookies[0].name).toBe('sid');
expect(result.cookies[0].value).toBe('linux-v10-value');
}, 'GstackLinuxV10');
});
test('imports Linux v11 cookies when secret-tool returns a key', async () => {
await withInstalledProfile('.config/chromium', LINUX_FIXTURE_DB, async () => {
const result = await importCookies('chromium', ['.linux-v11.com'], 'GstackLinuxV11');
expect(result.count).toBe(1);
expect(result.failed).toBe(0);
expect(result.cookies[0].name).toBe('auth');
expect(result.cookies[0].value).toBe('linux-v11-value');
}, 'GstackLinuxV11');
});
test('lists domains from Linux Chromium profiles', async () => {
await withInstalledProfile('.config/chromium', LINUX_FIXTURE_DB, async () => {
const result = listDomains('chromium', 'GstackLinuxDomains');
const domains = result.domains.map((entry: any) => entry.domain);
expect(result.browser).toBe('Chromium');
expect(domains).toContain('.linux-v10.com');
expect(domains).toContain('.linux-v11.com');
expect(domains).toContain('.linux-plain.com');
}, 'GstackLinuxDomains');
});
});
describe('Corrupt Data Handling', () => {