v1: Web vs Android credential types, frozen AuthTokens, and from_storage loader split

Author: teng-linCreated Sep 6, 2026Updated Sep 6, 2026
Labelsenhancementauthdo-not-implement-yetP2

Parent: #2376. Replaces #2181 and #2182 as implementation specs.

#2182 wanted a public ClientAuth / ClientAuthSnapshot and refresh_auth() -> ClientAuth. Do not implement that. The accepted destination is ADR-0039.

What 0.x does today

  • NotebookLMClient(auth: AuthTokens, ...) always takes a mutable Web-shaped token object (cookies, cookie_jar, csrf_token, session_id, …), even when backend="android".
  • from_storage(backend="android") still loads Web cookies and GETs the NotebookLM homepage (heal_psidts=False, poke=False) so deprecated rpc_call can later materialise a Web sidecar with populated CSRF/session fields.
  • client.auth is that same mutable AuthTokens instance (ADR-0016 identity invariant). Refresh mutates it in place and successful refresh_auth() returns it.
  • get_account_authuser() is implemented on both backends.
  • Android typed namespaces do not use the Web session; only construction + the deprecated hatch do.

v1 public contract

python
NotebookLMClient(
    credential: AuthTokens | AndroidMasterToken,
    *,
    config: ClientConfig | None = None,
)

Web AuthTokens (ADR-0032 Phase B) is a frozen bootstrap value, copied once into Web runtime ownership:

  • initial_cookies: CookieJar — immutable ordered sequence of Cookie rows, not a live jar, not a Mapping
  • csrf_token: str
  • session_id: str
  • authuser: int
  • account_email: str | None
  • storage_path: Path | None — persistence target only, not a credential source

Construction is pure: no file or network I/O. Custom repr redacts cookies, CSRF, and session IDs.

AndroidMasterToken is a public frozen credential: email, android_id, and a secret master-token value excluded from repr, errors, logs, metrics, and serialization. It is the public projection of the existing internal MasterToken — one canonical value, not a second model. Construction never silently writes it to disk.

Backend selection is independent: ClientConfig.backend, then NOTEBOOKLM_BACKEND, then "web". A credential never selects or overrides the backend. Web + AndroidMasterToken, or Android + AuthTokens (including empty/placeholder tokens), raises ConfigurationError synchronously before filesystem access, optional-dependency checks, credential acquisition, or network I/O.

from_storage

NotebookLMClient.from_storage(path, profile, allow_headless, *, config=...) stays the canonical factory. It resolves the backend before parsing or loading any credential.

Web precedence:

  1. explicit path
  2. presence of NOTEBOOKLM_AUTH_JSON (empty/malformed still fails as Web input; no fall-through)
  3. explicit / env / active / default profile

Android precedence:

  1. explicit path as the profile storage_state.json location whose sibling is master_token.json (the cookie file need not exist)
  2. explicit / env / active / default profile and its nominal master_token.json

Android ignores NOTEBOOKLM_AUTH_JSON without parsing it. Explicit path wins over profile for both backends. Path resolution starts at the nominal profile directory; a legacy cookie-file fallback must not redirect master-token lookup.

Also remove the pre-profiles home-root fallback (~/.notebooklm/storage_state.json outside profiles/<name>/) as part of the same v1 drain (warned since v0.8.1; listed on #2376). Preserve the relative order of the remaining sources.

ProfileStore transaction semantics stay: one canonical path, inter-process locks, atomic replace, 0o600/0o700, snapshot/CAS, account-route checks. Backend dispatch changes which loader runs, not on-disk formats. Missing, unreadable, malformed, and wrong-account errors stay distinguishable and secret-free.

client.auth and refresh

  • Web: client.auth is the immutable bootstrap AuthTokens. Live cookies, CSRF/session replacements, persistence baselines, and CAS/generation state stay private to Web owners (Kernel, session auth, CookiePersistence).
  • Android: client.auth is frozen secret-free AndroidAuth with master-token email and android_id only. No durable secret, no Web CSRF/cookies/authuser.
  • Account identity for callers: get_account_email() (backend-neutral, network-free when already known). Android email comes from the master token, not stale Web cookie metadata.
  • get_account_authuser() is Web-only. Android raises UnsupportedOperationError before I/O.
  • refresh_auth(*, allow_headless=False) -> None on both backends. Web refreshes live cookie/request-token owners through existing transactions. Android invalidates and re-mints the gRPC bearer from the selected master token with no Web I/O. allow_headless=True is Web-only (UnsupportedOperationError on Android). Refresh neither replaces nor reveals the Android durable credential.

This ends ADR-0016’s mutable-AuthTokens aliasing rule. The CORE_LOGGER_NAME == "notebooklm._core" decision is unchanged.

AuthTokens members to delete on the same cut

Member 0.x status
from_storage(...) warned since v0.8.1
sync storage_path + cookie_jar=None construction warned since v0.8.1
flat_cookies warned since v0.8.1
replace_cookie_jar() source warning; not in a published release yet
cookies, cookie_jar docs-only deprecation since v0.8.1 (dataclass fields cannot warn)
cookie_snapshot docs-only; source since=0.9.0 unshipped
jar warning-free 0.x migration shape; docs-scheduled v1 removal
cookie_header / cookie_header_for docs-only

Publish/API-pin Cookie and CookieJar as notebooklm.Cookie / notebooklm.CookieJar so AuthTokens(initial_cookies=...) needs no private import. Do not publish them accidentally via notebooklm.auth.__all__ unless that is an explicit extra decision.

Also publish AndroidMasterToken and AndroidAuth.

0.x until this cut

Keep mutable AuthTokens and the documented Android homepage bootstrap. Do not infer an Android credential mode from a missing, empty, malformed, or unreadable Web storage file. Those are distinct failures, not “master-token-only.”

Acceptance

  • Clean interpreter: Android import, NotebookLMClient(AndroidMasterToken(...)) / from_storage(..., config=ClientConfig(backend=AndroidBackendConfig())), open, refresh_auth(), and typed namespace calls with zero notebooklm._web* imports and zero homepage/batchexecute I/O
  • Web from_storage / direct AuthTokens still bootstrap cookies + CSRF/session correctly
  • Credential/backend mismatch raises ConfigurationError before I/O
  • client.auth identity and refresh_auth() -> None match ADR-0039 on both backends
  • Public __all__, API-compat allowlist (every field/method the audit reports), behavioral runway for breaks the audit cannot see, ADRs, docs/deprecations.md (move rows to Removed), changelog, examples
  • --check-stale rejects allowances with no matching ApiBreak (sync storage fallback, home-root fallback, sidecar class are behavioral/private)

Explicitly not this issue

  • Public ClientAuth / ClientAuthSnapshot / LiveAuthState
  • Returning client.auth from refresh_auth()
  • CookieJar as a Mapping (rejected; sequence of full-fidelity rows)
  • Putting credentials on ClientConfig
  • Letting the credential object choose the backend
  • Exposing the stored Android master-token secret through client.auth
  • Removing rpc_call (that is #2378; same release, separate PR surface)
  • Flipping artifact-absence / bound-settings / mind-map / prefetch defaults (#2379)