feat(cookies): cross-profile cookies export / cookies transfer

Transferring a logged-in session between Chrome profiles previously needed
an ad-hoc external script to decrypt the source profile's cookie store. Make
it first-class:

- `cookies export --from <profile> [--domain <d>[,<d>]]` decrypts another
  profile's on-disk cookies and prints CDP-shaped JSON for `cookies set --curl`.
- `cookies transfer --from <profile> [--domain <d>]` exports + injects into
  the connected browser in one shot (reuses the cookies_set path).

Source profile is resolved by directory name, display name, or "auto". The
store is copied to a temp file (immune to a running Chrome's lock/WAL), read
via sqlite3, and values are decrypted (macOS v10: AES-128-CBC, key from the
shared 'Chrome Safe Storage' Keychain entry). httpOnly/secure/per-domain
auth cookies round-trip intact; SameSite=None without Secure is downgraded
so CDP accepts it. macOS only for now (clear error elsewhere).
This commit is contained in:
leeguooooo
2026-06-12 13:59:37 +09:00
parent 7ba82bc6cc
commit f76ed1ddf5
6 changed files with 484 additions and 2 deletions
+45
View File
@@ -1289,6 +1289,51 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
"cookies" => {
let op = rest.first().unwrap_or(&"get");
match *op {
"transfer" => {
// Copy a logged-in session between Chrome profiles: decrypt
// the SOURCE profile's on-disk cookie store and inject the
// cookies into the active (connected) session — no CDP access
// to the source, no Chrome restart.
// cookies transfer --from <profile> [--domain <d>[,<d>]]
// `--from` wins; otherwise the global `--profile` is used.
let from = rest
.iter()
.position(|a| *a == "--from")
.and_then(|i| rest.get(i + 1).copied())
.or(flags.profile.as_deref());
let from = from.ok_or_else(|| ParseError::MissingArguments {
context: "cookies transfer".to_string(),
usage: "cookies transfer --from <profile> [--domain <domain>[,<domain>]]",
})?;
let domain = rest
.iter()
.position(|a| *a == "--domain")
.and_then(|i| rest.get(i + 1).copied());
let cookies =
crate::cookie_export::export_cookies(from, domain).map_err(|e| {
ParseError::InvalidValue {
message: format!("cookies transfer: {}", e),
usage: "cookies transfer --from <profile> [--domain <domain>]",
}
})?;
if cookies.is_empty() {
return Err(ParseError::InvalidValue {
message: format!(
"cookies transfer: no cookies found in profile \"{}\"{}",
from,
domain
.map(|d| format!(" for domain {}", d))
.unwrap_or_default()
),
usage: "cookies transfer --from <profile> [--domain <domain>]",
});
}
return Ok(json!({
"id": id,
"action": "cookies_set",
"cookies": cookies,
}));
}
"set" => {
// --curl <file> mode: import cookies from a JSON array,
// raw cURL dump, or bare Cookie header. Scoped to the