Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 36 additions & 1 deletion src/notebooklm_tools/utils/cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,11 @@ def run_headless_auth(

# Check if profile exists with saved login
if not has_chrome_profile(profile_name):
if raise_on_error:
raise AuthenticationError(
message=f"No saved browser profile is available for '{profile_name}'",
hint="Run 'nlm login' once in a desktop session to create and sign in the managed browser profile.",
)
return None

chrome_process: subprocess.Popen | None = None
Expand Down Expand Up @@ -2017,10 +2022,20 @@ def run_headless_auth(
# Find or create NotebookLM page
page = find_or_create_notebooklm_page(port)
if not page:
if raise_on_error:
raise AuthenticationError(
message="Could not open NotebookLM in the headless authentication browser",
hint="Run 'nlm login' in a desktop session to refresh the managed browser profile.",
)
return None

ws_url = _normalize_ws_url(page.get("webSocketDebuggerUrl"))
if not ws_url:
if raise_on_error:
raise AuthenticationError(
message="NotebookLM page did not expose a usable DevTools websocket",
hint="Run 'nlm login' in a desktop session to refresh the managed browser profile.",
)
return None

# Poll for login completion (navigation is async)
Expand All @@ -2037,18 +2052,33 @@ def run_headless_auth(
time.sleep(1)

if not logged_in:
# Not logged in - headless can't help
# Not logged in - headless can't help without a prior visible sign-in.
if raise_on_error:
raise AuthenticationError(
message=f"Saved browser profile '{profile_name}' is not signed in to NotebookLM",
hint="Run 'nlm login' in a desktop session once, then headless refresh can reuse that managed browser session.",
)
return None

# Wait for full page load
html, ready = _wait_for_page_ready(ws_url, timeout=timeout)
if not ready:
if raise_on_error:
raise AuthenticationError(
message="NotebookLM did not finish loading in the headless authentication browser",
hint="Run 'nlm login' in a desktop session to refresh the managed browser profile.",
)
return None

# Keep the raw list so per-domain values survive profile storage.
cookies_list = get_page_cookies(ws_url)

if not validate_cookies(cookies_list):
if raise_on_error:
raise AuthenticationError(
message="Saved browser profile did not expose the required NotebookLM cookies",
hint="Run 'nlm login' in a desktop session to refresh the managed browser profile.",
)
return None

# Get page HTML for CSRF extraction
Expand All @@ -2068,6 +2098,11 @@ def run_headless_auth(
extracted_at=time.time(),
)
if not _validate_headless_candidate(tokens, profile_name):
if raise_on_error:
raise AuthenticationError(
message="NotebookLM rejected the credentials extracted from the saved browser profile",
hint="Run 'nlm login' in a desktop session to refresh the managed browser profile.",
)
return None

save_kwargs: dict[str, Any] = {"profile_name": profile_name}
Expand Down
64 changes: 64 additions & 0 deletions tests/test_cdp_headless_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,67 @@ def test_headless_auth_can_surface_safe_port_failure():
)

assert "nlm login" in str(exc_info.value).lower()


def test_headless_auth_reports_unsigned_saved_profile_for_explicit_refresh():
"""Explicit refresh should explain when the managed browser is simply signed out."""
from notebooklm_tools.core.exceptions import AuthenticationError

page = {"webSocketDebuggerUrl": "ws://127.0.0.1:9223/devtools/page/1"}
with (
patch.object(cdp, "has_chrome_profile", return_value=True),
patch.object(cdp, "find_existing_nlm_chrome", return_value=(9223, "http://127.0.0.1:9223")),
patch.object(cdp, "find_or_create_notebooklm_page", return_value=page),
pytest.raises(AuthenticationError, match="not signed in") as exc_info,
):
cdp.run_headless_auth(
port=9223,
timeout=0,
profile_name="work",
raise_on_error=True,
)

assert "nlm login" in str(exc_info.value).lower()


def test_headless_auth_keeps_unsigned_saved_profile_best_effort_compatible():
"""Automatic recovery should still collapse a signed-out saved profile to None."""
page = {"webSocketDebuggerUrl": "ws://127.0.0.1:9223/devtools/page/1"}
with (
patch.object(cdp, "has_chrome_profile", return_value=True),
patch.object(cdp, "find_existing_nlm_chrome", return_value=(9223, "http://127.0.0.1:9223")),
patch.object(cdp, "find_or_create_notebooklm_page", return_value=page),
):
result = cdp.run_headless_auth(port=9223, timeout=0, profile_name="work")

assert result is None


def test_headless_auth_reports_candidate_rejection_for_explicit_refresh():
"""Explicit refresh should distinguish provider rejection from browser launch failures."""
from notebooklm_tools.core.exceptions import AuthenticationError

page = {"webSocketDebuggerUrl": "ws://127.0.0.1:9223/devtools/page/1"}
cookies = [{"name": "SID", "value": "fresh", "domain": ".google.com"}]
with (
patch.object(cdp, "has_chrome_profile", return_value=True),
patch.object(cdp, "find_existing_nlm_chrome", return_value=(9223, "http://127.0.0.1:9223")),
patch.object(cdp, "find_or_create_notebooklm_page", return_value=page),
patch.object(cdp, "get_current_url", return_value="https://notebook.google.com/"),
patch.object(cdp, "is_logged_in", return_value=True),
patch.object(cdp, "_wait_for_page_ready", return_value=("<html></html>", True)),
patch.object(cdp, "get_page_cookies", return_value=cookies),
patch("notebooklm_tools.core.auth.validate_cookies", return_value=True),
patch.object(cdp, "extract_csrf_token", return_value="csrf"),
patch.object(cdp, "extract_session_id", return_value="session"),
patch.object(cdp, "_validate_headless_candidate", return_value=False),
pytest.raises(AuthenticationError, match="rejected the credentials") as exc_info,
):
cdp.run_headless_auth(
port=9223,
timeout=1,
profile_name="work",
raise_on_error=True,
)

assert "nlm login" in str(exc_info.value).lower()
Loading