@ubaidbinwaris/linkedin-login 2.1.0

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/Readme.md ADDED
@@ -0,0 +1,156 @@
1
+ # @ubaidbinwaris/linkedin - Enterprise Automation Service
2
+
3
+ ![Version](https://img.shields.io/npm/v/@ubaidbinwaris/linkedin?style=flat-square)
4
+ ![License](https://img.shields.io/npm/l/@ubaidbinwaris/linkedin?style=flat-square)
5
+ ![Node](https://img.shields.io/node/v/@ubaidbinwaris/linkedin?style=flat-square)
6
+
7
+ A professional, deterministic backend service library for managing multiple LinkedIn accounts with strict concurrency control, session isolation, and enterprise-grade security.
8
+
9
+ > **v1.1.7 Update**: Introduces "Smart Mobile Verification" with Visible Browser Fallback.
10
+
11
+ ## 🚀 Key Features
12
+
13
+ * **🛡️ Multi-User Concurrency**: Built-in `SessionLock` prevents race conditions. Impossible to double-login the same user.
14
+ * **🔒 Enterprise Security**:
15
+ * Sessions stored as `SHA-256` hashed filenames (GDPR/Privacy friendly).
16
+ * Data encrypted with `AES-256-CBC` before storage.
17
+ * **🧠 Smart Validation**:
18
+ * Caches validation checks for 10 minutes to minimize ban risk from excessive reloading.
19
+ * Automatically refreshes stale sessions.
20
+ * **📱 Mobile & Fallback Support**:
21
+ * **Phase 1**: Detects "Open LinkedIn App" prompt and waits 2 minutes for user approval.
22
+ * **Phase 2**: If mobile fails, automatically launches a **Visible Browser** for manual intervention.
23
+
24
+ ## 🏗️ Architecture
25
+
26
+ The library follows a strict **Fail-Fast** or **Resolution** flow. It does not use "stealth" plugins, relying instead on standard browser behavior and human intervention protocols.
27
+
28
+ ```mermaid
29
+ sequenceDiagram
30
+ participant API as API/Worker
31
+ participant Pkg as LinkedIn Package
32
+ participant Browser as Playwright
33
+ participant Store as Session Store
34
+
35
+ API->>Pkg: loginToLinkedIn(email, pass)
36
+ Pkg->>Pkg: Acquire Lock (SessionLock)
37
+ alt is Locked
38
+ Pkg-->>API: Throw BUSY Error
39
+ end
40
+
41
+ Pkg->>Browser: Launch (Headless)
42
+ Pkg->>Store: Load Context
43
+
44
+ alt Session Valid & Recent
45
+ Pkg-->>API: Return Page (Skip Feed)
46
+ else Session Stale/Invalid
47
+ Pkg->>Browser: Goto Feed
48
+
49
+ alt Login Required
50
+ Pkg->>Browser: Fill Credentials
51
+ Pkg->>Browser: Submit
52
+
53
+ opt Checkpoint Detected
54
+ Pkg->>Browser: Check Mobile Prompt
55
+ alt Mobile Prompt Found
56
+ Pkg->>Pkg: Wait 2 Mins for Feed URL
57
+ end
58
+
59
+ alt Mobile Failed
60
+ Pkg->>Browser: Close Headless
61
+ Pkg->>Browser: Launch VISIBLE Browser
62
+ Pkg->>Browser: Re-Fill Credentials
63
+ Pkg->>Pkg: Wait for Manual Use
64
+ end
65
+ end
66
+ end
67
+
68
+ Pkg->>Store: Save Session (Encrypted)
69
+ Pkg-->>API: Return Page
70
+ end
71
+ ```
72
+
73
+ ## 📦 Installation
74
+
75
+ ```bash
76
+ npm install @ubaidbinwaris/linkedin
77
+ ```
78
+
79
+ ## 💻 Usage
80
+
81
+ ### 1. Basic Implementation
82
+ The simplest way to use the package. Locks and session management are handled automatically.
83
+
84
+ ```javascript
85
+ const { loginToLinkedIn } = require('@ubaidbinwaris/linkedin');
86
+
87
+ (async () => {
88
+ try {
89
+ const { browser, page } = await loginToLinkedIn({
90
+ headless: true // Will auto-switch to false if fallback needed
91
+ }, {
92
+ username: 'alice@example.com',
93
+ password: 'secure_password'
94
+ });
95
+
96
+ console.log("✅ Logged in successfully!");
97
+
98
+ // ... Perform scraping/automation tasks ...
99
+
100
+ await browser.close();
101
+
102
+ } catch (err) {
103
+ if (err.message === 'CHECKPOINT_DETECTED') {
104
+ console.error("❌ Critical: Account requires manual ID verification.");
105
+ } else if (err.message.includes('BUSY')) {
106
+ console.error("⚠️ User is already running a task.");
107
+ } else {
108
+ console.error("Error:", err.message);
109
+ }
110
+ }
111
+ })();
112
+ ```
113
+
114
+ ### 2. Custom Storage (Database Integration)
115
+ By default, sessions are saved to `./sessions`. Override this to use Redis, MongoDB, or PostgreSQL.
116
+
117
+ ```javascript
118
+ const { setSessionStorage } = require('@ubaidbinwaris/linkedin');
119
+
120
+ setSessionStorage({
121
+ read: async (email) => {
122
+ // Return encrypted JSON string from your DB
123
+ const result = await db.query('SELECT session_data FROM users WHERE email = $1', [email]);
124
+ return result.rows[0]?.session_data;
125
+ },
126
+ write: async (email, data) => {
127
+ // Save encrypted JSON string to your DB
128
+ await db.query('UPDATE users SET session_data = $1 WHERE email = $2', [data, email]);
129
+ }
130
+ });
131
+ ```
132
+
133
+ ### 3. Custom Logger
134
+ Pipe internal logs to your own system (e.g., Winston, UI Stream).
135
+
136
+ ```javascript
137
+ const { setLogger } = require('@ubaidbinwaris/linkedin');
138
+
139
+ setLogger({
140
+ info: (msg) => console.log(`[LI-INFO] ${msg}`),
141
+ warn: (msg) => console.warn(`[LI-WARN] ${msg}`),
142
+ error: (msg) => console.error(`[LI-ERR] ${msg}`)
143
+ });
144
+ ```
145
+
146
+ ## 🚨 Error Reference
147
+
148
+ | Error Message | Meaning | Handling Action |
149
+ | :--- | :--- | :--- |
150
+ | `CHECKPOINT_DETECTED` | Security challenge (ID upload/Captcha) could not be resolved. | Notify admin. Manual Login required. |
151
+ | `CHECKPOINT_DETECTED_M` | Manual Fallback (Visible Browser) timed out. | User didn't interact in time. Retry. |
152
+ | `BUSY: ...` | A task is already running for this email. | Queue the request or reject it. |
153
+ | `LOGIN_FAILED` | Credentials accepted, but session could not be verified. | Check proxy/network. |
154
+
155
+ ## License
156
+ ISC
package/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // Main entry point
2
+ const loginToLinkedIn = require('./src/login/login');
3
+ const { setSessionDir, setSessionStorage } = require('./src/session/sessionManager');
4
+ const { setLogger } = require('./src/utils/logger');
5
+ const config = require('./src/config');
6
+
7
+ module.exports = {
8
+ loginToLinkedIn,
9
+ setSessionDir,
10
+ setSessionStorage,
11
+ setLogger,
12
+ config
13
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@ubaidbinwaris/linkedin-login",
3
+ "version": "2.1.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "files": [
7
+ "src",
8
+ "index.js",
9
+ "cli.js",
10
+ "Readme.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "test": "echo \"Error: no test specified\" && exit 1",
15
+ "start": "node cli.js"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/UbaidBinWaris/linkedin.git"
20
+ },
21
+ "keywords": [
22
+ "linkedin",
23
+ "automation",
24
+ "bot",
25
+ "playwright"
26
+ ],
27
+ "author": "UbaidBinWaris",
28
+ "license": "MIT",
29
+ "type": "commonjs",
30
+ "bugs": {
31
+ "url": "https://github.com/UbaidBinWaris/linkedin/issues"
32
+ },
33
+ "homepage": "https://github.com/UbaidBinWaris/linkedin#readme",
34
+ "dependencies": {
35
+ "dotenv": "^17.2.4",
36
+ "playwright": "^1.58.2",
37
+ "winston": "^3.19.0"
38
+ }
39
+ }
@@ -0,0 +1,52 @@
1
+ const logger = require("../utils/logger");
2
+ const { randomDelay } = require("../utils/time");
3
+
4
+ const LINKEDIN_LOGIN = "https://www.linkedin.com/login";
5
+
6
+ /**
7
+ * Performs credential-based login.
8
+ */
9
+ async function performCredentialLogin(page, email, password) {
10
+ logger.info("Proceeding to credential login...");
11
+
12
+ if (!page.url().includes("login") && !page.url().includes("uas/request-password-reset")) {
13
+ await page.goto(LINKEDIN_LOGIN, { waitUntil: 'domcontentloaded' });
14
+ await randomDelay(1000, 2000);
15
+ }
16
+
17
+ logger.info("Entering credentials...");
18
+
19
+ await page.click('input[name="session_key"]');
20
+ await randomDelay(500, 1000);
21
+ await page.type('input[name="session_key"]', email, { delay: 100 });
22
+
23
+ await randomDelay(1000, 2000);
24
+
25
+ await page.click('input[name="session_password"]');
26
+ await page.type('input[name="session_password"]', password, { delay: 100 });
27
+
28
+ await randomDelay(1000, 2000);
29
+
30
+ logger.info("Submitting login form...");
31
+ await Promise.all([
32
+ page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
33
+ page.click('button[type="submit"]')
34
+ ]);
35
+ }
36
+
37
+ const { VALIDATION_SELECTORS } = require("../config");
38
+
39
+ /**
40
+ * Checks if the user is currently logged in (on feed).
41
+ */
42
+ async function isLoggedIn(page) {
43
+ try {
44
+ const selector = VALIDATION_SELECTORS.join(", ");
45
+ await page.waitForSelector(selector, { timeout: 10000 });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ module.exports = { performCredentialLogin, isLoggedIn };
@@ -0,0 +1,85 @@
1
+ const logger = require("../utils/logger");
2
+
3
+ /**
4
+ * Detects if a checkpoint/verification is triggered.
5
+ * Returns true if intervention is needed.
6
+ */
7
+ async function detectCheckpoint(page) {
8
+ try {
9
+ const url = page.url();
10
+ if (
11
+ url.includes("checkpoint") ||
12
+ url.includes("challenge") ||
13
+ url.includes("verification") ||
14
+ url.includes("consumer-login/error")
15
+ ) {
16
+ return true;
17
+ }
18
+
19
+ try {
20
+ await page.waitForSelector("h1:has-text('Security Verification')", { timeout: 1000 });
21
+ return true;
22
+ } catch (e) {
23
+ // Element not found
24
+ }
25
+
26
+ return false;
27
+ } catch (error) {
28
+ logger.error(`Error during checkpoint detection: ${error.message}`);
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Handles mobile verification/app approval prompts.
35
+ * Polls for success (feed navigation) for a set duration.
36
+ * @param {Page} page
37
+ * @returns {Promise<boolean>} true if resolved, false if timed out/failed
38
+ */
39
+ async function handleMobileVerification(page) {
40
+ try {
41
+ const isMobileVerif = await page.evaluate(() => {
42
+ const text = document.body.innerText;
43
+ // User checks: "Check your LinkedIn app", "Open your LinkedIn app", "Approve the sign-in"
44
+ return text.includes("Check your LinkedIn app") ||
45
+ text.includes("Open your LinkedIn app") ||
46
+ text.includes("Tap Yes on the prompt") ||
47
+ text.includes("verification request to your device") ||
48
+ text.includes("Approve the sign-in");
49
+ });
50
+
51
+ if (!isMobileVerif) return false;
52
+
53
+ logger.warn("ACTION REQUIRED: Check your LinkedIn app on your phone! Waiting 2 minutes...");
54
+
55
+ try {
56
+ // Poll for feed URL for 120 seconds
57
+ // Note: If navigation happens (user approves), this might throw "Execution context was destroyed"
58
+ // We handle that in the catch block by checking the URL.
59
+ await page.waitForFunction(() => {
60
+ return window.location.href.includes("/feed") ||
61
+ document.querySelector('.global-nav__search');
62
+ }, { timeout: 120000 });
63
+
64
+ logger.info("Mobile verification successful! Resuming...");
65
+ return true; // Resolved
66
+ } catch (err) {
67
+ // Check if we actually succeeded via navigation
68
+ try {
69
+ if (page.url().includes("/feed")) {
70
+ logger.info("Mobile verification successful (detected via navigation)! Resuming...");
71
+ return true;
72
+ }
73
+ } catch(e) {}
74
+
75
+ logger.warn(`Mobile verification wait ended: ${err.message}`);
76
+ return false;
77
+ }
78
+
79
+ } catch (e) {
80
+ logger.warn(`Mobile verification check failed: ${e.message}`);
81
+ return false;
82
+ }
83
+ }
84
+
85
+ module.exports = { detectCheckpoint, handleMobileVerification };
@@ -0,0 +1,62 @@
1
+ const { chromium } = require("playwright");
2
+ const logger = require("../utils/logger");
3
+ const { loadSession } = require("../session/sessionManager");
4
+
5
+ /**
6
+ * Creates and launches a browser instance.
7
+ * @param {Object} options - Launch options
8
+ */
9
+ async function createBrowser(options = {}) {
10
+ const launchOptions = {
11
+ headless: options.headless !== undefined ? options.headless : true,
12
+ args: [
13
+ "--no-sandbox",
14
+ "--disable-setuid-sandbox",
15
+ ],
16
+ ...options
17
+ };
18
+
19
+ logger.info("Launching browser (Standard Playwright)...");
20
+ return await chromium.launch(launchOptions);
21
+ }
22
+
23
+ /**
24
+ * Creates or loads a browser context.
25
+ * @param {import('playwright').Browser} browser
26
+ * @param {string} email - for session loading
27
+ */
28
+ async function createContext(browser, email) {
29
+ const contextOptions = {
30
+ userAgent: getRandomUserAgent(),
31
+ viewport: getRandomViewport(),
32
+ locale: 'en-US',
33
+ timezoneId: 'America/New_York',
34
+ permissions: ['geolocation'],
35
+ ignoreHTTPSErrors: true,
36
+ };
37
+
38
+ logger.info(`Checking for saved session for ${email}...`);
39
+ const storedContext = await loadSession(browser, contextOptions, email);
40
+
41
+ if (storedContext) {
42
+ logger.info("Session stored context loaded.");
43
+ return storedContext;
44
+ }
45
+
46
+ logger.info("No valid session found. Creating new context.");
47
+ return await browser.newContext(contextOptions);
48
+ }
49
+
50
+ function getRandomViewport() {
51
+ const width = 1280 + Math.floor(Math.random() * 640);
52
+ const height = 720 + Math.floor(Math.random() * 360);
53
+ return { width, height };
54
+ }
55
+
56
+ function getRandomUserAgent() {
57
+ const versions = ["120.0.0.0", "121.0.0.0", "122.0.0.0"];
58
+ const version = versions[Math.floor(Math.random() * versions.length)];
59
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
60
+ }
61
+
62
+ module.exports = { createBrowser, createContext };
package/src/config.js ADDED
@@ -0,0 +1,14 @@
1
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
2
+
3
+ module.exports = {
4
+ // Session expiry time: 30 days
5
+ SESSION_MAX_AGE: 90 * ONE_DAY_MS,
6
+
7
+ // Selectors to verify if the session is valid (logged in)
8
+ VALIDATION_SELECTORS: [
9
+ '.global-nav__search',
10
+ 'input[placeholder="Search"]',
11
+ '#global-nav-typeahead',
12
+ '.feed-shared-update-v2' // Feed post container
13
+ ]
14
+ };
@@ -0,0 +1,155 @@
1
+ const logger = require("../utils/logger");
2
+ const { saveSession, SessionLock } = require("../session/sessionManager");
3
+ const { createBrowser, createContext } = require("../browser/launcher");
4
+ const { detectCheckpoint, handleMobileVerification } = require("../auth/checkpoint");
5
+ const { performCredentialLogin, isLoggedIn } = require("../auth/actions");
6
+ const { randomDelay } = require("../utils/time");
7
+
8
+ const LINKEDIN_FEED = "https://www.linkedin.com/feed/";
9
+
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)
17
+ */
18
+ async function loginToLinkedIn(options = {}, credentials = null) {
19
+ const email = credentials?.username || process.env.LINKEDIN_EMAIL;
20
+ const password = credentials?.password || process.env.LINKEDIN_PASSWORD;
21
+
22
+ if (!email || !password) {
23
+ throw new Error("Missing LinkedIn credentials.");
24
+ }
25
+
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
+ }
45
+
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 };
58
+ }
59
+
60
+ logger.info(`[${email}] Session invalid. Clearing cookies and attempting credential login...`);
61
+
62
+ // Clear old/corrupt cookies to ensure fresh login
63
+ await context.clearCookies();
64
+
65
+ // ----------------------------
66
+ // STEP 3: Credential Login
67
+ // ----------------------------
68
+ await performCredentialLogin(page, email, password);
69
+
70
+ // ----------------------------
71
+ // STEP 4: Verify & Fail Fast
72
+ // ----------------------------
73
+ if (await detectCheckpoint(page)) {
74
+ // Attempt to handle mobile verification (2 min wait)
75
+ // If it returns true, it means we are now on the feed (resolved)
76
+ if (await handleMobileVerification(page)) {
77
+ // success, assume logged in
78
+ // Wait a moment for page to settle
79
+ await page.waitForTimeout(3000);
80
+
81
+ // Double check if we are really on feed
82
+ if (page.url().includes("/feed") || await isLoggedIn(page)) {
83
+ logger.info(`[${email}] Mobile Verification Successful ✅`);
84
+ await saveSession(context, email, true);
85
+ return { browser, context, page };
86
+ }
87
+ } else {
88
+ // Failed mobile verification.
89
+ // User Request: "otherwise after some delay using browser opens the browser and fill the form"
90
+
91
+ if (options.headless) {
92
+ logger.info("[Fallback] Mobile verification failed. Switching to VISIBLE browser for manual intervention...");
93
+ await browser.close();
94
+
95
+ // RE-LAUNCH in Visible Mode
96
+ const visibleBrowser = await createBrowser({ ...options, headless: false });
97
+ const visibleContext = await createContext(visibleBrowser, email);
98
+ const visiblePage = await visibleContext.newPage();
99
+ visiblePage.setDefaultTimeout(60000); // More time for manual interaction
100
+
101
+ try {
102
+ logger.info("[Fallback] Filling credentials in visible browser...");
103
+ await performCredentialLogin(visiblePage, email, password);
104
+
105
+ // Now wait for success (Feed)
106
+ logger.info("[Fallback] Waiting for user to complete login manually...");
107
+
108
+ // Wait for URL OR Selector
109
+ // We loop to check both condition
110
+ try {
111
+ await visiblePage.waitForFunction(() => {
112
+ return window.location.href.includes("/feed") ||
113
+ document.querySelector(".global-nav__search");
114
+ }, { timeout: 120000 });
115
+ } catch(e) {
116
+ logger.warn(`[Fallback] Wait finished with error: ${e.message}`);
117
+ }
118
+
119
+ // Double check status
120
+ if (visiblePage.url().includes("/feed") || await isLoggedIn(visiblePage)) {
121
+ logger.info(`[${email}] Manual Fallback Successful ✅`);
122
+ await saveSession(visibleContext, email, true);
123
+ return { browser: visibleBrowser, context: visibleContext, page: visiblePage };
124
+ }
125
+ } catch (fallbackErr) {
126
+ logger.error(`[Fallback] Manual intervention failed or timed out: ${fallbackErr.message}`);
127
+ await visibleBrowser.close();
128
+ throw new Error("CHECKPOINT_DETECTED_M"); // M for manual failed
129
+ }
130
+ }
131
+
132
+ const screenshotPath = `checkpoint_${email}_${Date.now()}.png`;
133
+ await page.screenshot({ path: screenshotPath });
134
+ logger.warn(`[${email}] Checkpoint detected! Screenshot saved: ${screenshotPath}`);
135
+ throw new Error("CHECKPOINT_DETECTED");
136
+ }
137
+ }
138
+
139
+ if (await isLoggedIn(page)) {
140
+ logger.info(`[${email}] Login successful ✅`);
141
+ await saveSession(context, email, true); // Save with validation = true
142
+ return { browser, context, page };
143
+ } else {
144
+ throw new Error("LOGIN_FAILED: Could not verify session after login attempt.");
145
+ }
146
+
147
+ } catch (error) {
148
+ logger.error(`[${email}] Login process failed: ${error.message}`);
149
+ await browser.close();
150
+ throw error;
151
+ }
152
+ });
153
+ }
154
+
155
+ module.exports = loginToLinkedIn;
@@ -0,0 +1,52 @@
1
+ const logger = require("../utils/logger");
2
+
3
+ /**
4
+ * In-memory lock map to prevent simultaneous login attempts for the same user.
5
+ * Map<email, Promise<any>>
6
+ */
7
+ const activeLogins = new Map();
8
+
9
+ const SessionLock = {
10
+ /**
11
+ * Executes a function with an exclusive lock for the given email.
12
+ * If a lock exists, it waits for it to release (or chains onto it?
13
+ * Actually user requested: "if activeLogins.has(email) { return activeLogins.get(email); }"
14
+ * This means if a login is in progress, return that SAME promise. Join the existing attempt.)
15
+ *
16
+ * @param {string} email
17
+ * @param {Function} fn - Async function to execute
18
+ * @returns {Promise<any>}
19
+ */
20
+ async withLoginLock(email, fn) {
21
+ if (activeLogins.has(email)) {
22
+ logger.info(`[SessionLock] ${email} is already logging in. Joining existing request...`);
23
+ return activeLogins.get(email);
24
+ }
25
+
26
+ logger.info(`[SessionLock] Acquiring lock for ${email}...`);
27
+
28
+ // Create a promise that wraps the execution
29
+ const promise = (async () => {
30
+ try {
31
+ return await fn();
32
+ } finally {
33
+ activeLogins.delete(email);
34
+ logger.info(`[SessionLock] Releasing lock for ${email}.`);
35
+ }
36
+ })();
37
+
38
+ activeLogins.set(email, promise);
39
+ return promise;
40
+ },
41
+
42
+ /**
43
+ * Checks if a user is currently locked.
44
+ * @param {string} email
45
+ * @returns {boolean}
46
+ */
47
+ isLocked(email) {
48
+ return activeLogins.has(email);
49
+ }
50
+ };
51
+
52
+ module.exports = SessionLock;
@@ -0,0 +1,208 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const crypto = require("crypto");
4
+ const logger = require("../utils/logger");
5
+ const SessionLock = require("./SessionLock"); // Keeping specific import, though widely used via login
6
+
7
+ let sessionDir = path.join(process.cwd(), "data", "linkedin");
8
+
9
+ /**
10
+ * Sets the directory where session files are stored.
11
+ * @param {string} dirPath - Absolute path to the session directory.
12
+ */
13
+ function setSessionDir(dirPath) {
14
+ sessionDir = dirPath;
15
+ }
16
+
17
+ const ALGORITHM = "aes-256-cbc";
18
+ const SECRET_KEY = process.env.SESSION_SECRET || "default_insecure_secret_key_32_bytes_long!!";
19
+ const key = crypto.createHash("sha256").update(String(SECRET_KEY)).digest();
20
+
21
+ function encrypt(text) {
22
+ const iv = crypto.randomBytes(16);
23
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
24
+ let encrypted = cipher.update(text, 'utf8', 'hex');
25
+ encrypted += cipher.final('hex');
26
+ return iv.toString('hex') + ':' + encrypted;
27
+ }
28
+
29
+ function decrypt(text) {
30
+ try {
31
+ const textParts = text.split(':');
32
+ const iv = Buffer.from(textParts.shift(), 'hex');
33
+ const encryptedText = textParts.join(':');
34
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
35
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
36
+ decrypted += decipher.final('utf8');
37
+ return decrypted;
38
+ } catch (error) {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Generates a SHA-256 hash of the email to use as filename.
45
+ * Privacy + filesystem safety.
46
+ * @param {string} email
47
+ * @returns {string} hex hash
48
+ */
49
+ function getSessionHash(email) {
50
+ if (!email) return "default";
51
+ return crypto.createHash("sha256").update(email).digest("hex");
52
+ }
53
+
54
+ function getSessionPath(email) {
55
+ const filename = `${getSessionHash(email)}.json`;
56
+ return path.join(sessionDir, filename);
57
+ }
58
+
59
+ function sessionExists(email) {
60
+ return fs.existsSync(getSessionPath(email));
61
+ }
62
+
63
+ const { SESSION_MAX_AGE } = require("../config"); // Assuming this exists, typically 7 days?
64
+
65
+ // Validation Cache Duration (10 minutes)
66
+ const VALIDATION_CACHE_MS = 10 * 60 * 1000;
67
+
68
+ const defaultStorage = {
69
+ async read(email) {
70
+ const filePath = getSessionPath(email);
71
+ if (!fs.existsSync(filePath)) return null;
72
+ return fs.readFileSync(filePath, "utf-8");
73
+ },
74
+
75
+ async write(email, data) {
76
+ const filePath = getSessionPath(email);
77
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
78
+ fs.writeFileSync(filePath, data, "utf-8");
79
+ }
80
+ };
81
+
82
+ let currentStorage = defaultStorage;
83
+
84
+ function setSessionStorage(adapter) {
85
+ if (adapter && typeof adapter.read === 'function' && typeof adapter.write === 'function') {
86
+ currentStorage = adapter;
87
+ } else {
88
+ logger.warn("Invalid storage adapter provided. Using default file storage.");
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Saves the browser context state.
94
+ * @param {import('playwright').BrowserContext} context
95
+ * @param {string} email
96
+ * @param {boolean} [isValidated] - If true, updates lastValidatedAt
97
+ */
98
+ async function saveSession(context, email, isValidated = false) {
99
+ try {
100
+ const state = await context.storageState();
101
+
102
+ // Load existing metadata if possible to preserve creation time?
103
+ // For now, simpler to just overwrite, but we might lose 'createdAt' if we had it.
104
+ // Let's just write new.
105
+
106
+ const sessionData = {
107
+ timestamp: Date.now(), // Last saved
108
+ lastValidatedAt: isValidated ? Date.now() : undefined,
109
+ state: state
110
+ // Future: proxy binding here
111
+ };
112
+
113
+ const jsonString = JSON.stringify(sessionData);
114
+ const encryptedData = encrypt(jsonString);
115
+
116
+ await currentStorage.write(email || "default", encryptedData);
117
+
118
+ logger.info(`Session saved for ${email} (Validated: ${isValidated}) 🔒`);
119
+ } catch (error) {
120
+ logger.error(`Failed to save session: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Loads the session.
126
+ * @returns {Promise<{ context: import('playwright').BrowserContext, needsValidation: boolean }>}
127
+ */
128
+ async function loadSession(browser, options = {}, email) {
129
+ const user = email || "default";
130
+
131
+ try {
132
+ const fileContent = await currentStorage.read(user);
133
+
134
+ if (!fileContent) {
135
+ logger.info(`No session found for ${user}.`);
136
+ return null;
137
+ }
138
+
139
+ let sessionData = null;
140
+ let state = null;
141
+
142
+ // Decrypt
143
+ const decrypted = decrypt(fileContent);
144
+ if (decrypted) {
145
+ try {
146
+ sessionData = JSON.parse(decrypted);
147
+ } catch (e) {
148
+ logger.error("Failed to parse session JSON.");
149
+ return null;
150
+ }
151
+ } else {
152
+ // Fallback for legacy plain JSON?
153
+ try {
154
+ sessionData = JSON.parse(fileContent);
155
+ } catch(e) {}
156
+ }
157
+
158
+ if (!sessionData || !sessionData.state) {
159
+ logger.warn("Invalid or corrupt session data.");
160
+ return null;
161
+ }
162
+
163
+ // Check Age (Total expiry)
164
+ const age = Date.now() - sessionData.timestamp;
165
+ if (SESSION_MAX_AGE && age > SESSION_MAX_AGE) {
166
+ logger.warn(`Session expired (Age: ${age}ms).`);
167
+ return null;
168
+ }
169
+
170
+ state = sessionData.state;
171
+
172
+ // Check Validation Freshness
173
+ let needsValidation = true;
174
+ if (sessionData.lastValidatedAt) {
175
+ const valAge = Date.now() - sessionData.lastValidatedAt;
176
+ if (valAge < VALIDATION_CACHE_MS) {
177
+ needsValidation = false;
178
+ logger.info(`Session validation cached (Age: ${Math.round(valAge/1000)}s). Skipping checks.`);
179
+ }
180
+ }
181
+
182
+ const context = await browser.newContext({
183
+ storageState: state,
184
+ ...options
185
+ });
186
+
187
+ // Attach metadata to context for caller to check?
188
+ // Or return object? Returning object is a breaking change for internal API but we are in v2.
189
+ // Let's attach to context object directly as a property for convenience
190
+ context.needsValidation = needsValidation;
191
+
192
+ return context;
193
+
194
+ } catch (error) {
195
+ logger.error(`Error loading session: ${error.message}`);
196
+ return null;
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ setSessionDir,
202
+ setSessionStorage,
203
+ sessionExists,
204
+ getSessionPath,
205
+ saveSession,
206
+ loadSession,
207
+ SessionLock
208
+ };
@@ -0,0 +1,51 @@
1
+ const { createLogger, format, transports } = require("winston");
2
+ const winston = require("winston");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+
6
+ const logDir = path.join(__dirname, "../../logs");
7
+
8
+ if (!fs.existsSync(logDir)) {
9
+ fs.mkdirSync(logDir, { recursive: true });
10
+ }
11
+
12
+ // Create default logger
13
+ const defaultLogger = winston.createLogger({
14
+ level: "info",
15
+ format: winston.format.combine(
16
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
17
+ winston.format.printf(({ timestamp, level, message }) => {
18
+ return `${timestamp} [${level.toUpperCase()}]: ${message}`;
19
+ })
20
+ ),
21
+ transports: [
22
+ new winston.transports.File({ filename: path.join(logDir, "error.log"), level: "error" }),
23
+ new winston.transports.File({ filename: path.join(logDir, "combined.log") }),
24
+ new winston.transports.Console() // Output to console
25
+ ],
26
+ });
27
+
28
+ let currentLogger = defaultLogger;
29
+
30
+ /**
31
+ * Sets a custom logger instance.
32
+ * @param {Object} customLogger - Logger object with info(), error(), warn(), debug() methods.
33
+ */
34
+ function setLogger(customLogger) {
35
+ if (customLogger && typeof customLogger.info === 'function') {
36
+ currentLogger = customLogger;
37
+ } else {
38
+ console.warn("Invalid logger provided. Using default.");
39
+ }
40
+ }
41
+
42
+ // Proxy object to forward calls to the current logger
43
+ const loggerProxy = {
44
+ info: (msg) => currentLogger.info(msg),
45
+ error: (msg) => currentLogger.error(msg),
46
+ warn: (msg) => currentLogger.warn(msg),
47
+ debug: (msg) => currentLogger.debug(msg),
48
+ setLogger // Export configuration method
49
+ };
50
+
51
+ module.exports = loggerProxy;
@@ -0,0 +1,17 @@
1
+ const readline = require("readline");
2
+
3
+ function waitForUserResume(message = "Press ENTER to continue...") {
4
+ return new Promise((resolve) => {
5
+ const rl = readline.createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout,
8
+ });
9
+
10
+ rl.question(message, () => {
11
+ rl.close();
12
+ resolve();
13
+ });
14
+ });
15
+ }
16
+
17
+ module.exports = { waitForUserResume };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns a promise that resolves after a random delay between min and max milliseconds.
3
+ * @param {number} min - Minimum delay in ms.
4
+ * @param {number} max - Maximum delay in ms.
5
+ * @returns {Promise<void>}
6
+ */
7
+ function randomDelay(min = 1000, max = 3000) {
8
+ const delay = Math.floor(Math.random() * (max - min + 1)) + min;
9
+ return new Promise((resolve) => setTimeout(resolve, delay));
10
+ }
11
+
12
+ module.exports = { randomDelay };