Remove auth --from-browser browser cookie extraction

Browser cookie auto-extraction is not viable: modern Chromium App-Bound
Encryption (Chrome 127+/current Brave) keys cookies off a SYSTEM-level
layer that cannot be decrypted off disk without admin rights and AV-flagged
SYSTEM impersonation, and fails on Brave specifically. Drop the
`--from-browser` flag, `_auth_from_browser`, `src/browser_tokens.py`, its
test, and the browser-cookie3 dependency. Auth is manual (DevTools wizard)
only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
JesseMarkowitz
2026-06-28 01:35:42 -04:00
co-authored by Claude Opus 4.8
parent 3cf0b1eaa8
commit 4a7ee5773f
6 changed files with 10 additions and 366 deletions
-101
View File
@@ -1,101 +0,0 @@
"""Extract session tokens directly from a local browser's cookie store.
Default browser is Brave. Uses browser-cookie3, which handles the
platform-specific cookie decryption (Linux: AES key derived from the OS
keyring "Safe Storage" secret; Windows: DPAPI; macOS: Keychain). The browser
must be the one the user is actually logged in with, on the same machine.
Failure modes worth knowing:
- Browser not installed / profile not found → BrowserTokenError
- Cookie DB locked (browser running with exclusive lock; rare on Linux,
common on Windows) → BrowserTokenError suggesting the browser be closed
- Logged out / cookie expired → BrowserTokenError naming the missing cookie
"""
import logging
logger = logging.getLogger(__name__)
DEFAULT_BROWSER = "brave"
SUPPORTED_BROWSERS = ("brave", "chrome", "chromium", "edge", "firefox")
# ChatGPT splits large session tokens across two cookies to stay under the
# browser's 4KB cookie limit; small tokens use the unsuffixed name.
_CHATGPT_COOKIE_0 = "__Secure-next-auth.session-token.0"
_CHATGPT_COOKIE_1 = "__Secure-next-auth.session-token.1"
_CHATGPT_COOKIE_SINGLE = "__Secure-next-auth.session-token"
_CLAUDE_COOKIE = "sessionKey"
class BrowserTokenError(Exception):
"""Raised when tokens cannot be extracted from the browser profile."""
def _load_cookies(browser: str, domain: str) -> dict[str, str]:
"""Return {cookie_name: value} for ``domain`` from the given browser."""
if browser not in SUPPORTED_BROWSERS:
raise BrowserTokenError(
f"Unsupported browser {browser!r}. "
f"Supported: {', '.join(SUPPORTED_BROWSERS)}"
)
import browser_cookie3
loader = getattr(browser_cookie3, browser)
try:
jar = loader(domain_name=domain)
except Exception as e:
# browser_cookie3 raises BrowserCookieError plus assorted OS/keyring
# errors. All mean the same thing to the caller: fall back to manual.
hint = ""
text = str(e).lower()
if "lock" in text or "database is locked" in text:
hint = " Close the browser and try again."
elif "not find" in text or "no such file" in text:
hint = " Is the browser installed on this machine?"
raise BrowserTokenError(
f"Could not read {browser} cookies for {domain}: {e}.{hint}"
) from e
return {cookie.name: cookie.value or "" for cookie in jar}
def extract_chatgpt_tokens(browser: str = DEFAULT_BROWSER) -> tuple[str, str | None]:
"""Return (session_token_0, session_token_1_or_None) from the browser.
Raises BrowserTokenError when no session cookie is present (not logged in,
or logged out since the last visit).
"""
cookies = _load_cookies(browser, "chatgpt.com")
token_0 = cookies.get(_CHATGPT_COOKIE_0, "").strip()
token_1 = cookies.get(_CHATGPT_COOKIE_1, "").strip() or None
if token_0:
logger.info(
"[browser-tokens] ChatGPT session cookies found in %s (chunked: %s)",
browser,
"yes" if token_1 else "no",
)
return token_0, token_1
single = cookies.get(_CHATGPT_COOKIE_SINGLE, "").strip()
if single:
logger.info("[browser-tokens] ChatGPT session cookie found in %s (single)", browser)
return single, None
raise BrowserTokenError(
f"No ChatGPT session cookie in {browser} — open https://chatgpt.com "
"in that browser and log in, then retry."
)
def extract_claude_session(browser: str = DEFAULT_BROWSER) -> str:
"""Return the Claude ``sessionKey`` cookie value from the browser."""
cookies = _load_cookies(browser, "claude.ai")
session_key = cookies.get(_CLAUDE_COOKIE, "").strip()
if session_key:
logger.info("[browser-tokens] Claude sessionKey found in %s", browser)
return session_key
raise BrowserTokenError(
f"No Claude sessionKey cookie in {browser} — open https://claude.ai "
"in that browser and log in, then retry."
)
+2 -98
View File
@@ -117,37 +117,17 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, no_log_file
@cli.command()
@click.option(
"--from-browser",
"from_browser",
is_flag=False,
flag_value="brave",
default=None,
metavar="[BROWSER]",
help=(
"Extract tokens straight from a local browser's cookie store instead "
"of the manual DevTools flow. Defaults to brave when no browser is "
"named; also supports chrome, chromium, edge, firefox. The browser "
"must be installed and logged in on this machine."
),
)
@click.pass_context
def auth(ctx: click.Context, from_browser: str | None) -> None:
def auth(ctx: click.Context) -> None:
"""Interactive setup wizard for session tokens.
Guides you through finding and saving your ChatGPT and Claude session
tokens. Tokens are never echoed to the terminal. With --from-browser,
tokens are pulled from the browser's cookie store and written to .env
without any copy-pasting.
tokens. Tokens are never echoed to the terminal.
Token lifetimes:
ChatGPT (__Secure-next-auth.session-token): ~7 days (JWT)
Claude (sessionKey): ~30 days (opaque string)
"""
if from_browser:
_auth_from_browser(from_browser.lower())
return
os_name = platform.system()
console.print("\n[bold cyan]AI Chat Exporter — Token Setup Wizard[/bold cyan]\n")
@@ -307,82 +287,6 @@ def _auth_claude(os_name: str) -> None:
_write_token_to_env("CLAUDE_SESSION_KEY", key)
def _auth_from_browser(browser: str) -> None:
"""Extract ChatGPT + Claude tokens from a local browser and write .env.
Each provider is validated against its live API before anything is
written, so a stale cookie (logged out since last visit) never replaces
a working token. Exits 1 only when nothing could be configured.
"""
from src.browser_tokens import (
BrowserTokenError,
SUPPORTED_BROWSERS,
extract_chatgpt_tokens,
extract_claude_session,
)
if browser not in SUPPORTED_BROWSERS:
err_console.print(
f"[red]Unsupported browser '{browser}'. "
f"Supported: {', '.join(SUPPORTED_BROWSERS)}[/red]"
)
sys.exit(1)
console.print(f"\n[bold cyan]Extracting session tokens from {browser}…[/bold cyan]\n")
configured = 0
# ── ChatGPT ──────────────────────────────────────────────────────────
try:
token, token_1 = extract_chatgpt_tokens(browser)
with console.status("[dim]Validating ChatGPT token…[/dim]"):
from src.providers.chatgpt import ChatGPTProvider
prov = ChatGPTProvider(session_token=token, session_token_1=token_1)
prov._fetch_access_token()
_set_env_key("CHATGPT_SESSION_TOKEN", token)
_set_env_key("CHATGPT_SESSION_TOKEN_1", token_1 or "")
console.print("[green]ChatGPT: token extracted, validated, and saved.[/green]")
configured += 1
except BrowserTokenError as e:
console.print(f"[yellow]ChatGPT: {e}[/yellow]")
except ProviderError as e:
console.print(
f"[yellow]ChatGPT: extracted cookie failed live validation "
f"({e.original}) — .env not changed. Log in to chatgpt.com in "
f"{browser} and retry.[/yellow]"
)
# ── Claude ───────────────────────────────────────────────────────────
try:
session_key = extract_claude_session(browser)
with console.status("[dim]Validating Claude session key…[/dim]"):
from src.providers.claude import ClaudeProvider
prov = ClaudeProvider(session_key)
prov.list_conversations(offset=0, limit=1)
_set_env_key("CLAUDE_SESSION_KEY", session_key)
console.print("[green]Claude: session key extracted, validated, and saved.[/green]")
configured += 1
except BrowserTokenError as e:
console.print(f"[yellow]Claude: {e}[/yellow]")
except ProviderError as e:
console.print(
f"[yellow]Claude: extracted cookie failed live validation "
f"({e.original}) — .env not changed. Log in to claude.ai in "
f"{browser} and retry.[/yellow]"
)
if configured:
console.print(
f"\n[green]Done — {configured} provider(s) configured. "
"Run 'ai-chat-exporter doctor' to verify.[/green]"
)
else:
err_console.print(
"\n[red]No tokens could be extracted. Use the manual wizard "
"instead: ai-chat-exporter auth[/red]"
)
sys.exit(1)
def _write_token_to_env(key: str, value: str) -> None:
"""Write or update a key in .env, offering to create the file if it doesn't exist."""
if click.confirm(f"Write {key} to .env?", default=True):