@ubaidbinwaris/linkedin 1.0.12 → 1.0.14
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/package.json +1 -1
- package/src/login/login.js +153 -411
package/package.json
CHANGED
package/src/login/login.js
CHANGED
|
@@ -1,460 +1,202 @@
|
|
|
1
|
+
const { chromium } = require("playwright");
|
|
1
2
|
const logger = require("../utils/logger");
|
|
2
|
-
// Use playwright-extra with the stealth plugin
|
|
3
|
-
const { chromium } = require("playwright-extra");
|
|
4
|
-
const stealth = require("puppeteer-extra-plugin-stealth")();
|
|
5
|
-
chromium.use(stealth);
|
|
6
|
-
|
|
7
3
|
const { waitForUserResume } = require("../utils/terminal");
|
|
8
|
-
const {
|
|
9
|
-
|
|
4
|
+
const { loadSession, saveSession } = require("../session/sessionManager");
|
|
5
|
+
|
|
6
|
+
const LINKEDIN_FEED = "https://www.linkedin.com/feed/";
|
|
7
|
+
const LINKEDIN_LOGIN = "https://www.linkedin.com/login";
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @throws {Error} If credentials are missing.
|
|
10
|
+
* Main authentication entry point
|
|
14
11
|
*/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
async function loginToLinkedIn(options = {}, credentials = null) {
|
|
13
|
+
const email = credentials?.username || process.env.LINKEDIN_EMAIL;
|
|
14
|
+
const password = credentials?.password || process.env.LINKEDIN_PASSWORD;
|
|
15
|
+
|
|
16
|
+
if (!email || !password) {
|
|
17
|
+
throw new Error("Missing LinkedIn credentials.");
|
|
18
18
|
}
|
|
19
|
-
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
const browser = await createBrowser(options);
|
|
21
|
+
const context = await createContext(browser, email);
|
|
22
|
+
const page = await context.newPage();
|
|
23
|
+
|
|
24
|
+
page.setDefaultTimeout(30000);
|
|
25
|
+
|
|
27
26
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
) {
|
|
35
|
-
return true;
|
|
27
|
+
// Try session-based login
|
|
28
|
+
await page.goto(LINKEDIN_FEED, { waitUntil: "domcontentloaded" });
|
|
29
|
+
|
|
30
|
+
if (await isLoggedIn(page)) {
|
|
31
|
+
logger.info(`Session valid for ${email}`);
|
|
32
|
+
return { browser, context, page };
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
logger.info(`Session invalid. Performing credential login for ${email}`);
|
|
36
|
+
await performCredentialLogin(page, email, password);
|
|
37
|
+
|
|
38
|
+
await handleCheckpoint(page, options);
|
|
39
|
+
|
|
40
|
+
if (!(await isLoggedIn(page))) {
|
|
41
|
+
throw new Error("Login failed. Could not verify authenticated state.");
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
await saveSession(context, email);
|
|
45
|
+
logger.info("Session saved successfully.");
|
|
46
|
+
|
|
47
|
+
return { browser, context, page };
|
|
48
|
+
|
|
48
49
|
} catch (error) {
|
|
49
|
-
logger.error(`
|
|
50
|
-
|
|
50
|
+
logger.error(`Login process failed: ${error.message}`);
|
|
51
|
+
await browser.close();
|
|
52
|
+
throw error;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
* Generates random viewport dimensions.
|
|
56
|
-
*/
|
|
57
|
-
function getRandomViewport() {
|
|
58
|
-
const width = 1280 + Math.floor(Math.random() * 640); // 1280 - 1920
|
|
59
|
-
const height = 720 + Math.floor(Math.random() * 360); // 720 - 1080
|
|
60
|
-
return { width, height };
|
|
61
|
-
}
|
|
56
|
+
module.exports = loginToLinkedIn;
|
|
62
57
|
|
|
63
|
-
/**
|
|
64
|
-
* Generates a random User-Agent string (simplified for now, ideally use a library).
|
|
65
|
-
*/
|
|
66
|
-
function getRandomUserAgent() {
|
|
67
|
-
const versions = ["120.0.0.0", "121.0.0.0", "122.0.0.0"];
|
|
68
|
-
const version = versions[Math.floor(Math.random() * versions.length)];
|
|
69
|
-
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
|
|
70
|
-
}
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Main login function.
|
|
81
|
-
* @param {Object} options - Launch options for the browser.
|
|
82
|
-
* @param {boolean} [options.headless=false] - Whether to run in headless mode.
|
|
83
|
-
* @param {number} [options.slowMo=50] - Slow motion delay in ms.
|
|
84
|
-
* @param {string} [options.proxy] - Optional proxy server URL.
|
|
85
|
-
* @param {Function} [options.onCheckpoint] - Callback when verification is needed in headless mode.
|
|
86
|
-
* @param {Object} [credentials] - Optional credentials object { username, password }
|
|
87
|
-
*/
|
|
88
|
-
async function loginToLinkedIn(options = {}, credentials = null) {
|
|
89
|
-
logger.info("Starting LinkedIn login process with stealth mode...");
|
|
59
|
+
async function createBrowser(options) {
|
|
60
|
+
return chromium.launch({
|
|
61
|
+
headless: options.headless ?? false,
|
|
62
|
+
slowMo: options.slowMo ?? 50,
|
|
63
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
90
66
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const password = credentials?.password || process.env.LINKEDIN_PASSWORD;
|
|
67
|
+
async function createContext(browser, email) {
|
|
68
|
+
const storedContext = await loadSession(browser, email);
|
|
94
69
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw new Error(errorMsg);
|
|
70
|
+
if (storedContext) {
|
|
71
|
+
logger.info("Loaded stored session.");
|
|
72
|
+
return storedContext;
|
|
99
73
|
}
|
|
100
74
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"--no-sandbox",
|
|
106
|
-
"--disable-setuid-sandbox",
|
|
107
|
-
"--disable-blink-features=AutomationControlled" // Extra stealth
|
|
108
|
-
],
|
|
109
|
-
...options,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
logger.info(`Launching browser for user: ${email}...`);
|
|
113
|
-
const browser = await chromium.launch(launchOptions);
|
|
114
|
-
|
|
115
|
-
let context;
|
|
116
|
-
let page;
|
|
117
|
-
|
|
118
|
-
const contextOptions = {
|
|
119
|
-
userAgent: getRandomUserAgent(),
|
|
120
|
-
viewport: getRandomViewport(),
|
|
121
|
-
locale: 'en-US',
|
|
122
|
-
timezoneId: 'America/New_York', // Align with proxy if used, otherwise standard
|
|
123
|
-
permissions: ['geolocation'],
|
|
124
|
-
ignoreHTTPSErrors: true,
|
|
125
|
-
};
|
|
75
|
+
logger.info("Creating new browser context.");
|
|
76
|
+
return browser.newContext();
|
|
77
|
+
}
|
|
78
|
+
|
|
126
79
|
|
|
80
|
+
async function isLoggedIn(page) {
|
|
127
81
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (context) {
|
|
135
|
-
logger.info("Session stored context created.");
|
|
136
|
-
} else {
|
|
137
|
-
logger.info("No valid session found. Starting fresh context.");
|
|
138
|
-
context = await browser.newContext(contextOptions);
|
|
139
|
-
}
|
|
82
|
+
await page.waitForSelector(".global-nav__search", { timeout: 10000 });
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
140
88
|
|
|
141
|
-
page = await context.newPage();
|
|
142
89
|
|
|
143
|
-
|
|
144
|
-
|
|
90
|
+
async function performCredentialLogin(page, email, password) {
|
|
91
|
+
await page.goto(LINKEDIN_LOGIN, { waitUntil: "domcontentloaded" });
|
|
145
92
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
waitUntil: "domcontentloaded",
|
|
149
|
-
});
|
|
93
|
+
await page.fill('input[name="session_key"]', email);
|
|
94
|
+
await page.fill('input[name="session_password"]', password);
|
|
150
95
|
|
|
151
|
-
|
|
152
|
-
|
|
96
|
+
await Promise.all([
|
|
97
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded" }),
|
|
98
|
+
page.click('button[type="submit"]')
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
153
101
|
|
|
154
|
-
// Check for checkpoint immediately after navigation
|
|
155
|
-
if (await detectCheckpoint(page)) {
|
|
156
|
-
if (launchOptions.headless) {
|
|
157
|
-
logger.warn("Checkpoint detected in headless mode.");
|
|
158
|
-
|
|
159
|
-
// Attempt to resolve simple checkpoints (e.g. "Yes, it's me", "Skip") automatically
|
|
160
|
-
try {
|
|
161
|
-
logger.info("Attempting to resolve simple checkpoint headlessly...");
|
|
162
|
-
const simpleResolved = await page.evaluate(async () => {
|
|
163
|
-
// Find buttons, links, or elements with button role
|
|
164
|
-
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]'));
|
|
165
|
-
const targetText = ['Yes', 'Skip', 'Not now', 'Continue', 'Sign in', 'Verify', 'Let’s do it', 'Next'];
|
|
166
|
-
|
|
167
|
-
// Find a candidate with one of these texts
|
|
168
|
-
const btn = candidates.find(b => {
|
|
169
|
-
const text = (b.innerText || b.value || '').trim();
|
|
170
|
-
return targetText.some(t => text.includes(t));
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (btn) {
|
|
174
|
-
btn.click();
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (simpleResolved) {
|
|
181
|
-
logger.info("Clicked a resolution button. Waiting to see if it clears...");
|
|
182
|
-
await randomDelay(2000, 4000);
|
|
183
|
-
if (!(await detectCheckpoint(page))) {
|
|
184
|
-
logger.info("Checkpoint resolved headlessly! Proceeding...");
|
|
185
|
-
try {
|
|
186
|
-
await page.waitForURL("**/feed**", { timeout: 10000 });
|
|
187
|
-
return { browser, context, page };
|
|
188
|
-
} catch (e) {
|
|
189
|
-
logger.warn("Resolved checkpoint but feed did not load. Continuing...");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
// Check for Mobile Verification ("Open your LinkedIn app")
|
|
194
|
-
const isMobileVerif = await page.evaluate(() => {
|
|
195
|
-
const text = document.body.innerText;
|
|
196
|
-
return text.includes("Open your LinkedIn app") ||
|
|
197
|
-
text.includes("Tap Yes on the prompt") ||
|
|
198
|
-
text.includes("verification request to your device");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (isMobileVerif) {
|
|
202
|
-
logger.info("Mobile verification detected (Open App / Tap Yes).");
|
|
203
|
-
logger.info("Waiting 2 minutes for you to approve on your device...");
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
// Poll for feed URL for 120 seconds
|
|
207
|
-
await page.waitForFunction(() => {
|
|
208
|
-
return window.location.href.includes("/feed") ||
|
|
209
|
-
document.querySelector('.global-nav__search');
|
|
210
|
-
}, { timeout: 120000 });
|
|
211
|
-
|
|
212
|
-
logger.info("Mobile verification successful! Resuming...");
|
|
213
|
-
return { browser, context, page };
|
|
214
|
-
} catch (err) {
|
|
215
|
-
logger.warn("Mobile verification timed out. Falling back to visible mode.");
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (err) {
|
|
220
|
-
logger.warn(`Failed to auto-resolve checkpoint: ${err.message}`);
|
|
221
|
-
}
|
|
222
102
|
|
|
103
|
+
async function handleCheckpoint(page, options) {
|
|
104
|
+
// Initial check
|
|
105
|
+
if (!(await detectCheckpoint(page))) return;
|
|
223
106
|
|
|
224
|
-
|
|
225
|
-
logger.info("Triggering onCheckpoint callback...");
|
|
226
|
-
await options.onCheckpoint();
|
|
227
|
-
logger.info("onCheckpoint resolved. Retrying login...");
|
|
228
|
-
await browser.close();
|
|
229
|
-
return loginToLinkedIn(options, credentials);
|
|
230
|
-
}
|
|
107
|
+
logger.warn("Checkpoint detected.");
|
|
231
108
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
|
|
235
|
-
logger.info("Automatically launching visible browser for verification...");
|
|
236
|
-
|
|
237
|
-
logger.info("Closing headless browser...");
|
|
238
|
-
await browser.close();
|
|
109
|
+
if (options.headless) {
|
|
110
|
+
logger.info("Headless mode detected. Attempting auto-resolution...");
|
|
239
111
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
112
|
+
// ---------------------------------------------------------
|
|
113
|
+
// STRATEGY 1: Click Simple Buttons (Yes, Skip, Continue)
|
|
114
|
+
// ---------------------------------------------------------
|
|
115
|
+
try {
|
|
116
|
+
const clicked = await page.evaluate(() => {
|
|
117
|
+
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]'));
|
|
118
|
+
const targetText = ['Yes', 'Skip', 'Not now', 'Continue', 'Sign in', 'Verify', 'Let’s do it', 'Next'];
|
|
244
119
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
120
|
+
const btn = candidates.find(b => {
|
|
121
|
+
const text = (b.innerText || b.value || '').trim();
|
|
122
|
+
return targetText.some(t => text.includes(t));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (btn) {
|
|
126
|
+
btn.click();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (clicked) {
|
|
133
|
+
logger.info("Clicked a resolution button. Waiting for navigation...");
|
|
134
|
+
await page.waitForTimeout(3000);
|
|
249
135
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
logger.warn("Checkpoint detected immediately. Manual verification required.");
|
|
256
|
-
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
257
|
-
await options.onCheckpoint();
|
|
258
|
-
} else {
|
|
259
|
-
await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
|
|
260
|
-
}
|
|
136
|
+
// Check if resolved
|
|
137
|
+
if (!(await detectCheckpoint(page))) {
|
|
138
|
+
logger.info("Checkpoint resolved via button click!");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
261
141
|
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
logger.warn(`Auto-resolve button click failed: ${e.message}`);
|
|
262
144
|
}
|
|
263
145
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
146
|
+
// ---------------------------------------------------------
|
|
147
|
+
// STRATEGY 2: Mobile App Verification (Wait & Poll)
|
|
148
|
+
// ---------------------------------------------------------
|
|
267
149
|
try {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return { browser, context, page };
|
|
279
|
-
} else {
|
|
280
|
-
logger.info("Session invalid or redirected to login page.");
|
|
281
|
-
}
|
|
282
|
-
} catch (err) {
|
|
283
|
-
logger.info("Could not verify session state. Proceeding to credential login.");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// CRITICAL FIX: If session was invalid, we MUST close the current context
|
|
287
|
-
// and start fresh. Otherwise, retained cookies might cause redirect loops
|
|
288
|
-
// (e.g. we go to /login, but LinkedIn sees cookies and redirects to /feed,
|
|
289
|
-
// so we can't find the email input).
|
|
290
|
-
logger.info("Closing invalid/expired session context...");
|
|
291
|
-
await context.close();
|
|
292
|
-
|
|
293
|
-
logger.info("Starting fresh context for credential login...");
|
|
294
|
-
context = await browser.newContext(contextOptions);
|
|
295
|
-
page = await context.newPage();
|
|
296
|
-
page.setDefaultTimeout(30000);
|
|
297
|
-
|
|
298
|
-
// -----------------------------
|
|
299
|
-
// STEP 2: Credential Login
|
|
300
|
-
// -----------------------------
|
|
301
|
-
logger.info("Proceeding to credential login...");
|
|
302
|
-
|
|
303
|
-
if (!page.url().includes("login") && !page.url().includes("uas/request-password-reset")) {
|
|
304
|
-
await page.goto("https://www.linkedin.com/login", { waitUntil: 'domcontentloaded' });
|
|
305
|
-
await randomDelay(1000, 2000);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
logger.info("Entering credentials...");
|
|
309
|
-
|
|
310
|
-
// Simulate human typing
|
|
311
|
-
await page.click('input[name="session_key"]');
|
|
312
|
-
await randomDelay(500, 1000);
|
|
313
|
-
await page.type('input[name="session_key"]', email, { delay: 100 }); // Type with delay
|
|
314
|
-
|
|
315
|
-
await randomDelay(1000, 2000);
|
|
316
|
-
|
|
317
|
-
await page.click('input[name="session_password"]');
|
|
318
|
-
await page.type('input[name="session_password"]', password, { delay: 100 });
|
|
319
|
-
|
|
320
|
-
await randomDelay(1000, 2000);
|
|
321
|
-
|
|
322
|
-
logger.info("Submitting login form...");
|
|
323
|
-
await Promise.all([
|
|
324
|
-
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
325
|
-
page.click('button[type="submit"]')
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
// Check for checkpoint again
|
|
329
|
-
if (await detectCheckpoint(page)) {
|
|
330
|
-
if (launchOptions.headless) {
|
|
331
|
-
logger.warn("Checkpoint detected in headless mode (post-login).");
|
|
332
|
-
|
|
333
|
-
// Attempt to resolve simple checkpoints (e.g. "Yes, it's me", "Skip") automatically
|
|
150
|
+
const isMobileVerif = await page.evaluate(() => {
|
|
151
|
+
const text = document.body.innerText;
|
|
152
|
+
return text.includes("Open your LinkedIn app") ||
|
|
153
|
+
text.includes("Tap Yes on the prompt") ||
|
|
154
|
+
text.includes("verification request to your device");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (isMobileVerif) {
|
|
158
|
+
logger.info("Mobile verification detected. Waiting 2 minutes for manual approval on device...");
|
|
159
|
+
|
|
334
160
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// Find buttons, links, or elements with button role
|
|
338
|
-
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]'));
|
|
339
|
-
const targetText = ['Yes', 'Skip', 'Not now', 'Continue', 'Sign in', 'Verify', 'Let’s do it', 'Next'];
|
|
340
|
-
|
|
341
|
-
// Find a candidate with one of these texts
|
|
342
|
-
const btn = candidates.find(b => {
|
|
343
|
-
const text = (b.innerText || b.value || '').trim();
|
|
344
|
-
return targetText.some(t => text.includes(t));
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (btn) {
|
|
348
|
-
btn.click();
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
return false;
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (simpleResolved) {
|
|
355
|
-
logger.info("Clicked a resolution button. Waiting to see if it clears...");
|
|
356
|
-
await randomDelay(2000, 4000);
|
|
357
|
-
if (!(await detectCheckpoint(page))) {
|
|
358
|
-
logger.info("Checkpoint resolved headlessly! Proceeding...");
|
|
359
|
-
// Re-verify session
|
|
360
|
-
try {
|
|
361
|
-
await page.waitForURL("**/feed**", { timeout: 10000 });
|
|
362
|
-
return { browser, context, page };
|
|
363
|
-
} catch (e) {
|
|
364
|
-
logger.warn("Resolved checkpoint but feed did not load. Continuing...");
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
} catch (err) {
|
|
369
|
-
logger.warn(`Failed to auto-resolve post-login checkpoint: ${err.message}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
373
|
-
logger.info("Triggering onCheckpoint callback...");
|
|
374
|
-
await options.onCheckpoint();
|
|
375
|
-
logger.info("onCheckpoint resolved. Retrying login...");
|
|
376
|
-
await browser.close();
|
|
377
|
-
return loginToLinkedIn(options, credentials);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
logger.info("Switching to visible mode for manual verification...");
|
|
381
|
-
|
|
382
|
-
// await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
|
|
383
|
-
logger.info("Automatically launching visible browser for verification...");
|
|
384
|
-
|
|
385
|
-
logger.info("Closing headless browser...");
|
|
386
|
-
await browser.close();
|
|
387
|
-
|
|
388
|
-
logger.info("Launching visible browser for verification...");
|
|
389
|
-
const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
|
|
390
|
-
|
|
391
|
-
logger.info("Verification successful. Resuming headless session...");
|
|
392
|
-
await visibleInstance.browser.close();
|
|
393
|
-
|
|
394
|
-
return loginToLinkedIn(options, { username: email, password });
|
|
395
|
-
} else {
|
|
396
|
-
logger.warn("Checkpoint detected after login attempt. Manual verification required.");
|
|
397
|
-
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
398
|
-
await options.onCheckpoint();
|
|
399
|
-
} else {
|
|
400
|
-
logger.info("Waiting for manual verification in the opened browser...");
|
|
401
|
-
logger.info("Please solve the CAPTCHA/verification. The browser will close automatically when you are redirected to the feed.");
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
// Wait for URL to include '/feed' OR any validation selector to appear
|
|
161
|
+
// Poll for feed URL for 120 seconds
|
|
162
|
+
// We use a loop or waitForFunction
|
|
405
163
|
await page.waitForFunction(() => {
|
|
406
164
|
return window.location.href.includes("/feed") ||
|
|
407
|
-
document.querySelector('.global-nav__search')
|
|
408
|
-
|
|
409
|
-
}, { timeout: 300000 }); // 5 minutes timeout
|
|
410
|
-
|
|
411
|
-
logger.info("Verification detected! resuming...");
|
|
412
|
-
} catch (err) {
|
|
413
|
-
logger.error("Timeout waiting for manual verification.");
|
|
414
|
-
throw new Error("Manual verification timed out.");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
165
|
+
document.querySelector('.global-nav__search');
|
|
166
|
+
}, { timeout: 120000 });
|
|
419
167
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
await page.waitForURL("**/feed**", { timeout: 20000 });
|
|
426
|
-
|
|
427
|
-
// Wait for at least one validation selector
|
|
428
|
-
await Promise.any(VALIDATION_SELECTORS.map(selector =>
|
|
429
|
-
page.waitForSelector(selector, { timeout: 15000 })
|
|
430
|
-
));
|
|
431
|
-
|
|
432
|
-
logger.info("Login confirmed ✅");
|
|
433
|
-
|
|
434
|
-
// Save session state
|
|
435
|
-
await saveSession(context, email);
|
|
436
|
-
logger.info("Session state saved 💾");
|
|
437
|
-
|
|
438
|
-
return { browser, context, page };
|
|
439
|
-
|
|
440
|
-
} catch (err) {
|
|
441
|
-
logger.error("Login failed or timed out waiting for feed.");
|
|
442
|
-
const screenshotPath = `error_login_${Date.now()}.png`;
|
|
443
|
-
try {
|
|
444
|
-
await page.screenshot({ path: screenshotPath });
|
|
445
|
-
logger.info(`Screenshot saved to ${screenshotPath}`);
|
|
446
|
-
} catch (opts) {
|
|
447
|
-
console.error("Failed to take error screenshot");
|
|
168
|
+
logger.info("Mobile verification successful! Resuming...");
|
|
169
|
+
return;
|
|
170
|
+
} catch (timeoutErr) {
|
|
171
|
+
logger.warn("Mobile verification timed out.");
|
|
172
|
+
}
|
|
448
173
|
}
|
|
449
|
-
|
|
450
|
-
|
|
174
|
+
} catch (e) {
|
|
175
|
+
logger.warn(`Mobile verification check failed: ${e.message}`);
|
|
451
176
|
}
|
|
452
177
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
178
|
+
// Re-check after attempts
|
|
179
|
+
if (await detectCheckpoint(page)) {
|
|
180
|
+
throw new Error("Checkpoint detected in headless mode and auto-resolution failed.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
} else {
|
|
184
|
+
// Visible mode
|
|
185
|
+
logger.warn("Verification required. Please complete manually.");
|
|
186
|
+
await waitForUserResume(
|
|
187
|
+
"Complete verification in browser, then press ENTER..."
|
|
188
|
+
);
|
|
457
189
|
}
|
|
458
190
|
}
|
|
459
191
|
|
|
460
|
-
|
|
192
|
+
async function detectCheckpoint(page) {
|
|
193
|
+
const url = page.url().toLowerCase();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
url.includes("checkpoint") ||
|
|
197
|
+
url.includes("challenge") ||
|
|
198
|
+
url.includes("verification") ||
|
|
199
|
+
url.includes("consumer-login/error")
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|