/ clis / notebooklm / rpc.js
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  }