@pan-sec/notebooklm-mcp 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/SECURITY.md +539 -0
  4. package/dist/auth/auth-manager.d.ts +137 -0
  5. package/dist/auth/auth-manager.d.ts.map +1 -0
  6. package/dist/auth/auth-manager.js +984 -0
  7. package/dist/auth/auth-manager.js.map +1 -0
  8. package/dist/auth/mcp-auth.d.ts +102 -0
  9. package/dist/auth/mcp-auth.d.ts.map +1 -0
  10. package/dist/auth/mcp-auth.js +286 -0
  11. package/dist/auth/mcp-auth.js.map +1 -0
  12. package/dist/config.d.ts +89 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +216 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/errors.d.ts +26 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +41 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/index.d.ts +32 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +371 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/library/notebook-library.d.ts +70 -0
  25. package/dist/library/notebook-library.d.ts.map +1 -0
  26. package/dist/library/notebook-library.js +279 -0
  27. package/dist/library/notebook-library.js.map +1 -0
  28. package/dist/library/types.d.ts +67 -0
  29. package/dist/library/types.d.ts.map +1 -0
  30. package/dist/library/types.js +8 -0
  31. package/dist/library/types.js.map +1 -0
  32. package/dist/resources/resource-handlers.d.ts +22 -0
  33. package/dist/resources/resource-handlers.d.ts.map +1 -0
  34. package/dist/resources/resource-handlers.js +216 -0
  35. package/dist/resources/resource-handlers.js.map +1 -0
  36. package/dist/session/browser-session.d.ts +108 -0
  37. package/dist/session/browser-session.d.ts.map +1 -0
  38. package/dist/session/browser-session.js +621 -0
  39. package/dist/session/browser-session.js.map +1 -0
  40. package/dist/session/session-manager.d.ts +77 -0
  41. package/dist/session/session-manager.d.ts.map +1 -0
  42. package/dist/session/session-manager.js +314 -0
  43. package/dist/session/session-manager.js.map +1 -0
  44. package/dist/session/session-timeout.d.ts +122 -0
  45. package/dist/session/session-timeout.d.ts.map +1 -0
  46. package/dist/session/session-timeout.js +281 -0
  47. package/dist/session/session-timeout.js.map +1 -0
  48. package/dist/session/shared-context-manager.d.ts +107 -0
  49. package/dist/session/shared-context-manager.d.ts.map +1 -0
  50. package/dist/session/shared-context-manager.js +447 -0
  51. package/dist/session/shared-context-manager.js.map +1 -0
  52. package/dist/tools/definitions/ask-question.d.ts +8 -0
  53. package/dist/tools/definitions/ask-question.d.ts.map +1 -0
  54. package/dist/tools/definitions/ask-question.js +211 -0
  55. package/dist/tools/definitions/ask-question.js.map +1 -0
  56. package/dist/tools/definitions/notebook-management.d.ts +3 -0
  57. package/dist/tools/definitions/notebook-management.d.ts.map +1 -0
  58. package/dist/tools/definitions/notebook-management.js +243 -0
  59. package/dist/tools/definitions/notebook-management.js.map +1 -0
  60. package/dist/tools/definitions/session-management.d.ts +3 -0
  61. package/dist/tools/definitions/session-management.d.ts.map +1 -0
  62. package/dist/tools/definitions/session-management.js +41 -0
  63. package/dist/tools/definitions/session-management.js.map +1 -0
  64. package/dist/tools/definitions/system.d.ts +3 -0
  65. package/dist/tools/definitions/system.d.ts.map +1 -0
  66. package/dist/tools/definitions/system.js +143 -0
  67. package/dist/tools/definitions/system.js.map +1 -0
  68. package/dist/tools/definitions.d.ts +12 -0
  69. package/dist/tools/definitions.d.ts.map +1 -0
  70. package/dist/tools/definitions.js +26 -0
  71. package/dist/tools/definitions.js.map +1 -0
  72. package/dist/tools/handlers.d.ts +213 -0
  73. package/dist/tools/handlers.d.ts.map +1 -0
  74. package/dist/tools/handlers.js +813 -0
  75. package/dist/tools/handlers.js.map +1 -0
  76. package/dist/tools/index.d.ts +8 -0
  77. package/dist/tools/index.d.ts.map +1 -0
  78. package/dist/tools/index.js +8 -0
  79. package/dist/tools/index.js.map +1 -0
  80. package/dist/types.d.ts +82 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +5 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/utils/audit-logger.d.ts +140 -0
  85. package/dist/utils/audit-logger.d.ts.map +1 -0
  86. package/dist/utils/audit-logger.js +361 -0
  87. package/dist/utils/audit-logger.js.map +1 -0
  88. package/dist/utils/cert-pinning.d.ts +97 -0
  89. package/dist/utils/cert-pinning.d.ts.map +1 -0
  90. package/dist/utils/cert-pinning.js +328 -0
  91. package/dist/utils/cert-pinning.js.map +1 -0
  92. package/dist/utils/cleanup-manager.d.ts +133 -0
  93. package/dist/utils/cleanup-manager.d.ts.map +1 -0
  94. package/dist/utils/cleanup-manager.js +673 -0
  95. package/dist/utils/cleanup-manager.js.map +1 -0
  96. package/dist/utils/cli-handler.d.ts +16 -0
  97. package/dist/utils/cli-handler.d.ts.map +1 -0
  98. package/dist/utils/cli-handler.js +102 -0
  99. package/dist/utils/cli-handler.js.map +1 -0
  100. package/dist/utils/crypto.d.ts +175 -0
  101. package/dist/utils/crypto.d.ts.map +1 -0
  102. package/dist/utils/crypto.js +612 -0
  103. package/dist/utils/crypto.js.map +1 -0
  104. package/dist/utils/logger.d.ts +61 -0
  105. package/dist/utils/logger.d.ts.map +1 -0
  106. package/dist/utils/logger.js +92 -0
  107. package/dist/utils/logger.js.map +1 -0
  108. package/dist/utils/page-utils.d.ts +54 -0
  109. package/dist/utils/page-utils.d.ts.map +1 -0
  110. package/dist/utils/page-utils.js +405 -0
  111. package/dist/utils/page-utils.js.map +1 -0
  112. package/dist/utils/response-validator.d.ts +98 -0
  113. package/dist/utils/response-validator.d.ts.map +1 -0
  114. package/dist/utils/response-validator.js +352 -0
  115. package/dist/utils/response-validator.js.map +1 -0
  116. package/dist/utils/secrets-scanner.d.ts +126 -0
  117. package/dist/utils/secrets-scanner.d.ts.map +1 -0
  118. package/dist/utils/secrets-scanner.js +443 -0
  119. package/dist/utils/secrets-scanner.js.map +1 -0
  120. package/dist/utils/secure-memory.d.ts +130 -0
  121. package/dist/utils/secure-memory.d.ts.map +1 -0
  122. package/dist/utils/secure-memory.js +279 -0
  123. package/dist/utils/secure-memory.js.map +1 -0
  124. package/dist/utils/security.d.ts +83 -0
  125. package/dist/utils/security.d.ts.map +1 -0
  126. package/dist/utils/security.js +272 -0
  127. package/dist/utils/security.js.map +1 -0
  128. package/dist/utils/settings-manager.d.ts +37 -0
  129. package/dist/utils/settings-manager.d.ts.map +1 -0
  130. package/dist/utils/settings-manager.js +125 -0
  131. package/dist/utils/settings-manager.js.map +1 -0
  132. package/dist/utils/stealth-utils.d.ts +135 -0
  133. package/dist/utils/stealth-utils.d.ts.map +1 -0
  134. package/dist/utils/stealth-utils.js +398 -0
  135. package/dist/utils/stealth-utils.js.map +1 -0
  136. package/dist/utils/tool-validation.d.ts +93 -0
  137. package/dist/utils/tool-validation.d.ts.map +1 -0
  138. package/dist/utils/tool-validation.js +277 -0
  139. package/dist/utils/tool-validation.js.map +1 -0
  140. package/docs/SECURITY_IMPLEMENTATION_PLAN.md +437 -0
  141. package/docs/configuration.md +94 -0
  142. package/docs/tools.md +34 -0
  143. package/docs/troubleshooting.md +59 -0
  144. package/docs/usage-guide.md +245 -0
  145. package/package.json +82 -0
@@ -0,0 +1,984 @@
1
+ /**
2
+ * Authentication Manager for NotebookLM
3
+ *
4
+ * Handles:
5
+ * - Interactive login (headful browser for setup)
6
+ * - Auto-login with credentials (email/password from ENV)
7
+ * - Browser state persistence (cookies + localStorage + sessionStorage)
8
+ * - Cookie expiry validation
9
+ * - State expiry checks (24h file age)
10
+ * - Hard reset for clean start
11
+ *
12
+ * Based on the Python implementation from auth.py
13
+ */
14
+ import fs from "fs/promises";
15
+ import { existsSync } from "fs";
16
+ import path from "path";
17
+ import { CONFIG, NOTEBOOKLM_AUTH_URL } from "../config.js";
18
+ import { log } from "../utils/logger.js";
19
+ import { humanType, randomDelay, realisticClick, randomMouseMovement, } from "../utils/stealth-utils.js";
20
+ import { maskEmail } from "../utils/security.js";
21
+ import { getSecureStorage } from "../utils/crypto.js";
22
+ /**
23
+ * Critical cookie names for Google authentication
24
+ */
25
+ const CRITICAL_COOKIE_NAMES = [
26
+ "SID",
27
+ "HSID",
28
+ "SSID", // Google session
29
+ "APISID",
30
+ "SAPISID", // API auth
31
+ "OSID",
32
+ "__Secure-OSID", // NotebookLM-specific
33
+ "__Secure-1PSID",
34
+ "__Secure-3PSID", // Secure variants
35
+ ];
36
+ export class AuthManager {
37
+ stateFilePath;
38
+ sessionFilePath;
39
+ constructor() {
40
+ this.stateFilePath = path.join(CONFIG.browserStateDir, "state.json");
41
+ this.sessionFilePath = path.join(CONFIG.browserStateDir, "session.json");
42
+ }
43
+ // ============================================================================
44
+ // Browser State Management
45
+ // ============================================================================
46
+ /**
47
+ * Save entire browser state (cookies + localStorage)
48
+ * Uses post-quantum encrypted storage for sensitive auth data
49
+ */
50
+ async saveBrowserState(context, page) {
51
+ try {
52
+ const secureStorage = getSecureStorage();
53
+ // Get storage state as JSON string (cookies + localStorage + IndexedDB)
54
+ const storageState = await context.storageState();
55
+ // Save encrypted state using post-quantum encryption
56
+ await secureStorage.save(this.stateFilePath, storageState);
57
+ // Also save sessionStorage if page is provided
58
+ if (page) {
59
+ try {
60
+ const sessionStorageData = await page.evaluate(() => {
61
+ // Properly extract sessionStorage as a plain object
62
+ const storage = {};
63
+ // @ts-expect-error - sessionStorage exists in browser context
64
+ for (let i = 0; i < sessionStorage.length; i++) {
65
+ // @ts-expect-error - sessionStorage exists in browser context
66
+ const key = sessionStorage.key(i);
67
+ if (key) {
68
+ // @ts-expect-error - sessionStorage exists in browser context
69
+ storage[key] = sessionStorage.getItem(key) || '';
70
+ }
71
+ }
72
+ return JSON.stringify(storage);
73
+ });
74
+ // Save sessionStorage with encryption
75
+ await secureStorage.save(this.sessionFilePath, sessionStorageData);
76
+ const entries = Object.keys(JSON.parse(sessionStorageData)).length;
77
+ const status = secureStorage.getStatus();
78
+ const encType = status.postQuantumEnabled ? "ML-KEM-768 + ChaCha20" : "ChaCha20-Poly1305";
79
+ log.success(`โœ… Browser state saved with ${encType} encryption (incl. sessionStorage: ${entries} entries)`);
80
+ }
81
+ catch (error) {
82
+ log.warning(`โš ๏ธ State saved, but sessionStorage failed: ${error}`);
83
+ }
84
+ }
85
+ else {
86
+ const status = secureStorage.getStatus();
87
+ const encType = status.postQuantumEnabled ? "ML-KEM-768 + ChaCha20" : "ChaCha20-Poly1305";
88
+ log.success(`โœ… Browser state saved with ${encType} encryption`);
89
+ }
90
+ return true;
91
+ }
92
+ catch (error) {
93
+ log.error(`โŒ Failed to save browser state: ${error}`);
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * Check if saved browser state exists (encrypted or unencrypted)
99
+ */
100
+ async hasSavedState() {
101
+ const secureStorage = getSecureStorage();
102
+ return secureStorage.exists(this.stateFilePath);
103
+ }
104
+ /**
105
+ * Get path to saved browser state (checks encrypted versions too)
106
+ */
107
+ getStatePath() {
108
+ const secureStorage = getSecureStorage();
109
+ if (secureStorage.exists(this.stateFilePath)) {
110
+ return this.stateFilePath;
111
+ }
112
+ return null;
113
+ }
114
+ /**
115
+ * Get valid state path (checks expiry)
116
+ */
117
+ async getValidStatePath() {
118
+ const statePath = this.getStatePath();
119
+ if (!statePath) {
120
+ return null;
121
+ }
122
+ if (await this.isStateExpired()) {
123
+ log.warning("โš ๏ธ Saved state is expired (>24h old)");
124
+ log.info("๐Ÿ’ก Run setup_auth tool to re-authenticate");
125
+ return null;
126
+ }
127
+ return statePath;
128
+ }
129
+ /**
130
+ * Load sessionStorage from file (decrypts if encrypted)
131
+ */
132
+ async loadSessionStorage() {
133
+ try {
134
+ const secureStorage = getSecureStorage();
135
+ const data = await secureStorage.load(this.sessionFilePath);
136
+ if (!data) {
137
+ log.warning("โš ๏ธ No sessionStorage found");
138
+ return null;
139
+ }
140
+ const sessionData = JSON.parse(data);
141
+ const status = secureStorage.getStatus();
142
+ const encType = status.postQuantumEnabled ? "ML-KEM-768 + ChaCha20" : "ChaCha20-Poly1305";
143
+ log.success(`โœ… Loaded sessionStorage with ${encType} decryption (${Object.keys(sessionData).length} entries)`);
144
+ return sessionData;
145
+ }
146
+ catch (error) {
147
+ log.warning(`โš ๏ธ Failed to load sessionStorage: ${error}`);
148
+ return null;
149
+ }
150
+ }
151
+ // ============================================================================
152
+ // Cookie Validation
153
+ // ============================================================================
154
+ /**
155
+ * Validate if saved state is still valid
156
+ */
157
+ async validateState(context) {
158
+ try {
159
+ const cookies = await context.cookies();
160
+ if (cookies.length === 0) {
161
+ log.warning("โš ๏ธ No cookies found in state");
162
+ return false;
163
+ }
164
+ // Check for Google auth cookies
165
+ const googleCookies = cookies.filter((c) => c.domain.includes("google.com"));
166
+ if (googleCookies.length === 0) {
167
+ log.warning("โš ๏ธ No Google cookies found");
168
+ return false;
169
+ }
170
+ // Check if important cookies are expired
171
+ const currentTime = Date.now() / 1000;
172
+ for (const cookie of googleCookies) {
173
+ const expires = cookie.expires ?? -1;
174
+ if (expires !== -1 && expires < currentTime) {
175
+ log.warning(`โš ๏ธ Cookie '${cookie.name}' has expired`);
176
+ return false;
177
+ }
178
+ }
179
+ log.success("โœ… State validation passed");
180
+ return true;
181
+ }
182
+ catch (error) {
183
+ log.warning(`โš ๏ธ State validation failed: ${error}`);
184
+ return false;
185
+ }
186
+ }
187
+ /**
188
+ * Validate if critical authentication cookies are still valid
189
+ */
190
+ async validateCookiesExpiry(context) {
191
+ try {
192
+ const cookies = await context.cookies();
193
+ if (cookies.length === 0) {
194
+ log.warning("โš ๏ธ No cookies found");
195
+ return false;
196
+ }
197
+ // Find critical cookies
198
+ const criticalCookies = cookies.filter((c) => CRITICAL_COOKIE_NAMES.includes(c.name));
199
+ if (criticalCookies.length === 0) {
200
+ log.warning("โš ๏ธ No critical auth cookies found");
201
+ return false;
202
+ }
203
+ // Check expiration for each critical cookie
204
+ const currentTime = Date.now() / 1000;
205
+ const expiredCookies = [];
206
+ for (const cookie of criticalCookies) {
207
+ const expires = cookie.expires ?? -1;
208
+ // -1 means session cookie (valid until browser closes)
209
+ if (expires === -1) {
210
+ continue;
211
+ }
212
+ // Check if cookie is expired
213
+ if (expires < currentTime) {
214
+ expiredCookies.push(cookie.name);
215
+ }
216
+ }
217
+ if (expiredCookies.length > 0) {
218
+ log.warning(`โš ๏ธ Expired cookies: ${expiredCookies.join(", ")}`);
219
+ return false;
220
+ }
221
+ log.success(`โœ… All ${criticalCookies.length} critical cookies are valid`);
222
+ return true;
223
+ }
224
+ catch (error) {
225
+ log.warning(`โš ๏ธ Cookie validation failed: ${error}`);
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * Check if the saved state file is too old (>24 hours)
231
+ */
232
+ async isStateExpired() {
233
+ try {
234
+ const stats = await fs.stat(this.stateFilePath);
235
+ const fileAgeSeconds = (Date.now() - stats.mtimeMs) / 1000;
236
+ const maxAgeSeconds = 24 * 60 * 60; // 24 hours
237
+ if (fileAgeSeconds > maxAgeSeconds) {
238
+ const hoursOld = fileAgeSeconds / 3600;
239
+ log.warning(`โš ๏ธ Saved state is ${hoursOld.toFixed(1)}h old (max: 24h)`);
240
+ return true;
241
+ }
242
+ return false;
243
+ }
244
+ catch {
245
+ return true; // File doesn't exist = expired
246
+ }
247
+ }
248
+ // ============================================================================
249
+ // Interactive Login
250
+ // ============================================================================
251
+ /**
252
+ * Perform interactive login
253
+ * User will see a browser window and login manually
254
+ *
255
+ * SIMPLE & RELIABLE: Just wait for URL to change to notebooklm.google.com
256
+ */
257
+ async performLogin(page, sendProgress) {
258
+ try {
259
+ log.info("๐ŸŒ Opening Google login page...");
260
+ log.warning("๐Ÿ“ Please login to your Google account");
261
+ log.warning("โณ Browser will close automatically once you reach NotebookLM");
262
+ log.info("");
263
+ // Progress: Navigating
264
+ await sendProgress?.("Navigating to Google login...", 3, 10);
265
+ // Navigate to Google login (redirects to NotebookLM after auth)
266
+ await page.goto(NOTEBOOKLM_AUTH_URL, { timeout: 60000 });
267
+ // Progress: Waiting for login
268
+ await sendProgress?.("Waiting for manual login (up to 10 minutes)...", 4, 10);
269
+ // Wait for user to complete login
270
+ log.warning("โณ Waiting for login (up to 10 minutes)...");
271
+ const checkIntervalMs = 1000; // Check every 1 second
272
+ const maxAttempts = 600; // 10 minutes total
273
+ let lastProgressUpdate = 0;
274
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
275
+ try {
276
+ const currentUrl = page.url();
277
+ const elapsedSeconds = Math.floor(attempt * (checkIntervalMs / 1000));
278
+ // Send progress every 10 seconds
279
+ if (elapsedSeconds - lastProgressUpdate >= 10) {
280
+ lastProgressUpdate = elapsedSeconds;
281
+ const progressStep = Math.min(8, 4 + Math.floor(elapsedSeconds / 60));
282
+ await sendProgress?.(`Waiting for login... (${elapsedSeconds}s elapsed)`, progressStep, 10);
283
+ }
284
+ // โœ… SIMPLE: Check if we're on NotebookLM (any path!)
285
+ if (currentUrl.startsWith("https://notebooklm.google.com/")) {
286
+ await sendProgress?.("Login successful! NotebookLM detected!", 9, 10);
287
+ log.success("โœ… Login successful! NotebookLM URL detected.");
288
+ log.success(`โœ… Current URL: ${currentUrl}`);
289
+ // Short wait to ensure page is loaded
290
+ await page.waitForTimeout(2000);
291
+ return true;
292
+ }
293
+ // Still on accounts.google.com - log periodically
294
+ if (currentUrl.includes("accounts.google.com") && attempt % 30 === 0 && attempt > 0) {
295
+ log.warning(`โณ Still waiting... (${elapsedSeconds}s elapsed)`);
296
+ }
297
+ await page.waitForTimeout(checkIntervalMs);
298
+ }
299
+ catch {
300
+ await page.waitForTimeout(checkIntervalMs);
301
+ continue;
302
+ }
303
+ }
304
+ // Timeout reached - final check
305
+ const currentUrl = page.url();
306
+ if (currentUrl.startsWith("https://notebooklm.google.com/")) {
307
+ await sendProgress?.("Login successful (detected on timeout check)!", 9, 10);
308
+ log.success("โœ… Login successful (detected on timeout check)");
309
+ return true;
310
+ }
311
+ log.error("โŒ Login verification failed - timeout reached");
312
+ log.warning(`Current URL: ${currentUrl}`);
313
+ return false;
314
+ }
315
+ catch (error) {
316
+ log.error(`โŒ Login failed: ${error}`);
317
+ return false;
318
+ }
319
+ }
320
+ // ============================================================================
321
+ // Auto-Login with Credentials
322
+ // ============================================================================
323
+ /**
324
+ * Attempt to authenticate using configured credentials
325
+ */
326
+ async loginWithCredentials(context, page, email, password) {
327
+ const maskedEmail = maskEmail(email);
328
+ log.warning(`๐Ÿ” Attempting automatic login for ${maskedEmail}...`);
329
+ // Log browser visibility
330
+ if (!CONFIG.headless) {
331
+ log.info(" ๐Ÿ‘๏ธ Browser is VISIBLE for debugging");
332
+ }
333
+ else {
334
+ log.info(" ๐Ÿ™ˆ Browser is HEADLESS (invisible)");
335
+ }
336
+ log.info(` ๐ŸŒ Navigating to Google login...`);
337
+ try {
338
+ await page.goto(NOTEBOOKLM_AUTH_URL, {
339
+ waitUntil: "domcontentloaded",
340
+ timeout: CONFIG.browserTimeout,
341
+ });
342
+ log.success(` โœ… Page loaded: ${page.url().slice(0, 80)}...`);
343
+ }
344
+ catch (error) {
345
+ log.warning(` โš ๏ธ Page load timeout (continuing anyway)`);
346
+ }
347
+ const deadline = Date.now() + CONFIG.autoLoginTimeoutMs;
348
+ log.info(` โฐ Auto-login timeout: ${CONFIG.autoLoginTimeoutMs / 1000}s`);
349
+ // Already on NotebookLM?
350
+ log.info(" ๐Ÿ” Checking if already authenticated...");
351
+ if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
352
+ log.success("โœ… Already authenticated");
353
+ await this.saveBrowserState(context, page);
354
+ return true;
355
+ }
356
+ log.warning(" โŒ Not authenticated yet, proceeding with login...");
357
+ // Handle possible account chooser
358
+ log.info(" ๐Ÿ” Checking for account chooser...");
359
+ if (await this.handleAccountChooser(page, email)) {
360
+ log.success(" โœ… Account selected from chooser");
361
+ if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
362
+ log.success("โœ… Automatic login successful");
363
+ await this.saveBrowserState(context, page);
364
+ return true;
365
+ }
366
+ }
367
+ // Email step
368
+ log.info(" ๐Ÿ“ง Entering email address...");
369
+ if (!(await this.fillIdentifier(page, email))) {
370
+ if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) {
371
+ log.success("โœ… Automatic login successful");
372
+ await this.saveBrowserState(context, page);
373
+ return true;
374
+ }
375
+ log.warning("โš ๏ธ Email input not detected");
376
+ }
377
+ // Password step (wait until visible)
378
+ let waitAttempts = 0;
379
+ log.warning(" โณ Waiting for password page to load...");
380
+ while (Date.now() < deadline && !(await this.fillPassword(page, password))) {
381
+ waitAttempts++;
382
+ // Log every 10 seconds (20 attempts * 0.5s)
383
+ if (waitAttempts % 20 === 0) {
384
+ const secondsWaited = waitAttempts * 0.5;
385
+ const secondsRemaining = (deadline - Date.now()) / 1000;
386
+ log.warning(` โณ Still waiting for password field... (${secondsWaited}s elapsed, ${secondsRemaining.toFixed(0)}s remaining)`);
387
+ log.info(` ๐Ÿ“ Current URL: ${page.url().slice(0, 100)}`);
388
+ }
389
+ if (page.url().includes("challenge")) {
390
+ log.warning("โš ๏ธ Additional verification required (Google challenge page).");
391
+ return false;
392
+ }
393
+ await page.waitForTimeout(500);
394
+ }
395
+ // Wait for Google redirect after login
396
+ log.info(" ๐Ÿ”„ Waiting for Google redirect to NotebookLM...");
397
+ if (await this.waitForRedirectAfterLogin(page, deadline)) {
398
+ log.success("โœ… Automatic login successful");
399
+ await this.saveBrowserState(context, page);
400
+ return true;
401
+ }
402
+ // Login failed - diagnose
403
+ log.error("โŒ Automatic login timed out");
404
+ // Take screenshot for debugging
405
+ try {
406
+ const screenshotPath = path.join(CONFIG.dataDir, `login_failed_${Date.now()}.png`);
407
+ await page.screenshot({ path: screenshotPath });
408
+ log.info(` ๐Ÿ“ธ Screenshot saved: ${screenshotPath}`);
409
+ }
410
+ catch (error) {
411
+ log.warning(` โš ๏ธ Could not save screenshot: ${error}`);
412
+ }
413
+ // Diagnose specific failure reason
414
+ const currentUrl = page.url();
415
+ log.warning(" ๐Ÿ” Diagnosing failure...");
416
+ if (currentUrl.includes("accounts.google.com")) {
417
+ if (currentUrl.includes("/signin/identifier")) {
418
+ log.error(" โŒ Still on email page - email input might have failed");
419
+ log.info(" ๐Ÿ’ก Check if email is correct in .env");
420
+ }
421
+ else if (currentUrl.includes("/challenge")) {
422
+ log.error(" โŒ Google requires additional verification (2FA, CAPTCHA, suspicious login)");
423
+ log.info(" ๐Ÿ’ก Try logging in manually first: use setup_auth tool");
424
+ }
425
+ else if (currentUrl.includes("/pwd") || currentUrl.includes("/password")) {
426
+ log.error(" โŒ Still on password page - password input might have failed");
427
+ log.info(" ๐Ÿ’ก Check if password is correct in .env");
428
+ }
429
+ else {
430
+ log.error(` โŒ Stuck on Google accounts page: ${currentUrl.slice(0, 80)}...`);
431
+ }
432
+ }
433
+ else if (currentUrl.includes("notebooklm.google.com")) {
434
+ log.warning(" โš ๏ธ Reached NotebookLM but couldn't detect successful login");
435
+ log.info(" ๐Ÿ’ก This might be a timing issue - try again");
436
+ }
437
+ else {
438
+ log.error(` โŒ Unexpected page: ${currentUrl.slice(0, 80)}...`);
439
+ }
440
+ return false;
441
+ }
442
+ // ============================================================================
443
+ // Helper Methods
444
+ // ============================================================================
445
+ /**
446
+ * Wait for Google to redirect to NotebookLM after successful login (SIMPLE & RELIABLE)
447
+ *
448
+ * Just checks if URL changes to notebooklm.google.com - no complex UI element searching!
449
+ * Matches the simplified approach used in performLogin().
450
+ */
451
+ async waitForRedirectAfterLogin(page, deadline) {
452
+ log.info(" โณ Waiting for redirect to NotebookLM...");
453
+ while (Date.now() < deadline) {
454
+ try {
455
+ const currentUrl = page.url();
456
+ // Simple check: Are we on NotebookLM?
457
+ if (currentUrl.startsWith("https://notebooklm.google.com/")) {
458
+ log.success(" โœ… NotebookLM URL detected!");
459
+ // Short wait to ensure page is loaded
460
+ await page.waitForTimeout(2000);
461
+ return true;
462
+ }
463
+ }
464
+ catch {
465
+ // Ignore errors
466
+ }
467
+ await page.waitForTimeout(500);
468
+ }
469
+ log.error(" โŒ Redirect timeout - NotebookLM URL not reached");
470
+ return false;
471
+ }
472
+ /**
473
+ * Wait for NotebookLM to load (SIMPLE & RELIABLE)
474
+ *
475
+ * Just checks if URL starts with notebooklm.google.com - no complex UI element searching!
476
+ * Matches the simplified approach used in performLogin().
477
+ */
478
+ async waitForNotebook(page, timeoutMs) {
479
+ const endTime = Date.now() + timeoutMs;
480
+ while (Date.now() < endTime) {
481
+ try {
482
+ const currentUrl = page.url();
483
+ // Simple check: Are we on NotebookLM?
484
+ if (currentUrl.startsWith("https://notebooklm.google.com/")) {
485
+ log.success(" โœ… NotebookLM URL detected");
486
+ return true;
487
+ }
488
+ }
489
+ catch {
490
+ // Ignore errors
491
+ }
492
+ await page.waitForTimeout(1000);
493
+ }
494
+ return false;
495
+ }
496
+ /**
497
+ * Handle possible account chooser
498
+ */
499
+ async handleAccountChooser(page, email) {
500
+ try {
501
+ const chooser = await page.$$("div[data-identifier], li[data-identifier]");
502
+ if (chooser.length > 0) {
503
+ for (const item of chooser) {
504
+ const identifier = (await item.getAttribute("data-identifier"))?.toLowerCase() || "";
505
+ if (identifier === email.toLowerCase()) {
506
+ await item.click();
507
+ await randomDelay(150, 320);
508
+ await page.waitForTimeout(500);
509
+ return true;
510
+ }
511
+ }
512
+ // Click "Use another account"
513
+ await this.clickText(page, [
514
+ "Use another account",
515
+ "Weiteres Konto hinzufรผgen",
516
+ "Anderes Konto verwenden",
517
+ ]);
518
+ await randomDelay(150, 320);
519
+ return false;
520
+ }
521
+ return false;
522
+ }
523
+ catch {
524
+ return false;
525
+ }
526
+ }
527
+ /**
528
+ * Fill email identifier field with human-like typing
529
+ */
530
+ async fillIdentifier(page, email) {
531
+ log.info(" ๐Ÿ“ง Looking for email field...");
532
+ const emailSelectors = [
533
+ "input#identifierId",
534
+ "input[name='identifier']",
535
+ "input[type='email']",
536
+ ];
537
+ let emailSelector = null;
538
+ let emailField = null;
539
+ for (const selector of emailSelectors) {
540
+ try {
541
+ const candidate = await page.waitForSelector(selector, {
542
+ state: "attached",
543
+ timeout: 3000,
544
+ });
545
+ if (!candidate)
546
+ continue;
547
+ try {
548
+ if (!(await candidate.isVisible())) {
549
+ continue; // Hidden field
550
+ }
551
+ }
552
+ catch {
553
+ continue;
554
+ }
555
+ emailField = candidate;
556
+ emailSelector = selector;
557
+ log.success(` โœ… Email field visible: ${selector}`);
558
+ break;
559
+ }
560
+ catch {
561
+ continue;
562
+ }
563
+ }
564
+ if (!emailField || !emailSelector) {
565
+ log.warning(" โ„น๏ธ No visible email field found (likely pre-filled)");
566
+ log.info(` ๐Ÿ“ Current URL: ${page.url().slice(0, 100)}`);
567
+ return false;
568
+ }
569
+ // Human-like mouse movement to field
570
+ try {
571
+ const box = await emailField.boundingBox();
572
+ if (box) {
573
+ const targetX = box.x + box.width / 2;
574
+ const targetY = box.y + box.height / 2;
575
+ await randomMouseMovement(page, targetX, targetY);
576
+ await randomDelay(200, 500);
577
+ }
578
+ }
579
+ catch {
580
+ // Ignore errors
581
+ }
582
+ // Click to focus
583
+ try {
584
+ await realisticClick(page, emailSelector, false);
585
+ }
586
+ catch (error) {
587
+ log.warning(` โš ๏ธ Could not click email field (${error}); trying direct focus`);
588
+ try {
589
+ await emailField.focus();
590
+ }
591
+ catch {
592
+ log.error(" โŒ Failed to focus email field");
593
+ return false;
594
+ }
595
+ }
596
+ // โœ… FASTER: Programmer typing speed (90-120 WPM from config)
597
+ log.info(` โŒจ๏ธ Typing email: ${maskEmail(email)}`);
598
+ try {
599
+ const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1));
600
+ await humanType(page, emailSelector, email, { wpm, withTypos: false });
601
+ log.success(" โœ… Email typed successfully");
602
+ }
603
+ catch (error) {
604
+ log.error(` โŒ Typing failed: ${error}`);
605
+ try {
606
+ await page.fill(emailSelector, email);
607
+ log.success(" โœ… Filled email using fallback");
608
+ }
609
+ catch {
610
+ return false;
611
+ }
612
+ }
613
+ // Human "thinking" pause before clicking Next
614
+ await randomDelay(400, 1200);
615
+ // Click Next button
616
+ log.info(" ๐Ÿ”˜ Looking for Next button...");
617
+ const nextSelectors = [
618
+ "button:has-text('Next')",
619
+ "button:has-text('Weiter')",
620
+ "#identifierNext",
621
+ ];
622
+ let nextClicked = false;
623
+ for (const selector of nextSelectors) {
624
+ try {
625
+ const button = await page.locator(selector);
626
+ if ((await button.count()) > 0) {
627
+ await realisticClick(page, selector, true);
628
+ log.success(` โœ… Next button clicked: ${selector}`);
629
+ nextClicked = true;
630
+ break;
631
+ }
632
+ }
633
+ catch {
634
+ continue;
635
+ }
636
+ }
637
+ if (!nextClicked) {
638
+ log.warning(" โš ๏ธ Button not found, pressing Enter");
639
+ await emailField.press("Enter");
640
+ }
641
+ // Variable delay
642
+ await randomDelay(800, 1500);
643
+ log.success(" โœ… Email step complete");
644
+ return true;
645
+ }
646
+ /**
647
+ * Fill password field with human-like typing
648
+ */
649
+ async fillPassword(page, password) {
650
+ log.info(" ๐Ÿ” Looking for password field...");
651
+ const passwordSelectors = ["input[name='Passwd']", "input[type='password']"];
652
+ let passwordSelector = null;
653
+ let passwordField = null;
654
+ for (const selector of passwordSelectors) {
655
+ try {
656
+ passwordField = await page.$(selector);
657
+ if (passwordField) {
658
+ passwordSelector = selector;
659
+ log.success(` โœ… Password field found: ${selector}`);
660
+ break;
661
+ }
662
+ }
663
+ catch {
664
+ continue;
665
+ }
666
+ }
667
+ if (!passwordField) {
668
+ // Not found yet, but don't fail - this is called in a loop
669
+ return false;
670
+ }
671
+ // Human-like mouse movement to field
672
+ try {
673
+ const box = await passwordField.boundingBox();
674
+ if (box) {
675
+ const targetX = box.x + box.width / 2;
676
+ const targetY = box.y + box.height / 2;
677
+ await randomMouseMovement(page, targetX, targetY);
678
+ await randomDelay(300, 700);
679
+ }
680
+ }
681
+ catch {
682
+ // Ignore errors
683
+ }
684
+ // Click to focus
685
+ if (passwordSelector) {
686
+ await realisticClick(page, passwordSelector, false);
687
+ }
688
+ // โœ… FASTER: Programmer typing speed (90-120 WPM from config)
689
+ log.info(" โŒจ๏ธ Typing password...");
690
+ try {
691
+ const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1));
692
+ if (passwordSelector) {
693
+ await humanType(page, passwordSelector, password, { wpm, withTypos: false });
694
+ }
695
+ log.success(" โœ… Password typed successfully");
696
+ }
697
+ catch (error) {
698
+ log.error(` โŒ Typing failed: ${error}`);
699
+ return false;
700
+ }
701
+ // Human "review" pause before submitting password
702
+ await randomDelay(300, 1000);
703
+ // Click Next button
704
+ log.info(" ๐Ÿ”˜ Looking for Next button...");
705
+ const pwdNextSelectors = [
706
+ "button:has-text('Next')",
707
+ "button:has-text('Weiter')",
708
+ "#passwordNext",
709
+ ];
710
+ let pwdNextClicked = false;
711
+ for (const selector of pwdNextSelectors) {
712
+ try {
713
+ const button = await page.locator(selector);
714
+ if ((await button.count()) > 0) {
715
+ await realisticClick(page, selector, true);
716
+ log.success(` โœ… Next button clicked: ${selector}`);
717
+ pwdNextClicked = true;
718
+ break;
719
+ }
720
+ }
721
+ catch {
722
+ continue;
723
+ }
724
+ }
725
+ if (!pwdNextClicked) {
726
+ log.warning(" โš ๏ธ Button not found, pressing Enter");
727
+ await passwordField.press("Enter");
728
+ }
729
+ // Variable delay
730
+ await randomDelay(800, 1500);
731
+ log.success(" โœ… Password step complete");
732
+ return true;
733
+ }
734
+ /**
735
+ * Click text element
736
+ */
737
+ async clickText(page, texts) {
738
+ for (const text of texts) {
739
+ const selector = `text="${text}"`;
740
+ try {
741
+ const locator = page.locator(selector);
742
+ if ((await locator.count()) > 0) {
743
+ await realisticClick(page, selector, true);
744
+ await randomDelay(120, 260);
745
+ return true;
746
+ }
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ }
752
+ return false;
753
+ }
754
+ // maskEmail is now imported from security.ts for consistent sanitization
755
+ // ============================================================================
756
+ // Additional Helper Methods
757
+ // ============================================================================
758
+ /**
759
+ * Load authentication state from a specific file path (decrypts if encrypted)
760
+ */
761
+ async loadAuthState(context, statePath) {
762
+ try {
763
+ const secureStorage = getSecureStorage();
764
+ // Read and decrypt state
765
+ const stateData = await secureStorage.load(statePath);
766
+ if (!stateData) {
767
+ log.warning(`โš ๏ธ No state file found at ${statePath}`);
768
+ return false;
769
+ }
770
+ const state = JSON.parse(stateData);
771
+ // Add cookies to context
772
+ if (state.cookies) {
773
+ await context.addCookies(state.cookies);
774
+ const status = secureStorage.getStatus();
775
+ const encType = status.postQuantumEnabled ? "ML-KEM-768 + ChaCha20" : "ChaCha20-Poly1305";
776
+ log.success(`โœ… Loaded ${state.cookies.length} cookies with ${encType} decryption from ${statePath}`);
777
+ return true;
778
+ }
779
+ log.warning(`โš ๏ธ No cookies found in state file`);
780
+ return false;
781
+ }
782
+ catch (error) {
783
+ log.error(`โŒ Failed to load auth state: ${error}`);
784
+ return false;
785
+ }
786
+ }
787
+ /**
788
+ * Perform interactive setup (for setup_auth tool)
789
+ * Opens a PERSISTENT browser for manual login
790
+ *
791
+ * CRITICAL: Uses the SAME persistent context as runtime!
792
+ * This ensures cookies are automatically saved to the Chrome profile.
793
+ *
794
+ * Benefits over temporary browser:
795
+ * - Session cookies persist correctly (Playwright bug workaround)
796
+ * - Same fingerprint as runtime
797
+ * - No need for addCookies() workarounds
798
+ * - Automatic cookie persistence via Chrome profile
799
+ *
800
+ * @param sendProgress Optional progress callback
801
+ * @param overrideHeadless Optional override for headless mode (true = visible, false = headless)
802
+ * If not provided, defaults to true (visible) for setup
803
+ */
804
+ async performSetup(sendProgress, overrideHeadless) {
805
+ const { chromium } = await import("patchright");
806
+ // Determine headless mode: override or default to true (visible for setup)
807
+ // overrideHeadless contains show_browser value (true = show, false = hide)
808
+ const shouldShowBrowser = overrideHeadless !== undefined ? overrideHeadless : true;
809
+ try {
810
+ // CRITICAL: Clear ALL old auth data FIRST (for account switching)
811
+ log.info("๐Ÿ”„ Preparing for new account authentication...");
812
+ await sendProgress?.("Clearing old authentication data...", 1, 10);
813
+ await this.clearAllAuthData();
814
+ log.info("๐Ÿš€ Launching persistent browser for interactive setup...");
815
+ log.info(` ๐Ÿ“ Profile: ${CONFIG.chromeProfileDir}`);
816
+ await sendProgress?.("Launching persistent browser...", 2, 10);
817
+ // โœ… CRITICAL FIX: Use launchPersistentContext (same as runtime!)
818
+ // This ensures session cookies persist correctly
819
+ const context = await chromium.launchPersistentContext(CONFIG.chromeProfileDir, {
820
+ headless: !shouldShowBrowser, // Use override or default to visible for setup
821
+ channel: "chrome",
822
+ viewport: CONFIG.viewport,
823
+ locale: "en-US",
824
+ timezoneId: "Europe/Berlin",
825
+ args: [
826
+ "--disable-blink-features=AutomationControlled",
827
+ "--disable-dev-shm-usage",
828
+ "--no-first-run",
829
+ "--no-default-browser-check",
830
+ ],
831
+ });
832
+ // Get or create a page
833
+ const pages = context.pages();
834
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
835
+ // Perform login with progress updates
836
+ const loginSuccess = await this.performLogin(page, sendProgress);
837
+ if (loginSuccess) {
838
+ // โœ… Save browser state to state.json (for validation & backup)
839
+ // Chrome ALSO saves everything to the persistent profile automatically!
840
+ await sendProgress?.("Saving authentication state...", 9, 10);
841
+ await this.saveBrowserState(context, page);
842
+ log.success("โœ… Setup complete - authentication saved to:");
843
+ log.success(` ๐Ÿ“„ State file: ${this.stateFilePath}`);
844
+ log.success(` ๐Ÿ“ Chrome profile: ${CONFIG.chromeProfileDir}`);
845
+ log.info("๐Ÿ’ก Session cookies will now persist across restarts!");
846
+ }
847
+ // Close persistent context
848
+ await context.close();
849
+ return loginSuccess;
850
+ }
851
+ catch (error) {
852
+ log.error(`โŒ Setup failed: ${error}`);
853
+ return false;
854
+ }
855
+ }
856
+ // ============================================================================
857
+ // Cleanup
858
+ // ============================================================================
859
+ /**
860
+ * Clear ALL authentication data for account switching
861
+ *
862
+ * CRITICAL: This deletes EVERYTHING to ensure only ONE account is active:
863
+ * - All state files (encrypted .pqenc, .enc, and unencrypted .json)
864
+ * - sessionStorage files
865
+ * - Chrome profile directory (browser fingerprint, cache, etc.)
866
+ * - Post-quantum key pairs
867
+ *
868
+ * Use this BEFORE authenticating a new account!
869
+ */
870
+ async clearAllAuthData() {
871
+ log.warning("๐Ÿ—‘๏ธ Clearing ALL authentication data for account switch...");
872
+ let deletedCount = 0;
873
+ const secureStorage = getSecureStorage();
874
+ // 1. Delete all state files in browser_state_dir (including encrypted versions)
875
+ try {
876
+ const files = await fs.readdir(CONFIG.browserStateDir);
877
+ for (const file of files) {
878
+ if (file.endsWith(".json") || file.endsWith(".enc") || file.endsWith(".pqenc")) {
879
+ await fs.unlink(path.join(CONFIG.browserStateDir, file));
880
+ log.info(` โœ… Deleted: ${file}`);
881
+ deletedCount++;
882
+ }
883
+ }
884
+ }
885
+ catch (error) {
886
+ log.warning(` โš ๏ธ Could not delete state files: ${error}`);
887
+ }
888
+ // 2. Delete PQ key files
889
+ try {
890
+ await secureStorage.delete(path.join(CONFIG.configDir, "pq-keys"));
891
+ }
892
+ catch {
893
+ // Ignore
894
+ }
895
+ // 3. Delete Chrome profile (THE KEY for account switching!)
896
+ // This removes ALL browser data: cookies, cache, fingerprint, etc.
897
+ try {
898
+ const chromeProfileDir = CONFIG.chromeProfileDir;
899
+ if (existsSync(chromeProfileDir)) {
900
+ await fs.rm(chromeProfileDir, { recursive: true, force: true });
901
+ log.success(` โœ… Deleted Chrome profile: ${chromeProfileDir}`);
902
+ deletedCount++;
903
+ }
904
+ }
905
+ catch (error) {
906
+ log.warning(` โš ๏ธ Could not delete Chrome profile: ${error}`);
907
+ }
908
+ if (deletedCount === 0) {
909
+ log.info(" โ„น๏ธ No old auth data found (already clean)");
910
+ }
911
+ else {
912
+ log.success(`โœ… All auth data cleared (${deletedCount} items) - ready for new account!`);
913
+ }
914
+ }
915
+ /**
916
+ * Clear all saved authentication state (including encrypted versions)
917
+ */
918
+ async clearState() {
919
+ try {
920
+ const secureStorage = getSecureStorage();
921
+ // Delete state file (handles .json, .enc, .pqenc)
922
+ await secureStorage.delete(this.stateFilePath);
923
+ // Delete session file (handles .json, .enc, .pqenc)
924
+ await secureStorage.delete(this.sessionFilePath);
925
+ log.success("โœ… Authentication state cleared (including encrypted files)");
926
+ return true;
927
+ }
928
+ catch (error) {
929
+ log.error(`โŒ Failed to clear state: ${error}`);
930
+ return false;
931
+ }
932
+ }
933
+ /**
934
+ * HARD RESET: Completely delete ALL authentication state
935
+ */
936
+ async hardResetState() {
937
+ try {
938
+ log.warning("๐Ÿงน Performing HARD RESET of all authentication state...");
939
+ let deletedCount = 0;
940
+ // Delete state file
941
+ try {
942
+ await fs.unlink(this.stateFilePath);
943
+ log.info(` ๐Ÿ—‘๏ธ Deleted: ${this.stateFilePath}`);
944
+ deletedCount++;
945
+ }
946
+ catch {
947
+ // File doesn't exist
948
+ }
949
+ // Delete session file
950
+ try {
951
+ await fs.unlink(this.sessionFilePath);
952
+ log.info(` ๐Ÿ—‘๏ธ Deleted: ${this.sessionFilePath}`);
953
+ deletedCount++;
954
+ }
955
+ catch {
956
+ // File doesn't exist
957
+ }
958
+ // Delete entire browser_state_dir
959
+ try {
960
+ const files = await fs.readdir(CONFIG.browserStateDir);
961
+ for (const file of files) {
962
+ await fs.unlink(path.join(CONFIG.browserStateDir, file));
963
+ deletedCount++;
964
+ }
965
+ log.info(` ๐Ÿ—‘๏ธ Deleted: ${CONFIG.browserStateDir}/ (${files.length} files)`);
966
+ }
967
+ catch {
968
+ // Directory doesn't exist or empty
969
+ }
970
+ if (deletedCount === 0) {
971
+ log.info(" โ„น๏ธ No state to delete (already clean)");
972
+ }
973
+ else {
974
+ log.success(`โœ… Hard reset complete: ${deletedCount} items deleted`);
975
+ }
976
+ return true;
977
+ }
978
+ catch (error) {
979
+ log.error(`โŒ Hard reset failed: ${error}`);
980
+ return false;
981
+ }
982
+ }
983
+ }
984
+ //# sourceMappingURL=auth-manager.js.map