@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.
- package/LICENSE +22 -0
- package/README.md +548 -0
- package/dist/auth/auth-manager.d.ts +139 -0
- package/dist/auth/auth-manager.d.ts.map +1 -0
- package/dist/auth/auth-manager.js +981 -0
- package/dist/auth/auth-manager.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +216 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-wrapper.d.ts +8 -0
- package/dist/http-wrapper.d.ts.map +1 -0
- package/dist/http-wrapper.js +221 -0
- package/dist/http-wrapper.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +499 -0
- package/dist/index.js.map +1 -0
- package/dist/library/notebook-library.d.ts +81 -0
- package/dist/library/notebook-library.d.ts.map +1 -0
- package/dist/library/notebook-library.js +362 -0
- package/dist/library/notebook-library.js.map +1 -0
- package/dist/library/types.d.ts +67 -0
- package/dist/library/types.d.ts.map +1 -0
- package/dist/library/types.js +8 -0
- package/dist/library/types.js.map +1 -0
- package/dist/session/browser-session.d.ts +108 -0
- package/dist/session/browser-session.d.ts.map +1 -0
- package/dist/session/browser-session.js +630 -0
- package/dist/session/browser-session.js.map +1 -0
- package/dist/session/session-manager.d.ts +76 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +273 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/session/shared-context-manager.d.ts +107 -0
- package/dist/session/shared-context-manager.d.ts.map +1 -0
- package/dist/session/shared-context-manager.js +447 -0
- package/dist/session/shared-context-manager.js.map +1 -0
- package/dist/tools/index.d.ts +225 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +1396 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cleanup-manager.d.ts +133 -0
- package/dist/utils/cleanup-manager.d.ts.map +1 -0
- package/dist/utils/cleanup-manager.js +673 -0
- package/dist/utils/cleanup-manager.js.map +1 -0
- package/dist/utils/logger.d.ts +61 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/page-utils.d.ts +54 -0
- package/dist/utils/page-utils.d.ts.map +1 -0
- package/dist/utils/page-utils.js +422 -0
- package/dist/utils/page-utils.js.map +1 -0
- package/dist/utils/stealth-utils.d.ts +135 -0
- package/dist/utils/stealth-utils.d.ts.map +1 -0
- package/dist/utils/stealth-utils.js +398 -0
- package/dist/utils/stealth-utils.js.map +1 -0
- 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
|