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:
co-authored by
Claude Opus 4.8
parent
3cf0b1eaa8
commit
4a7ee5773f
@@ -87,23 +87,22 @@ This checks token presence, format, expiry, directory permissions, disk space, a
|
|||||||
|
|
||||||
Session tokens are how your browser stays logged in. This tool uses them to access your chat history on your behalf.
|
Session tokens are how your browser stays logged in. This tool uses them to access your chat history on your behalf.
|
||||||
|
|
||||||
### The fast way: extract from your browser
|
Tokens are entered manually — copy them from your browser's DevTools and run the wizard:
|
||||||
|
|
||||||
If the browser you're logged in with runs on the same machine as this tool:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ai-chat-exporter auth --from-browser # Brave (default)
|
ai-chat-exporter auth
|
||||||
ai-chat-exporter auth --from-browser firefox # or chrome, chromium, edge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This reads the session cookies straight from the browser's cookie store (decrypted via the OS keyring/DPAPI/Keychain), validates each token against the live API, and writes `.env` — no DevTools, no copy-pasting. A token that fails validation never overwrites a working one. If extraction fails (browser not installed here, cookie DB locked, logged out), fall back to the manual flow below.
|
The wizard detects your OS, shows the correct DevTools shortcut, and writes the values to `.env` without echoing them to the terminal.
|
||||||
|
|
||||||
|
> **Why no auto-extraction from the browser?** Modern Chromium browsers (Chrome 127+, current Brave) encrypt cookies on Windows with **App-Bound Encryption**: the key is bound to the browser through a SYSTEM-level service, so reading cookies off disk requires Administrator rights and a PsExec-style SYSTEM impersonation that antivirus flags as credential theft — and it still fails on Brave specifically. There is no reliable, non-invasive way to pull these cookies automatically, so the manual DevTools flow below is the supported path.
|
||||||
|
|
||||||
### Token Lifetimes
|
### Token Lifetimes
|
||||||
|
|
||||||
| Provider | Cookie Name | Lifetime | Expiry Detection |
|
| Provider | Cookie Name | Lifetime | Expiry Detection |
|
||||||
|----------|-------------|----------|-----------------|
|
|----------|-------------|----------|-----------------|
|
||||||
| ChatGPT | `__Secure-next-auth.session-token.0` + `.1` | ~7 days | JWT `exp` claim (decoded automatically) |
|
| ChatGPT | `__Secure-next-auth.session-token.0` + `.1` | refresh ~weekly | `error` field of `/api/auth/session` — `doctor` reports "ChatGPT token active". The token is an encrypted JWE, so its `exp` is **not** readable client-side, and the `expires` field is a misleading rolling window; the `error` (`RefreshAccessTokenError` when dead) is the honest signal. |
|
||||||
| Claude | `sessionKey` | ~30 days | Only detectable via 401 response |
|
| Claude | `sessionKey` | ~30 days | Opaque token — only detectable via 401 response |
|
||||||
|
|
||||||
### Finding Tokens in Chrome DevTools
|
### Finding Tokens in Chrome DevTools
|
||||||
|
|
||||||
@@ -285,10 +284,9 @@ Each provider+project combination maps to a flat Joplin notebook created automat
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
ai-chat-exporter auth # manual wizard (DevTools flow)
|
ai-chat-exporter auth # manual wizard (DevTools flow)
|
||||||
ai-chat-exporter auth --from-browser # extract from Brave's cookie store
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Guided wizard to find and save session tokens and ChatGPT project IDs. Detects OS and shows the correct DevTools shortcut. `--from-browser [brave|chrome|chromium|edge|firefox]` skips the wizard and pulls tokens directly from a local browser, validating them live before writing `.env`.
|
Guided wizard to find and save session tokens and ChatGPT project IDs. Detects OS and shows the correct DevTools shortcut, then writes the values to `.env`.
|
||||||
|
|
||||||
### `doctor` — Health check
|
### `doctor` — Health check
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ dependencies = [
|
|||||||
"rich==13.7.1",
|
"rich==13.7.1",
|
||||||
"python-slugify==8.0.4",
|
"python-slugify==8.0.4",
|
||||||
"PyJWT==2.8.0",
|
"PyJWT==2.8.0",
|
||||||
"browser-cookie3==0.20.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ setuptools==82.0.0
|
|||||||
text-unidecode==1.3
|
text-unidecode==1.3
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
wheel==0.46.3
|
wheel==0.46.3
|
||||||
browser-cookie3==0.20.1
|
|
||||||
|
|||||||
@@ -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
@@ -117,37 +117,17 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, no_log_file
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@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
|
@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.
|
"""Interactive setup wizard for session tokens.
|
||||||
|
|
||||||
Guides you through finding and saving your ChatGPT and Claude session
|
Guides you through finding and saving your ChatGPT and Claude session
|
||||||
tokens. Tokens are never echoed to the terminal. With --from-browser,
|
tokens. Tokens are never echoed to the terminal.
|
||||||
tokens are pulled from the browser's cookie store and written to .env
|
|
||||||
without any copy-pasting.
|
|
||||||
|
|
||||||
Token lifetimes:
|
Token lifetimes:
|
||||||
ChatGPT (__Secure-next-auth.session-token): ~7 days (JWT)
|
ChatGPT (__Secure-next-auth.session-token): ~7 days (JWT)
|
||||||
Claude (sessionKey): ~30 days (opaque string)
|
Claude (sessionKey): ~30 days (opaque string)
|
||||||
"""
|
"""
|
||||||
if from_browser:
|
|
||||||
_auth_from_browser(from_browser.lower())
|
|
||||||
return
|
|
||||||
|
|
||||||
os_name = platform.system()
|
os_name = platform.system()
|
||||||
|
|
||||||
console.print("\n[bold cyan]AI Chat Exporter — Token Setup Wizard[/bold cyan]\n")
|
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)
|
_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:
|
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."""
|
"""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):
|
if click.confirm(f"Write {key} to .env?", default=True):
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
"""Unit tests for browser cookie extraction (browser_cookie3 mocked)."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from src.browser_tokens import (
|
|
||||||
BrowserTokenError,
|
|
||||||
extract_chatgpt_tokens,
|
|
||||||
extract_claude_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _fake_bc3(monkeypatch, cookies_by_domain, raises=None):
|
|
||||||
"""Install a stub browser_cookie3 module whose ``brave`` loader returns
|
|
||||||
SimpleNamespace cookies for the requested domain (or raises)."""
|
|
||||||
module = types.ModuleType("browser_cookie3")
|
|
||||||
|
|
||||||
def brave(domain_name=""):
|
|
||||||
if raises is not None:
|
|
||||||
raise raises
|
|
||||||
return [
|
|
||||||
SimpleNamespace(name=name, value=value)
|
|
||||||
for name, value in cookies_by_domain.get(domain_name, {}).items()
|
|
||||||
]
|
|
||||||
|
|
||||||
module.brave = brave
|
|
||||||
monkeypatch.setitem(sys.modules, "browser_cookie3", module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
class TestExtractChatGPT:
|
|
||||||
def test_chunked_cookies(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {
|
|
||||||
"chatgpt.com": {
|
|
||||||
"__Secure-next-auth.session-token.0": "eyJpart0",
|
|
||||||
"__Secure-next-auth.session-token.1": "part1rest",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
assert extract_chatgpt_tokens("brave") == ("eyJpart0", "part1rest")
|
|
||||||
|
|
||||||
def test_single_cookie_fallback(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {
|
|
||||||
"chatgpt.com": {"__Secure-next-auth.session-token": "eyJsingle"},
|
|
||||||
})
|
|
||||||
assert extract_chatgpt_tokens("brave") == ("eyJsingle", None)
|
|
||||||
|
|
||||||
def test_missing_cookie_raises_with_login_hint(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {"chatgpt.com": {"unrelated": "x"}})
|
|
||||||
with pytest.raises(BrowserTokenError, match="log in"):
|
|
||||||
extract_chatgpt_tokens("brave")
|
|
||||||
|
|
||||||
def test_loader_failure_wrapped(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {}, raises=RuntimeError("database is locked"))
|
|
||||||
with pytest.raises(BrowserTokenError, match="Close the browser"):
|
|
||||||
extract_chatgpt_tokens("brave")
|
|
||||||
|
|
||||||
def test_unsupported_browser_rejected(self):
|
|
||||||
with pytest.raises(BrowserTokenError, match="Unsupported browser"):
|
|
||||||
extract_chatgpt_tokens("netscape")
|
|
||||||
|
|
||||||
|
|
||||||
class TestExtractClaude:
|
|
||||||
def test_session_key(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {"claude.ai": {"sessionKey": "sk-ant-sid01-xyz"}})
|
|
||||||
assert extract_claude_session("brave") == "sk-ant-sid01-xyz"
|
|
||||||
|
|
||||||
def test_missing_raises(self, monkeypatch):
|
|
||||||
_fake_bc3(monkeypatch, {"claude.ai": {}})
|
|
||||||
with pytest.raises(BrowserTokenError, match="log in"):
|
|
||||||
extract_claude_session("brave")
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuthFromBrowserCLI:
|
|
||||||
def test_writes_env_after_validation(self, monkeypatch, tmp_path):
|
|
||||||
"""--from-browser extracts, validates live (mocked), and writes .env."""
|
|
||||||
from click.testing import CliRunner
|
|
||||||
from src.main import cli
|
|
||||||
from src.cache import Cache
|
|
||||||
|
|
||||||
cache = Cache(tmp_path / "cache")
|
|
||||||
cache.acknowledge_tos()
|
|
||||||
|
|
||||||
_fake_bc3(monkeypatch, {
|
|
||||||
"chatgpt.com": {
|
|
||||||
"__Secure-next-auth.session-token.0": "eyJtok0",
|
|
||||||
"__Secure-next-auth.session-token.1": "tok1",
|
|
||||||
},
|
|
||||||
"claude.ai": {"sessionKey": "sk-ant-sid01-abc"},
|
|
||||||
})
|
|
||||||
|
|
||||||
# Stub out the live validation calls.
|
|
||||||
import src.providers.chatgpt as chatgpt_mod
|
|
||||||
import src.providers.claude as claude_mod
|
|
||||||
monkeypatch.setattr(
|
|
||||||
chatgpt_mod.ChatGPTProvider, "_fetch_access_token", lambda self: "at"
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
claude_mod.ClaudeProvider,
|
|
||||||
"list_conversations",
|
|
||||||
lambda self, offset=0, limit=100: [],
|
|
||||||
)
|
|
||||||
|
|
||||||
runner = CliRunner(mix_stderr=True)
|
|
||||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
||||||
result = runner.invoke(
|
|
||||||
cli,
|
|
||||||
["--no-log-file", "auth", "--from-browser"],
|
|
||||||
env={"CACHE_DIR": str(tmp_path / "cache")},
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
from pathlib import Path
|
|
||||||
env_text = Path(".env").read_text()
|
|
||||||
|
|
||||||
assert "CHATGPT_SESSION_TOKEN=eyJtok0" in env_text
|
|
||||||
assert "CHATGPT_SESSION_TOKEN_1=tok1" in env_text
|
|
||||||
assert "CLAUDE_SESSION_KEY=sk-ant-sid01-abc" in env_text
|
|
||||||
assert "2 provider(s) configured" in result.output
|
|
||||||
|
|
||||||
def test_validation_failure_leaves_env_untouched(self, monkeypatch, tmp_path):
|
|
||||||
from click.testing import CliRunner
|
|
||||||
from src.main import cli
|
|
||||||
from src.cache import Cache
|
|
||||||
from src.providers.base import ProviderError
|
|
||||||
|
|
||||||
cache = Cache(tmp_path / "cache")
|
|
||||||
cache.acknowledge_tos()
|
|
||||||
|
|
||||||
_fake_bc3(monkeypatch, {
|
|
||||||
"chatgpt.com": {"__Secure-next-auth.session-token.0": "eyJstale"},
|
|
||||||
"claude.ai": {},
|
|
||||||
})
|
|
||||||
|
|
||||||
import src.providers.chatgpt as chatgpt_mod
|
|
||||||
|
|
||||||
def _fail(self):
|
|
||||||
raise ProviderError("chatgpt", "auth", RuntimeError("401"))
|
|
||||||
|
|
||||||
monkeypatch.setattr(chatgpt_mod.ChatGPTProvider, "_fetch_access_token", _fail)
|
|
||||||
|
|
||||||
runner = CliRunner(mix_stderr=True)
|
|
||||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
||||||
result = runner.invoke(
|
|
||||||
cli,
|
|
||||||
["--no-log-file", "auth", "--from-browser", "brave"],
|
|
||||||
env={"CACHE_DIR": str(tmp_path / "cache")},
|
|
||||||
)
|
|
||||||
import pathlib
|
|
||||||
env_exists = pathlib.Path(".env").exists()
|
|
||||||
|
|
||||||
assert result.exit_code == 1
|
|
||||||
assert "failed live validation" in result.output
|
|
||||||
assert not env_exists
|
|
||||||
Reference in New Issue
Block a user