Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1e1239e
Initial commit with task details
konard Aug 2, 2026
875d023
test(js): define installed cookie import behavior
konard Aug 2, 2026
a23c0fa
feat(js): import cookies from installed browsers
konard Aug 2, 2026
c25f592
test(python): define installed cookie import behavior
konard Aug 2, 2026
83d6116
feat(python): import cookies from installed browsers
konard Aug 2, 2026
a3f7066
test(rust): define installed cookie import behavior
konard Aug 2, 2026
86683db
feat(rust): import cookies from installed browsers
konard Aug 2, 2026
923688d
fix(cache): coordinate credential reads across runtimes
konard Aug 2, 2026
b0f1463
Revert "Initial commit with task details"
konard Aug 2, 2026
cdfd73f
Merge remote-tracking branch 'origin/main' into issue-69-d8df3ab30de1
konard Aug 2, 2026
e57e0e9
test(cache): reproduce in-process credential expiry
konard Aug 2, 2026
b66184d
fix(cache): expire in-process credentials by ttl
konard Aug 2, 2026
6382ecf
style(python): sort cookie cache test imports
konard Aug 2, 2026
54ee7fc
test(cookies): cover credential refresh edge cases
konard Aug 2, 2026
6959e3c
fix(cookies): reuse refreshed platform credentials
konard Aug 2, 2026
e3cbb13
docs(python): clarify imported cookie shape
konard Aug 2, 2026
93fa3d2
test(cookies): isolate partial result caches
konard Aug 2, 2026
d26e14d
fix(cookies): separate strict and partial result caches
konard Aug 2, 2026
56f3559
Merge remote-tracking branch 'origin/main' into issue-69-d8df3ab30de1
konard Aug 2, 2026
000ecde
fix(js): support sqlite with install scripts disabled
konard Aug 2, 2026
abb6505
fix(cache): enforce owner-only Windows ACLs
konard Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ safe, dedicated automation profile before attaching. Use
`launchRealBrowser()` in JavaScript and `launch_real_browser()` in Python or
Rust.

All implementations also expose installed-browser profile discovery and local
cookie import for Chrome, Edge, Brave, Chromium, and Firefox. Cookie values are
returned in the automation-engine shape and cached locally with owner-only
permissions so platform credential stores are touched at most once per TTL.

## Core Concept: Page State Machine

Browser Commander manages the browser as a state machine with two states:
Expand Down
24 changes: 24 additions & 0 deletions docs/feature-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ This matrix tracks the shared API surface across the maintained language impleme
| Seed cookies after connection | Supported | Supported | Supported |
| Return browser and page handles | Raw engine handles | Shared `EngineAdapter` | Raw engine handles |

## Installed-Browser Cookie Parity

| Capability | JavaScript | Rust | Python |
| ----------------------------------------------------------------- | ---------- | --------- | --------- |
| Discover Chrome, Edge, Brave, Chromium, and Firefox profiles | Supported | Supported | Supported |
| Read Playwright-compatible cookie fields | Supported | Supported | Supported |
| Filter by domain and choose a named profile | Supported | Supported | Supported |
| Chromium version-24 host-hash validation and timestamp conversion | Supported | Supported | Supported |
| macOS Keychain + AES-128-CBC | Supported | Supported | Supported |
| Linux libsecret/KWallet + AES-128-CBC | Supported | Supported | Supported |
| Windows DPAPI key + legacy AES-256-GCM | Supported | Supported | Supported |
| Firefox `cookies.sqlite` | Supported | Supported | Supported |
| Owner-only derived-key/result cache with TTL and refresh controls | Supported | Supported | Supported |
| Cross-process credential-read lock | Supported | Supported | Supported |

Current Windows Chromium app-bound `v20` values require Chromium's privileged
service and are intentionally reported as unsupported for ordinary external
processes. All three APIs can skip those individual values when partial import
is acceptable. The shared derived-key cache uses one schema and lock identity,
so JavaScript, Rust, and Python processes do not independently prompt within a
TTL window. Cache directories/files use `0700`/`0600` modes on POSIX; on
Windows they remove inherited ACL entries and grant access only to the current
user.

## Automation-Friendly Launch Defaults

`launchBrowser()`/`launch_browser()` and the real-browser launch helpers add
Expand Down
74 changes: 74 additions & 0 deletions experiments/cookie-cache-parity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import { execFile as execFileCallback } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";

import { getCachedCredential } from "../js/src/browser/browser-cookie-cache.js";

const execFile = promisify(execFileCallback);
const repositoryRoot = path.dirname(
path.dirname(fileURLToPath(import.meta.url)),
);
const temporaryDirectory = await mkdtemp(
path.join(os.tmpdir(), "browser-commander-cookie-cache-parity-"),
);
const expectedKey = Buffer.alloc(16, 7);

try {
await getCachedCredential({
cache: {
enabled: true,
dir: temporaryDirectory,
ttlSeconds: 60,
},
identity: "chrome:linux:safe-storage",
refresh: false,
metadata: {
browser: "chrome",
platform: "linux",
source: "safe-storage",
},
create: async () => expectedKey,
});

const pythonSource = `
from pathlib import Path
import sys

from browser_commander.browser.browser_cookie_cache import (
NormalizedCookieCache,
get_cached_credential,
)

cache = NormalizedCookieCache(True, Path(sys.argv[1]), 60.0)
key = get_cached_credential(
cache,
"chrome:linux:safe-storage",
lambda: (_ for _ in ()).throw(RuntimeError("credential provider was called")),
refresh=False,
metadata={},
)
assert key == bytes([7]) * 16
`;
const pythonPath = path.join(repositoryRoot, "python", "src");
await execFile(
process.env.PYTHON ?? "python",
["-c", pythonSource, temporaryDirectory],
{
env: {
...process.env,
PYTHONPATH: [pythonPath, process.env.PYTHONPATH]
.filter(Boolean)
.join(path.delimiter),
},
},
);

assert.ok(true, "Python reused the JavaScript-derived credential cache");
console.log("JavaScript/Python cookie credential cache parity passed");
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
5 changes: 5 additions & 0 deletions js/.changeset/installed-browser-cookies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'browser-commander': minor
---

Add installed-browser profile discovery and privacy-preserving cookie import for Chrome, Edge, Brave, Chromium, and Firefox, including OS credential caching and Playwright/Puppeteer-compatible output.
79 changes: 77 additions & 2 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,41 @@ const connection = await launchRealBrowser({
await connection.browser.close();
```

Cookie seeding copies only cookies you explicitly provide; the helper does not
read, decrypt, or expose cookies from a browser's default profile.
Cookie seeding copies only cookies you explicitly provide. To seed a dedicated
profile from one of your installed browser profiles, use the explicit local
cookie-import helper:

```javascript
import {
launchAndConnectRealBrowser,
listBrowserProfiles,
readBrowserCookies,
} from 'browser-commander';

console.log(await listBrowserProfiles({ browser: 'chrome' }));

const cookies = await readBrowserCookies({
browser: 'chrome', // chrome, edge, brave, chromium, or firefox
profile: 'Default',
domainFilter: 'example.com',
cache: { ttlMinutes: 60 },
});

const connection = await launchAndConnectRealBrowser({
engine: 'playwright',
channel: 'chrome',
userDataDir: '/tmp/my-dedicated-profile',
seedCookies: cookies,
});
```

The import stays on the local machine and runs only when called. It never sends
cookie data anywhere. Decrypted result and derived-key cache files are stored
under `~/.browser-commander/cookie-cache/` with owner-only permissions. The
default 60-minute TTL and a cross-process lock ensure that concurrent or repeated
scripts touch Keychain, libsecret/KWallet, or DPAPI at most once per TTL window.
Set `refresh: true` to force a new read, customize `cache.dir`/`ttlMinutes`, or
set `cache: false` to opt out of disk caching.
`launchAndConnectRealBrowser()` remains available as a descriptive alias.
Remote-debugging, loopback, and profile arguments remain managed even when all
optional defaults are ignored; `headless: true` adds `--headless=new`.
Expand Down Expand Up @@ -391,6 +424,48 @@ and profile path alongside `{ browser, page }`.
of defaults to omit (or `true` to omit all optional defaults). The older
`args` append option remains supported.

### listBrowserProfiles(options)

Discover cookie-bearing profiles for Chrome, Edge, Brave, Chromium, and Firefox.
Pass an optional `browser` to narrow discovery. Each result contains
`{ browser, name, displayName, path, isDefault }`.

### readBrowserCookies(options)

Read cookies from an installed browser profile and return the exact
`{ name, value, domain, path, expires, httpOnly, secure, sameSite }` shape used by
Playwright and Puppeteer:

```javascript
const cookies = await readBrowserCookies({
browser: 'firefox',
profile: 'default-release', // optional; the default profile is selected first
domainFilter: 'example.com', // optional substring match
cache: { dir: './private-cookie-cache', ttlMinutes: 30 },
refresh: false,
});
```

Platform support:

| Browser family | macOS | Linux | Windows |
| ----------------------------- | ------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------- |
| Chrome, Edge, Brave, Chromium | Keychain + AES-128-CBC (`v10`/`v11`) | libsecret/KWallet + AES-128-CBC (`v11`), or the Chromium `v10` fallback key | DPAPI-protected AES-256-GCM key (`v10`/`v11`) |
| Firefox | `cookies.sqlite` | `cookies.sqlite` | `cookies.sqlite` |

Firefox cookie values are stored directly in its local cookie database. Chromium
database version 24 domain hashes and Chrome's 1601-based timestamps are handled
automatically. Current Windows Chromium can use app-bound `v20` encryption,
which intentionally requires the browser's privileged elevation service and
cannot be decrypted by an ordinary external process. The helper reports that
boundary instead of bypassing it; use a browser-supported export or an existing
Browser Commander storage-state file for those cookies. Set
`ignoreDecryptionErrors: true` only when returning the remaining decryptable
cookies is acceptable.

Treat imported cookies like passwords: keep cache directories private, use a
short TTL, never commit them, and seed only a dedicated automation profile.

### saveStorageState(page, filePath)

Save the current cookies and localStorage in Playwright's portable storage
Expand Down
Loading
Loading