@ubaidbinwaris/linkedin 1.1.0 → 1.1.3
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/auth/checkpoint.js +43 -1
- package/src/login/login.js +108 -178
package/package.json
CHANGED
package/src/auth/checkpoint.js
CHANGED
|
@@ -28,4 +28,46 @@ async function detectCheckpoint(page) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Handles mobile verification/app approval prompts.
|
|
33
|
+
* Polls for success (feed navigation) for a set duration.
|
|
34
|
+
* @param {Page} page
|
|
35
|
+
* @returns {Promise<boolean>} true if resolved, false if timed out/failed
|
|
36
|
+
*/
|
|
37
|
+
async function handleMobileVerification(page) {
|
|
38
|
+
try {
|
|
39
|
+
const isMobileVerif = await page.evaluate(() => {
|
|
40
|
+
const text = document.body.innerText;
|
|
41
|
+
// User checks: "Check your LinkedIn app", "Open your LinkedIn app", "Approve the sign-in"
|
|
42
|
+
return text.includes("Check your LinkedIn app") ||
|
|
43
|
+
text.includes("Open your LinkedIn app") ||
|
|
44
|
+
text.includes("Tap Yes on the prompt") ||
|
|
45
|
+
text.includes("verification request to your device") ||
|
|
46
|
+
text.includes("Approve the sign-in");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!isMobileVerif) return false;
|
|
50
|
+
|
|
51
|
+
logger.warn("ACTION REQUIRED: Check your LinkedIn app on your phone! Waiting 2 minutes...");
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Poll for feed URL for 120 seconds
|
|
55
|
+
await page.waitForFunction(() => {
|
|
56
|
+
return window.location.href.includes("/feed") ||
|
|
57
|
+
document.querySelector('.global-nav__search');
|
|
58
|
+
}, { timeout: 120000 });
|
|
59
|
+
|
|
60
|
+
logger.info("Mobile verification successful! Resuming...");
|
|
61
|
+
return true; // Resolved
|
|
62
|
+
} catch (timeoutErr) {
|
|
63
|
+
logger.warn("Mobile verification timed out (No approval detected).");
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logger.warn(`Mobile verification check failed: ${e.message}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { detectCheckpoint, handleMobileVerification };
|
package/src/login/login.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
const { chromium } = require("playwright");
|
|
2
1
|
const logger = require("../utils/logger");
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
2
|
+
const { saveSession, SessionLock } = require("../session/sessionManager");
|
|
3
|
+
const { createBrowser, createContext } = require("../browser/launcher");
|
|
4
|
+
const { detectCheckpoint } = require("../auth/checkpoint");
|
|
5
|
+
const { performCredentialLogin, isLoggedIn } = require("../auth/actions");
|
|
6
|
+
const { randomDelay } = require("../utils/time");
|
|
5
7
|
|
|
6
8
|
const LINKEDIN_FEED = "https://www.linkedin.com/feed/";
|
|
7
|
-
const LINKEDIN_LOGIN = "https://www.linkedin.com/login";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Deterministic Login Flow wrapped in SessionLock
|
|
12
|
+
* 1. Acquire Lock (queues if busy)
|
|
13
|
+
* 2. Launch Browser
|
|
14
|
+
* 3. Load Session -> Check needsValidation
|
|
15
|
+
* 4. Login/Verify
|
|
16
|
+
* 5. Save Session (with timestamp)
|
|
11
17
|
*/
|
|
12
18
|
async function loginToLinkedIn(options = {}, credentials = null) {
|
|
13
19
|
const email = credentials?.username || process.env.LINKEDIN_EMAIL;
|
|
@@ -17,186 +23,110 @@ async function loginToLinkedIn(options = {}, credentials = null) {
|
|
|
17
23
|
throw new Error("Missing LinkedIn credentials.");
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!(await isLoggedIn(page))) {
|
|
41
|
-
throw new Error("Login failed. Could not verify authenticated state.");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await saveSession(context, email);
|
|
45
|
-
logger.info("Session saved successfully.");
|
|
46
|
-
|
|
47
|
-
return { browser, context, page };
|
|
48
|
-
|
|
49
|
-
} catch (error) {
|
|
50
|
-
logger.error(`Login process failed: ${error.message}`);
|
|
51
|
-
await browser.close();
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
module.exports = loginToLinkedIn;
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
}
|
|
66
|
-
|
|
67
|
-
async function createContext(browser, email) {
|
|
68
|
-
const storedContext = await loadSession(browser, email);
|
|
69
|
-
|
|
70
|
-
if (storedContext) {
|
|
71
|
-
logger.info("Loaded stored session.");
|
|
72
|
-
return storedContext;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
logger.info("Creating new browser context.");
|
|
76
|
-
return browser.newContext();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
async function isLoggedIn(page) {
|
|
81
|
-
try {
|
|
82
|
-
await page.waitForSelector(".global-nav__search", { timeout: 10000 });
|
|
83
|
-
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async function performCredentialLogin(page, email, password) {
|
|
91
|
-
await page.goto(LINKEDIN_LOGIN, { waitUntil: "domcontentloaded" });
|
|
92
|
-
|
|
93
|
-
await page.fill('input[name="session_key"]', email);
|
|
94
|
-
await page.fill('input[name="session_password"]', password);
|
|
95
|
-
|
|
96
|
-
await Promise.all([
|
|
97
|
-
page.waitForNavigation({ waitUntil: "domcontentloaded" }),
|
|
98
|
-
page.click('button[type="submit"]')
|
|
99
|
-
]);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
async function handleCheckpoint(page, options) {
|
|
104
|
-
// Initial check
|
|
105
|
-
if (!(await detectCheckpoint(page))) return;
|
|
106
|
-
|
|
107
|
-
logger.warn("Checkpoint detected.");
|
|
108
|
-
|
|
109
|
-
if (options.headless) {
|
|
110
|
-
logger.info("Headless mode detected. Attempting auto-resolution...");
|
|
111
|
-
|
|
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'];
|
|
119
|
-
|
|
120
|
-
const btn = candidates.find(b => {
|
|
121
|
-
const text = (b.innerText || b.value || '').trim();
|
|
122
|
-
return targetText.some(t => text.includes(t));
|
|
123
|
-
});
|
|
26
|
+
// Wrap entire process in lock
|
|
27
|
+
return SessionLock.withLoginLock(email, async () => {
|
|
28
|
+
const browser = await createBrowser(options);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// ----------------------------
|
|
32
|
+
// STEP 1: Load Session & Context
|
|
33
|
+
// ----------------------------
|
|
34
|
+
const context = await createContext(browser, email);
|
|
35
|
+
const page = await context.newPage();
|
|
36
|
+
page.setDefaultTimeout(30000);
|
|
37
|
+
|
|
38
|
+
// Check validation cache
|
|
39
|
+
// needsValidation is attached to context in sectionManager
|
|
40
|
+
if (context.needsValidation === false) {
|
|
41
|
+
logger.info(`[${email}] Session validation cached. Skipping feed check.`);
|
|
42
|
+
await page.goto(LINKEDIN_FEED, { waitUntil: "domcontentloaded" });
|
|
43
|
+
return { browser, context, page };
|
|
44
|
+
}
|
|
124
45
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
46
|
+
// ----------------------------
|
|
47
|
+
// STEP 2: Verify Session (if needed)
|
|
48
|
+
// ----------------------------
|
|
49
|
+
logger.info(`[${email}] Verifying session...`);
|
|
50
|
+
await page.goto(LINKEDIN_FEED, { waitUntil: "domcontentloaded" });
|
|
51
|
+
await randomDelay(1000, 2000);
|
|
52
|
+
|
|
53
|
+
if (await isLoggedIn(page)) {
|
|
54
|
+
logger.info(`[${email}] Session valid ✅`);
|
|
55
|
+
// Update validation timestamp
|
|
56
|
+
await saveSession(context, email, true);
|
|
57
|
+
return { browser, context, page };
|
|
128
58
|
}
|
|
129
|
-
return false;
|
|
130
|
-
});
|
|
131
59
|
|
|
132
|
-
|
|
133
|
-
logger.info("Clicked a resolution button. Waiting for navigation...");
|
|
134
|
-
await page.waitForTimeout(3000);
|
|
60
|
+
logger.info(`[${email}] Session invalid. Attempting credential login...`);
|
|
135
61
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
62
|
+
// ----------------------------
|
|
63
|
+
// STEP 3: Credential Login
|
|
64
|
+
// ----------------------------
|
|
65
|
+
await performCredentialLogin(page, email, password);
|
|
66
|
+
|
|
67
|
+
// ----------------------------
|
|
68
|
+
// STEP 4: Verify & Fail Fast
|
|
69
|
+
// ----------------------------
|
|
70
|
+
if (await detectCheckpoint(page)) {
|
|
71
|
+
// Attempt to handle mobile verification (2 min wait)
|
|
72
|
+
// If it returns true, it means we are now on the feed (resolved)
|
|
73
|
+
if (await handleMobileVerification(page)) {
|
|
74
|
+
// success, assume logged in
|
|
75
|
+
} else {
|
|
76
|
+
// Failed mobile verification.
|
|
77
|
+
// User Request: "otherwise after some delay using browser opens the browser and fill the form"
|
|
78
|
+
|
|
79
|
+
if (options.headless) {
|
|
80
|
+
logger.info("[Fallback] Mobile verification failed. Switching to VISIBLE browser for manual intervention...");
|
|
81
|
+
await browser.close();
|
|
82
|
+
|
|
83
|
+
// RE-LAUNCH in Visible Mode
|
|
84
|
+
const visibleBrowser = await createBrowser({ ...options, headless: false });
|
|
85
|
+
const visibleContext = await createContext(visibleBrowser, email);
|
|
86
|
+
const visiblePage = await visibleContext.newPage();
|
|
87
|
+
visiblePage.setDefaultTimeout(60000); // More time for manual interaction
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
logger.info("[Fallback] Filling credentials in visible browser...");
|
|
91
|
+
await performCredentialLogin(visiblePage, email, password);
|
|
92
|
+
|
|
93
|
+
// Now wait for success (Feed)
|
|
94
|
+
logger.info("[Fallback] Waiting for user to complete login manually...");
|
|
95
|
+
await visiblePage.waitForSelector(".global-nav__search", { timeout: 120000 });
|
|
96
|
+
|
|
97
|
+
if (await isLoggedIn(visiblePage)) {
|
|
98
|
+
logger.info(`[${email}] Manual Fallback Successful ✅`);
|
|
99
|
+
await saveSession(visibleContext, email, true);
|
|
100
|
+
return { browser: visibleBrowser, context: visibleContext, page: visiblePage };
|
|
101
|
+
}
|
|
102
|
+
} catch (fallbackErr) {
|
|
103
|
+
logger.error(`[Fallback] Manual intervention failed or timed out: ${fallbackErr.message}`);
|
|
104
|
+
await visibleBrowser.close();
|
|
105
|
+
throw new Error("CHECKPOINT_DETECTED_M"); // M for manual failed
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const screenshotPath = `checkpoint_${email}_${Date.now()}.png`;
|
|
110
|
+
await page.screenshot({ path: screenshotPath });
|
|
111
|
+
logger.warn(`[${email}] Checkpoint detected! Screenshot saved: ${screenshotPath}`);
|
|
112
|
+
throw new Error("CHECKPOINT_DETECTED");
|
|
113
|
+
}
|
|
140
114
|
}
|
|
141
|
-
}
|
|
142
|
-
} catch (e) {
|
|
143
|
-
logger.warn(`Auto-resolve button click failed: ${e.message}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------
|
|
147
|
-
// STRATEGY 2: Mobile App Verification (Wait & Poll)
|
|
148
|
-
// ---------------------------------------------------------
|
|
149
|
-
try {
|
|
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
115
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await page.waitForFunction(() => {
|
|
164
|
-
return window.location.href.includes("/feed") ||
|
|
165
|
-
document.querySelector('.global-nav__search');
|
|
166
|
-
}, { timeout: 120000 });
|
|
167
|
-
|
|
168
|
-
logger.info("Mobile verification successful! Resuming...");
|
|
169
|
-
return;
|
|
170
|
-
} catch (timeoutErr) {
|
|
171
|
-
logger.warn("Mobile verification timed out.");
|
|
172
|
-
}
|
|
116
|
+
if (await isLoggedIn(page)) {
|
|
117
|
+
logger.info(`[${email}] Login successful ✅`);
|
|
118
|
+
await saveSession(context, email, true); // Save with validation = true
|
|
119
|
+
return { browser, context, page };
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error("LOGIN_FAILED: Could not verify session after login attempt.");
|
|
173
122
|
}
|
|
174
|
-
} catch (e) {
|
|
175
|
-
logger.warn(`Mobile verification check failed: ${e.message}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
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
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function detectCheckpoint(page) {
|
|
193
|
-
const url = page.url().toLowerCase();
|
|
194
123
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.error(`[${email}] Login process failed: ${error.message}`);
|
|
126
|
+
await browser.close();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
201
130
|
}
|
|
202
131
|
|
|
132
|
+
module.exports = loginToLinkedIn;
|