@roomi-fields/notebooklm-mcp 1.1.2

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