Skip to content

Commit 1a0e21f

Browse files
Miriadresearch
andcommitted
feat: add notebooklm/auth.ts
Co-authored-by: research <research@miriad.systems>
1 parent e17e564 commit 1a0e21f

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

lib/services/notebooklm/auth.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* NotebookLM TypeScript Client — Authentication
3+
*
4+
* Reads Google cookies from the NOTEBOOKLM_AUTH_JSON env var (Playwright
5+
* storage state format), fetches the NotebookLM homepage to extract CSRF
6+
* and session tokens, and returns an AuthTokens object for RPC calls.
7+
*
8+
* @module lib/services/notebooklm/auth
9+
*/
10+
11+
import type { AuthTokens, NotebookLMCookie } from './types';
12+
13+
// ---------------------------------------------------------------------------
14+
// Constants
15+
// ---------------------------------------------------------------------------
16+
17+
const NOTEBOOKLM_HOME = 'https://notebooklm.google.com/';
18+
19+
const ALLOWED_DOMAINS = [
20+
'.google.com',
21+
'notebooklm.google.com',
22+
'.googleusercontent.com',
23+
];
24+
25+
const CSRF_REGEX = /"SNlM0e"\s*:\s*"([^"]+)"/;
26+
const SESSION_REGEX = /"FdrFJe"\s*:\s*"([^"]+)"/;
27+
28+
/** Default timeout for the initial auth fetch (ms) */
29+
const AUTH_FETCH_TIMEOUT_MS = 30_000;
30+
31+
// ---------------------------------------------------------------------------
32+
// Helpers
33+
// ---------------------------------------------------------------------------
34+
35+
/**
36+
* Check if a cookie domain is in the allowed list.
37+
*/
38+
function isAllowedDomain(domain: string): boolean {
39+
const normalized = domain.startsWith('.') ? domain : `.${domain}`;
40+
return ALLOWED_DOMAINS.some(
41+
(allowed) =>
42+
normalized === allowed ||
43+
normalized.endsWith(allowed) ||
44+
domain === allowed.replace(/^\./, '')
45+
);
46+
}
47+
48+
/**
49+
* Parse the Playwright storage state JSON from the env var.
50+
*/
51+
function parseCookiesFromEnv(): Record<string, string> {
52+
const authJson = process.env.NOTEBOOKLM_AUTH_JSON;
53+
if (!authJson) {
54+
throw new Error(
55+
'[NotebookLM] NOTEBOOKLM_AUTH_JSON env var is not set. ' +
56+
'Set it to a Playwright storage state JSON with Google cookies.'
57+
);
58+
}
59+
60+
let storageState: { cookies?: NotebookLMCookie[] };
61+
try {
62+
storageState = JSON.parse(authJson) as { cookies?: NotebookLMCookie[] };
63+
} catch {
64+
throw new Error(
65+
'[NotebookLM] NOTEBOOKLM_AUTH_JSON is not valid JSON. ' +
66+
'Expected Playwright storage state format: {"cookies": [...]}'
67+
);
68+
}
69+
70+
const rawCookies = storageState.cookies;
71+
if (!Array.isArray(rawCookies) || rawCookies.length === 0) {
72+
throw new Error(
73+
'[NotebookLM] No cookies found in NOTEBOOKLM_AUTH_JSON. ' +
74+
'Expected format: {"cookies": [{"name": "SID", "value": "...", "domain": ".google.com"}, ...]}'
75+
);
76+
}
77+
78+
// Filter to allowed domains and deduplicate (last wins)
79+
const cookies: Record<string, string> = {};
80+
for (const cookie of rawCookies) {
81+
if (
82+
cookie.name &&
83+
cookie.value &&
84+
cookie.domain &&
85+
isAllowedDomain(cookie.domain)
86+
) {
87+
cookies[cookie.name] = cookie.value;
88+
}
89+
}
90+
91+
if (!cookies['SID']) {
92+
throw new Error(
93+
'[NotebookLM] SID cookie not found in NOTEBOOKLM_AUTH_JSON. ' +
94+
'The Google auth cookies may be missing or from the wrong domain.'
95+
);
96+
}
97+
98+
return cookies;
99+
}
100+
101+
/**
102+
* Build a Cookie header string from a cookies record.
103+
*/
104+
function buildCookieHeader(cookies: Record<string, string>): string {
105+
return Object.entries(cookies)
106+
.map(([name, value]) => `${name}=${value}`)
107+
.join('; ');
108+
}
109+
110+
/**
111+
* Fetch with an AbortController timeout.
112+
*/
113+
async function fetchWithTimeout(
114+
url: string,
115+
init: RequestInit,
116+
timeoutMs: number
117+
): Promise<Response> {
118+
const controller = new AbortController();
119+
const timer = setTimeout(() => controller.abort(), timeoutMs);
120+
121+
try {
122+
const response = await fetch(url, {
123+
...init,
124+
signal: controller.signal,
125+
});
126+
return response;
127+
} finally {
128+
clearTimeout(timer);
129+
}
130+
}
131+
132+
// ---------------------------------------------------------------------------
133+
// Main auth function
134+
// ---------------------------------------------------------------------------
135+
136+
/**
137+
* Initialize authentication for the NotebookLM client.
138+
*
139+
* 1. Reads cookies from NOTEBOOKLM_AUTH_JSON env var
140+
* 2. Fetches the NotebookLM homepage to extract CSRF + session tokens
141+
* 3. Returns AuthTokens for use in RPC calls
142+
*
143+
* @throws Error if cookies are missing, auth is expired, or tokens can't be extracted
144+
*/
145+
export async function initAuth(): Promise<AuthTokens> {
146+
console.log('[NotebookLM] Initializing authentication...');
147+
148+
// Step 1: Parse cookies from env
149+
const cookies = parseCookiesFromEnv();
150+
const cookieHeader = buildCookieHeader(cookies);
151+
152+
console.log(
153+
`[NotebookLM] Found ${Object.keys(cookies).length} cookies from allowed domains`
154+
);
155+
156+
// Step 2: Fetch NotebookLM homepage to get CSRF and session tokens
157+
let html: string;
158+
let finalUrl: string;
159+
160+
try {
161+
const response = await fetchWithTimeout(
162+
NOTEBOOKLM_HOME,
163+
{
164+
method: 'GET',
165+
headers: {
166+
Cookie: cookieHeader,
167+
'User-Agent':
168+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
169+
},
170+
redirect: 'follow',
171+
},
172+
AUTH_FETCH_TIMEOUT_MS
173+
);
174+
175+
finalUrl = response.url;
176+
html = await response.text();
177+
} catch (error: unknown) {
178+
if (error instanceof Error && error.name === 'AbortError') {
179+
throw new Error(
180+
'[NotebookLM] Auth fetch timed out after ' +
181+
`${AUTH_FETCH_TIMEOUT_MS}ms. Check network connectivity.`
182+
);
183+
}
184+
throw new Error(
185+
`[NotebookLM] Failed to fetch NotebookLM homepage: ${error instanceof Error ? error.message : String(error)}`
186+
);
187+
}
188+
189+
// Step 3: Check for auth redirect (Google login page)
190+
if (
191+
finalUrl.includes('accounts.google.com') ||
192+
finalUrl.includes('/signin') ||
193+
finalUrl.includes('ServiceLogin')
194+
) {
195+
throw new Error(
196+
'[NotebookLM] Authentication expired — redirected to Google login page. ' +
197+
'Update NOTEBOOKLM_AUTH_JSON with fresh cookies from a logged-in browser session.'
198+
);
199+
}
200+
201+
// Step 4: Extract CSRF token
202+
const csrfMatch = html.match(CSRF_REGEX);
203+
if (!csrfMatch || !csrfMatch[1]) {
204+
throw new Error(
205+
'[NotebookLM] Could not extract CSRF token (SNlM0e) from NotebookLM page. ' +
206+
'The page structure may have changed, or auth cookies may be invalid.'
207+
);
208+
}
209+
const csrfToken = csrfMatch[1];
210+
211+
// Step 5: Extract session ID
212+
const sessionMatch = html.match(SESSION_REGEX);
213+
if (!sessionMatch || !sessionMatch[1]) {
214+
throw new Error(
215+
'[NotebookLM] Could not extract session ID (FdrFJe) from NotebookLM page. ' +
216+
'The page structure may have changed, or auth cookies may be invalid.'
217+
);
218+
}
219+
const sessionId = sessionMatch[1];
220+
221+
console.log('[NotebookLM] Authentication initialized successfully');
222+
223+
return {
224+
cookies,
225+
cookieHeader,
226+
csrfToken,
227+
sessionId,
228+
};
229+
}

0 commit comments

Comments
 (0)