NotebookLM authentication material is account-equivalent. storage_state.json contains live
cookies; master_token.json can mint new sessions. Treat both as secrets: keep them out of source
control, logs, bug reports, shared artifact stores, and untrusted process environments. Prefer a
dedicated account for unattended use. See installation.md and
troubleshooting.md for current authentication and session-refresh guidance.
The Android backend requires the active profile's master_token.json and
mints short-lived Google bearer tokens from it. Its typed namespace calls do
not require storage_state.json, but the profile may contain both files when
the same account also uses Web. Protect each as described below. Android and
Web are both unofficial integrations over undocumented Google services, so
backend selection does not change the security or availability guarantees.
The authentication architecture maps credential owners and recovery, while the transfer-security data flow shows URL validation, per-hop credential stripping, bounded reads, and atomic publication.
ProfileStoreowns one caller-spelled profile path, its derived token path, typed reads, and profile commits.MasterTokenFileis the legacy arbitrary-path token owner. A production operation must not independently pair a profile path with an unrelated token path.credential_io.pyis the only unchecked atomic-write capability. Profile and arbitrary-token writes use separate private wrappers so an aggregate cannot be committed through the wrong authority.- Profile directories/files are created with restrictive POSIX permissions (
0700/0600). On Windows, access follows inherited ACLs rather than emulated POSIX mode bits. ProfileDocumentpreserves unknown JSON and raw cookie rows, but pure typed views never serialize themselves back over that raw document. Filtering and corruption policy stay at operation boundaries.CookieJarandMasterTokenredact secret values from repr. Auth diagnostics log bounded types, counts, paths, and statuses rather than cookie/token values.
Three explicit owners replace free-floating process state:
SingleFlightowns cross-loop flights, strong leader-task references, and per-canonical-path success epochs. Flight claim plus stale-epoch comparison is atomic. Settled slots prompt-pop, so terminal cookie-bearing results are not retained in the registry.ColdRecoveryStateowns weak-loop path locks and success generations. Its one threading lock is held only for synchronous map access, never across an await.RotationStateowns weak-loop keepalive locks and per-canonical-path monotonic attempt stamps. The claim is stamped before the RotateCookies POST, so failure or cancellation consumes the same 60-second throttle slot as success.
ColdRecoveryCoordinator is a one-shot operation. It claims synchronously before its first await,
runs L2.5 then the coalesced L3/L4 ladder, and deletes all eleven injected callbacks on every exit.
AccountRepairService applies the same rule to its six collaborators. This bounds references to
cookie jars, paths, browser functions, and persistence capabilities to one operation.
Cancellation does not mean rollback:
- a cancelled single-flight follower settles without cancelling the shared worker or siblings;
- a cancelled leader is mirrored as
CancelledError, not rewritten as another exception; - cancellation before caller-jar replacement leaves that jar untouched, while cancellation after replacement does not undo it;
- cancellation of an
asyncio.to_threadpersistence call reaches the caller immediately, but an already-dispatched worker may still finish and commit; - reset helpers are test-only and reject live flights or locked paths instead of clearing active work.
Inline PSIDTS recovery keeps raw and typed responsibilities separate. cookies.py owns pure
network-free loaders. psidts_recovery.py receives a loader explicitly, reads raw rows through
ProfileStore/ProfileDocument, records a value-only observation before the network POST,
converts live jars through CookieJar, and persists through ProfileStore. It does not import the
cookie module or the storage.py facade. Pure RFC 6265 routing and expiry helpers are canonical in
cookie_types; the recovery owner exposes compatibility aliases and injects the single
mint_service RotateCookies endpoint through its thin routing wrappers.
The sentinel, contention, full-reread, observation, POST, typed CAS, and disk-reread order is security-relevant. A write result is not proof that the desired cookie landed; the post-save disk state is authoritative and may show that a sibling process won. Catches are deliberately narrow: Unicode failures, cancellation, and unlisted errors retain their identity rather than being silently treated as a declined heal.
MasterTokenError is defined in the dependency-bottom types leaf but preserves its historical
notebooklm._auth.master_token module and pickle identity. The same is true for Account and
PlaywrightAccountRepairResult with notebooklm._auth.account. Facade, storage, and compatibility
imports resolve to those exact objects; adapters do not wrap or translate unexpected exceptions.
A missing optional gpsoauth install is deliberately not a MasterTokenError: it propagates as
MissingDependencyError, preserving the shared dependency/install-hint classification instead of
being flattened into a credential rejection.
Account repair extracts the active email before its handled-error region, offloads only cookie
loading, and performs typed write/clear operations synchronously. Only OSError, ValueError,
RuntimeError, and httpx.HTTPError become the historical result value. Cancellation and unlisted
exceptions escape. Cleanup failure is warning-only and cannot replace the original handled error.
Phase 12C changes ownership, not the v0.x public contract or disk schema. Public auth/storage saver
identities, loader signatures, late-bound adapters, client cookie-saver injection, keepalive raw
state identity views, result objects, messages, causes, and traceback projections remain pinned.
The raw keepalive names are non-owning views into RotationState; rebinding them or introducing a
second registry is unsupported.
The measured auth graph is 32 modules / 13,686 lines / 123 unique edges (111 module + 12
function-local). Module-only and all-scope SCC sets are empty; the former
cookies/master_token/psidts_recovery/storage cycle has been removed. Exact touched production LOC
is pinned in development.md; those values are evidence and not spare capacity.
- Restrict profile directories to the service account that runs NotebookLM.
- Do not place unrelated secrets in the environment inherited by
NOTEBOOKLM_REFRESH_CMD; only the documented NotebookLM secret variables are scrubbed before the child starts. - Keep CDP endpoints loopback-only and trusted. A CDP connection is account-equivalent.
- Disable
NOTEBOOKLM_DISABLE_KEEPALIVE_POKEonly when the network policy requires it; doing so removes proactive rotation, not the need for fresh cookies. - Rotate or revoke a master token immediately if its file may have been exposed.
- Preserve the canonical lock files and atomic writers; custom writers must retain the documented locking, permissions, and exact-path behavior.
- MCP stdio host-path file-add is off unless
NOTEBOOKLM_MCP_ALLOWED_ROOTSis set to an explicit upload directory — not$HOMEand not~/.notebooklm. Allowed files are opened without following symlinks or junctions and copied into a private temporary directory before authentication or upload. Replacing a caller path cannot redirect the backend's later file open; the copy is removed when the call completes, fails, or is cancelled. Remote HTTP never opens a server-hostpath.
storage_state.json and master_token.json remain account-equivalent when an
experimental MCP or REST process opens them. The operator-facing threat model —
MCP OAuth refresh tokens, signed /files/dl and /files/ul HMAC URLs, stdio
source_add(path=...) server-host reads, loopback vs token, and GET /healthz
liveness — lives in SECURITY.md. See also
mcp-guide.md and
ADR-0024.