rpc.js
1 import { AuthRequiredError, CliError } from '@jackwener/opencli/errors'; 2 import { NOTEBOOKLM_DOMAIN } from './shared.js'; 3 export function extractNotebooklmPageAuthFromHtml(html, sourcePath = '/', preferredTokens) { 4 const csrfMatch = html.match(/"SNlM0e":"([^"]+)"/); 5 const sessionMatch = html.match(/"FdrFJe":"([^"]+)"/); 6 const csrfToken = preferredTokens?.csrfToken?.trim() || (csrfMatch ? csrfMatch[1] : ''); 7 const sessionId = preferredTokens?.sessionId?.trim() || (sessionMatch ? sessionMatch[1] : ''); 8 if (!csrfToken || !sessionId) { 9 throw new CliError('NOTEBOOKLM_TOKENS', 'NotebookLM page tokens were not found in the current page HTML', 'Open the NotebookLM notebook page in Chrome, wait for it to finish loading, then retry with --verbose if it still fails.'); 10 } 11 return { csrfToken, sessionId, sourcePath: sourcePath || '/' }; 12 } 13 async function probeNotebooklmPageAuth(page) { 14 const raw = await page.evaluate(`(() => { 15 const wiz = window.WIZ_global_data || {}; 16 const html = document.documentElement.innerHTML; 17 return { 18 html, 19 sourcePath: location.pathname || '/', 20 readyState: document.readyState || '', 21 csrfToken: typeof wiz.SNlM0e === 'string' ? wiz.SNlM0e : '', 22 sessionId: typeof wiz.FdrFJe === 'string' ? wiz.FdrFJe : '', 23 }; 24 })()`); 25 return { 26 html: String(raw?.html ?? ''), 27 sourcePath: String(raw?.sourcePath ?? '/'), 28 readyState: String(raw?.readyState ?? ''), 29 csrfToken: String(raw?.csrfToken ?? ''), 30 sessionId: String(raw?.sessionId ?? ''), 31 }; 32 } 33 export async function getNotebooklmPageAuth(page) { 34 let lastError; 35 for (let attempt = 0; attempt < 2; attempt += 1) { 36 const probe = await probeNotebooklmPageAuth(page); 37 try { 38 return extractNotebooklmPageAuthFromHtml(probe.html, probe.sourcePath, { csrfToken: probe.csrfToken, sessionId: probe.sessionId }); 39 } 40 catch (error) { 41 lastError = error; 42 if (attempt === 0 && typeof page.wait === 'function') { 43 await page.wait(0.5).catch(() => undefined); 44 continue; 45 } 46 } 47 } 48 throw lastError; 49 } 50 export function buildNotebooklmRpcBody(rpcId, params, csrfToken) { 51 const rpcRequest = [[[rpcId, JSON.stringify(params), null, 'generic']]]; 52 return `f.req=${encodeURIComponent(JSON.stringify(rpcRequest))}&at=${encodeURIComponent(csrfToken)}&`; 53 } 54 export function stripNotebooklmAntiXssi(rawBody) { 55 if (!rawBody.startsWith(")]}'")) 56 return rawBody; 57 return rawBody.replace(/^\)\]\}'\r?\n/, ''); 58 } 59 export function parseNotebooklmChunkedResponse(rawBody) { 60 const cleaned = stripNotebooklmAntiXssi(rawBody).trim(); 61 if (!cleaned) 62 return []; 63 const lines = cleaned.split('\n'); 64 const chunks = []; 65 for (let i = 0; i < lines.length; i += 1) { 66 const line = lines[i].trim(); 67 if (!line) 68 continue; 69 if (/^\d+$/.test(line)) { 70 const nextLine = lines[i + 1]; 71 if (!nextLine) 72 continue; 73 try { 74 chunks.push(JSON.parse(nextLine)); 75 } 76 catch { 77 // Ignore malformed chunks and keep scanning. 78 } 79 i += 1; 80 continue; 81 } 82 if (line.startsWith('[')) { 83 try { 84 chunks.push(JSON.parse(line)); 85 } 86 catch { 87 // Ignore malformed chunks and keep scanning. 88 } 89 } 90 } 91 return chunks; 92 } 93 export function extractNotebooklmRpcResult(rawBody, rpcId) { 94 const chunks = parseNotebooklmChunkedResponse(rawBody); 95 for (const chunk of chunks) { 96 if (!Array.isArray(chunk)) 97 continue; 98 const items = Array.isArray(chunk[0]) ? chunk : [chunk]; 99 for (const item of items) { 100 if (!Array.isArray(item) || item.length < 1) 101 continue; 102 if (item[0] === 'er') { 103 const errorCode = typeof item[2] === 'number' 104 ? item[2] 105 : typeof item[5] === 'number' 106 ? item[5] 107 : null; 108 if (errorCode === 401 || errorCode === 403) { 109 throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${errorCode})`); 110 } 111 throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC failed${errorCode ? ` (code=${errorCode})` : ''}`, 'Retry from an already logged-in NotebookLM session, or inspect the raw response with debug logging.'); 112 } 113 if (item[0] === 'wrb.fr' && item[1] === rpcId) { 114 const payload = item[2]; 115 if (typeof payload === 'string') { 116 try { 117 return JSON.parse(payload); 118 } 119 catch { 120 return payload; 121 } 122 } 123 return payload; 124 } 125 } 126 } 127 return null; 128 } 129 export async function fetchNotebooklmInPage(page, url, options = {}) { 130 const method = options.method ?? 'GET'; 131 const headers = options.headers ?? {}; 132 const body = options.body ?? ''; 133 const raw = await page.evaluate(`(async () => { 134 const request = { 135 url: ${JSON.stringify(url)}, 136 method: ${JSON.stringify(method)}, 137 headers: ${JSON.stringify(headers)}, 138 body: ${JSON.stringify(body)}, 139 }; 140 141 const response = await fetch(request.url, { 142 method: request.method, 143 headers: request.headers, 144 body: request.method === 'GET' ? undefined : request.body, 145 credentials: 'include', 146 }); 147 148 return { 149 ok: response.ok, 150 status: response.status, 151 body: await response.text(), 152 finalUrl: response.url, 153 }; 154 })()`); 155 return { 156 ok: Boolean(raw?.ok), 157 status: Number(raw?.status ?? 0), 158 body: String(raw?.body ?? ''), 159 finalUrl: String(raw?.finalUrl ?? url), 160 }; 161 } 162 export async function callNotebooklmRpc(page, rpcId, params, options = {}) { 163 const auth = await getNotebooklmPageAuth(page); 164 const requestBody = buildNotebooklmRpcBody(rpcId, params, auth.csrfToken); 165 const url = `https://${NOTEBOOKLM_DOMAIN}/_/LabsTailwindUi/data/batchexecute` + 166 `?rpcids=${rpcId}&source-path=${encodeURIComponent(auth.sourcePath)}` + 167 `&hl=${encodeURIComponent(options.hl ?? 'en')}` + 168 `&f.sid=${encodeURIComponent(auth.sessionId)}&rt=c`; 169 const response = await fetchNotebooklmInPage(page, url, { 170 method: 'POST', 171 headers: { 172 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 173 }, 174 body: requestBody, 175 }); 176 if (response.status === 401 || response.status === 403) { 177 throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${response.status})`); 178 } 179 if (!response.ok) { 180 throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC request failed with HTTP ${response.status}`, 'Retry from the NotebookLM home page in an already logged-in Chrome session.'); 181 } 182 return { 183 auth, 184 url, 185 requestBody, 186 response, 187 result: extractNotebooklmRpcResult(response.body, rpcId), 188 }; 189 }