@ubaidbinwaris/linkedin 1.0.11 → 1.0.13

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/login/login.js +93 -401
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubaidbinwaris/linkedin",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,434 +1,126 @@
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 { sessionExists, saveSession, loadSession } = require("../session/sessionManager");
9
- const { randomDelay } = require("../utils/time");
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
- * Validates that necessary environment variables are set.
13
- * @throws {Error} If credentials are missing.
10
+ * Main authentication entry point
14
11
  */
15
- function validateCredentials() {
16
- if (!process.env.LINKEDIN_EMAIL || !process.env.LINKEDIN_PASSWORD) {
17
- throw new Error("Missing LINKEDIN_EMAIL or LINKEDIN_PASSWORD in environment variables.");
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
- * Detects if a checkpoint/verification is triggered.
23
- * @param {import('playwright').Page} page
24
- * @returns {Promise<boolean>}
25
- */
26
- async function detectCheckpoint(page) {
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
- const url = page.url();
29
- if (
30
- url.includes("checkpoint") ||
31
- url.includes("challenge") ||
32
- url.includes("verification") ||
33
- url.includes("consumer-login/error")
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
- // Also check for specific checkpoint elements if URL check isn't enough
39
- // This is a non-blocking check with a short timeout
40
- try {
41
- await page.waitForSelector("h1:has-text('Security Verification')", { timeout: 1000 });
42
- return true;
43
- } catch (e) {
44
- // Element not found, likely no checkpoint
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
- return false;
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(`Error during checkpoint detection: ${error.message}`);
50
- return false;
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
- * Main login function.
74
- * @param {Object} options - Launch options for the browser.
75
- * @param {boolean} [options.headless=false] - Whether to run in headless mode.
76
- * @param {number} [options.slowMo=50] - Slow motion delay in ms.
77
- * @param {string} [options.proxy] - Optional proxy server URL.
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
- // Determine credentials
92
- const email = credentials?.username || process.env.LINKEDIN_EMAIL;
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 (!email || !password) {
96
- const errorMsg = "Missing credentials. Provide them in arguments or set LINKEDIN_EMAIL/LINKEDIN_PASSWORD env vars.";
97
- logger.error(errorMsg);
98
- throw new Error(errorMsg);
70
+ if (storedContext) {
71
+ logger.info("Loaded stored session.");
72
+ return storedContext;
99
73
  }
100
74
 
101
- const launchOptions = {
102
- headless: options.headless !== undefined ? options.headless : false,
103
- slowMo: options.slowMo || 50,
104
- args: [
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
+ }
126
78
 
79
+
80
+ async function isLoggedIn(page) {
127
81
  try {
128
- // -----------------------------
129
- // STEP 1: Try Using Saved Session
130
- // -----------------------------
131
- logger.info(`Checking for saved session for ${email}...`);
132
- context = await loadSession(browser, contextOptions, email);
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
-
143
- // Set a default timeout for all actions
144
- page.setDefaultTimeout(30000);
145
-
146
- logger.info("Navigating to LinkedIn feed...");
147
- await page.goto("https://www.linkedin.com/feed/", {
148
- waitUntil: "domcontentloaded",
149
- });
150
-
151
- // Human-like pause
152
- await randomDelay(2000, 4000);
153
-
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
- // Re-verify session
186
- try {
187
- await page.waitForURL("**/feed**", { timeout: 10000 });
188
- return { browser, context, page };
189
- } catch (e) {
190
- logger.warn("Resolved checkpoint but feed did not load. Continuing...");
191
- }
192
- }
193
- }
194
- } catch (err) {
195
- logger.warn(`Failed to auto-resolve checkpoint: ${err.message}`);
196
- }
197
-
198
- if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
199
- logger.info("Triggering onCheckpoint callback...");
200
- await options.onCheckpoint();
201
- logger.info("onCheckpoint resolved. Retrying login...");
202
- await browser.close();
203
- return loginToLinkedIn(options, credentials);
204
- }
205
-
206
- logger.info("Switching to visible mode for manual verification...");
207
-
208
- // await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
209
- logger.info("Automatically launching visible browser for verification...");
210
-
211
- logger.info("Closing headless browser...");
212
- await browser.close();
213
-
214
- logger.info("Launching visible browser for verification...");
215
- // Call recursively in visible mode
216
- // We pass the same options but force headless: false
217
- const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
218
-
219
- // Once the visible instance returns, it means login was successful and session is saved.
220
- logger.info("Verification successful in visible mode.");
221
- logger.info("Closing visible browser and resuming headless session...");
222
- await visibleInstance.browser.close();
223
-
224
- // Restart the original headless request.
225
- // It should now find the valid session and proceed without checkpoints.
226
- return loginToLinkedIn(options, { username: email, password });
227
-
228
- } else {
229
- logger.warn("Checkpoint detected immediately. Manual verification required.");
230
- if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
231
- await options.onCheckpoint();
232
- } else {
233
- await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
234
- }
235
- }
236
- }
237
89
 
238
- const { VALIDATION_SELECTORS } = require("../config");
239
-
240
- // Verify Session Validity
241
- try {
242
- // Check for any of the validation selectors
243
- const isLoggedIn = await Promise.race([
244
- Promise.any(VALIDATION_SELECTORS.map(selector =>
245
- page.waitForSelector(selector, { timeout: 15000 }).then(() => true)
246
- )),
247
- page.waitForSelector('.login-form, #username, input[name="session_key"]', { timeout: 5000 }).then(() => false)
248
- ]).catch(() => false);
249
-
250
- if (isLoggedIn) {
251
- logger.info("Session is valid. Login successful.");
252
- return { browser, context, page };
253
- } else {
254
- logger.info("Session invalid or redirected to login page.");
255
- }
256
- } catch (err) {
257
- logger.info("Could not verify session state. Proceeding to credential login.");
258
- }
90
+ async function performCredentialLogin(page, email, password) {
91
+ await page.goto(LINKEDIN_LOGIN, { waitUntil: "domcontentloaded" });
259
92
 
260
- // CRITICAL FIX: If session was invalid, we MUST close the current context
261
- // and start fresh. Otherwise, retained cookies might cause redirect loops
262
- // (e.g. we go to /login, but LinkedIn sees cookies and redirects to /feed,
263
- // so we can't find the email input).
264
- logger.info("Closing invalid/expired session context...");
265
- await context.close();
266
-
267
- logger.info("Starting fresh context for credential login...");
268
- context = await browser.newContext(contextOptions);
269
- page = await context.newPage();
270
- page.setDefaultTimeout(30000);
271
-
272
- // -----------------------------
273
- // STEP 2: Credential Login
274
- // -----------------------------
275
- logger.info("Proceeding to credential login...");
276
-
277
- if (!page.url().includes("login") && !page.url().includes("uas/request-password-reset")) {
278
- await page.goto("https://www.linkedin.com/login", { waitUntil: 'domcontentloaded' });
279
- await randomDelay(1000, 2000);
280
- }
93
+ await page.fill('input[name="session_key"]', email);
94
+ await page.fill('input[name="session_password"]', password);
281
95
 
282
- logger.info("Entering credentials...");
283
-
284
- // Simulate human typing
285
- await page.click('input[name="session_key"]');
286
- await randomDelay(500, 1000);
287
- await page.type('input[name="session_key"]', email, { delay: 100 }); // Type with delay
288
-
289
- await randomDelay(1000, 2000);
290
-
291
- await page.click('input[name="session_password"]');
292
- await page.type('input[name="session_password"]', password, { delay: 100 });
293
-
294
- await randomDelay(1000, 2000);
295
-
296
- logger.info("Submitting login form...");
297
- await Promise.all([
298
- page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
299
- page.click('button[type="submit"]')
300
- ]);
301
-
302
- // Check for checkpoint again
303
- if (await detectCheckpoint(page)) {
304
- if (launchOptions.headless) {
305
- logger.warn("Checkpoint detected in headless mode (post-login).");
306
-
307
- // Attempt to resolve simple checkpoints (e.g. "Yes, it's me", "Skip") automatically
308
- try {
309
- logger.info("Attempting to resolve simple checkpoint headlessly (post-login)...");
310
- const simpleResolved = await page.evaluate(async () => {
311
- // Find buttons, links, or elements with button role
312
- const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]'));
313
- const targetText = ['Yes', 'Skip', 'Not now', 'Continue', 'Sign in', 'Verify', 'Let’s do it', 'Next'];
314
-
315
- // Find a candidate with one of these texts
316
- const btn = candidates.find(b => {
317
- const text = (b.innerText || b.value || '').trim();
318
- return targetText.some(t => text.includes(t));
319
- });
320
-
321
- if (btn) {
322
- btn.click();
323
- return true;
324
- }
325
- return false;
326
- });
327
-
328
- if (simpleResolved) {
329
- logger.info("Clicked a resolution button. Waiting to see if it clears...");
330
- await randomDelay(2000, 4000);
331
- if (!(await detectCheckpoint(page))) {
332
- logger.info("Checkpoint resolved headlessly! Proceeding...");
333
- // Re-verify session
334
- try {
335
- await page.waitForURL("**/feed**", { timeout: 10000 });
336
- return { browser, context, page };
337
- } catch (e) {
338
- logger.warn("Resolved checkpoint but feed did not load. Continuing...");
339
- }
340
- }
341
- }
342
- } catch (err) {
343
- logger.warn(`Failed to auto-resolve post-login checkpoint: ${err.message}`);
344
- }
345
-
346
- if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
347
- logger.info("Triggering onCheckpoint callback...");
348
- await options.onCheckpoint();
349
- logger.info("onCheckpoint resolved. Retrying login...");
350
- await browser.close();
351
- return loginToLinkedIn(options, credentials);
352
- }
353
-
354
- logger.info("Switching to visible mode for manual verification...");
355
-
356
- // await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
357
- logger.info("Automatically launching visible browser for verification...");
358
-
359
- logger.info("Closing headless browser...");
360
- await browser.close();
361
-
362
- logger.info("Launching visible browser for verification...");
363
- const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
364
-
365
- logger.info("Verification successful. Resuming headless session...");
366
- await visibleInstance.browser.close();
367
-
368
- return loginToLinkedIn(options, { username: email, password });
369
- } else {
370
- logger.warn("Checkpoint detected after login attempt. Manual verification required.");
371
- if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
372
- await options.onCheckpoint();
373
- } else {
374
- logger.info("Waiting for manual verification in the opened browser...");
375
- logger.info("Please solve the CAPTCHA/verification. The browser will close automatically when you are redirected to the feed.");
376
-
377
- try {
378
- // Wait for URL to include '/feed' OR any validation selector to appear
379
- await page.waitForFunction(() => {
380
- return window.location.href.includes("/feed") ||
381
- document.querySelector('.global-nav__search') ||
382
- document.querySelector('#global-nav-typeahead');
383
- }, { timeout: 300000 }); // 5 minutes timeout
384
-
385
- logger.info("Verification detected! resuming...");
386
- } catch (err) {
387
- logger.error("Timeout waiting for manual verification.");
388
- throw new Error("Manual verification timed out.");
389
- }
390
- }
391
- }
392
- }
96
+ await Promise.all([
97
+ page.waitForNavigation({ waitUntil: "domcontentloaded" }),
98
+ page.click('button[type="submit"]')
99
+ ]);
100
+ }
393
101
 
394
- // -----------------------------
395
- // Post-Login Verification
396
- // -----------------------------
397
- logger.info("Verifying login success...");
398
- try {
399
- await page.waitForURL("**/feed**", { timeout: 20000 });
400
-
401
- // Wait for at least one validation selector
402
- await Promise.any(VALIDATION_SELECTORS.map(selector =>
403
- page.waitForSelector(selector, { timeout: 15000 })
404
- ));
405
-
406
- logger.info("Login confirmed ✅");
407
-
408
- // Save session state
409
- await saveSession(context, email);
410
- logger.info("Session state saved 💾");
411
-
412
- return { browser, context, page };
413
-
414
- } catch (err) {
415
- logger.error("Login failed or timed out waiting for feed.");
416
- const screenshotPath = `error_login_${Date.now()}.png`;
417
- try {
418
- await page.screenshot({ path: screenshotPath });
419
- logger.info(`Screenshot saved to ${screenshotPath}`);
420
- } catch (opts) {
421
- console.error("Failed to take error screenshot");
422
- }
423
-
424
- throw new Error("Login failed: Could not reach feed page.");
425
- }
426
102
 
427
- } catch (error) {
428
- logger.error(`Critical error in loginToLinkedIn: ${error.message}`);
429
- if (browser) await browser.close();
430
- throw error;
103
+ async function handleCheckpoint(page, options) {
104
+ if (!(await detectCheckpoint(page))) return;
105
+
106
+ if (options.headless) {
107
+ throw new Error("Checkpoint detected in headless mode.");
431
108
  }
109
+
110
+ logger.warn("Verification required. Please complete manually.");
111
+
112
+ await waitForUserResume(
113
+ "Complete verification in browser, then press ENTER..."
114
+ );
115
+ }
116
+
117
+ async function detectCheckpoint(page) {
118
+ const url = page.url().toLowerCase();
119
+
120
+ return (
121
+ url.includes("checkpoint") ||
122
+ url.includes("challenge") ||
123
+ url.includes("verification")
124
+ );
432
125
  }
433
126
 
434
- module.exports = loginToLinkedIn;