@ubaidbinwaris/linkedin 1.0.13 → 1.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 CHANGED
@@ -1,185 +1,115 @@
1
- # LinkedIn Automation Bot & Module 🤖
1
+ # LinkedIn Session Manager & Automation Service (v2.0.0)
2
2
 
3
- A robust, stealthy Node.js automation tool designed to interact with LinkedIn using [Playwright](https://playwright.dev/). This package can be used as a standalone **CLI tool** or integrated as a **Module** into larger applications (supporting database storage, custom logging, and headless controls).
3
+ A robust, deterministic backend service foundation for managing multiple LinkedIn accounts with strict concurrency control and session isolation.
4
4
 
5
- > **⚠️ Disclaimer**: This tool is for educational purposes only. Automating interactions on LinkedIn violates their User Agreement. Use at your own risk. The authors are not responsible for any account bans or restrictions.
5
+ > **v2.0.0 Change**: This major version pivots from a CLI tool to a backend service architecture. It removes "stealth" plugins and "auto-resolution" magic in favor of stability, reliability, and "fail-fast" principles.
6
6
 
7
- ---
7
+ ## Core Features
8
8
 
9
- ## 🚀 Key Features
9
+ - **Multi-User Architecture**: Designed to handle hundreds of accounts safely.
10
+ - **Concurrency Control**: In-memory locking (`SessionLock`) prevents parallel login attempts for the same user.
11
+ - **Session Isolation**:
12
+ - Sessions stored as `SHA-256` hashes of emails (privacy).
13
+ - AES-256 Encryption for session data.
14
+ - **Smart Validation**:
15
+ - Trusts sessions validated within the last 10 minutes (reduces feed navigation load).
16
+ - Automatically refreshes older sessions.
17
+ - **Deterministic Flow**:
18
+ - **Launch** -> **Check Session** -> **Login** -> **Fail/Success**.
19
+ - **Fail Fast**: If a checkpoint/challenge is detected, it throws `CHECKPOINT_DETECTED` immediately, allowing the upper layer (API/Worker) to handle it (e.g., notify admin).
10
20
 
11
- - **🕵️‍♂️ Stealth Automation**: Leverages `puppeteer-extra-plugin-stealth` to minimize detection risks.
12
- - **👥 Multi-User Support**: Manage multiple LinkedIn accounts with isolated sessions.
13
- - **💾 Flexible Session Storage**: Store sessions in local files (default) or **inject your own database adapter** (MongoDB, Postgres, Redis, etc.).
14
- - **🖥️ Dual Modes**:
15
- - **Headless**: Runs efficiently in the background.
16
- - **Visible**: Watch the bot work for debugging.
17
- - **⌨️ CLI & Interactive REPL**: Control the bot directly from the terminal.
18
- - **🔌 Module Integration**: Easily integrate into existing Express/Node.js servers with custom logging and webhook-style checkpoint handling.
19
- - **🧠 Smart Validation**: Automatically detects checkpoints (CAPTCHA/Pin) and allows manual resolution via terminal or callback.
21
+ ## Installation
20
22
 
21
- ---
22
-
23
- ## 📦 Installation
24
-
25
- ### 1. Install via NPM
26
23
  ```bash
27
24
  npm install @ubaidbinwaris/linkedin
28
25
  ```
29
26
 
30
- ### 2. (Optional) Clone for Source Usage
31
- ```bash
32
- git clone https://github.com/UbaidBinWaris/linkedin.git
33
- cd linkedin
34
- npm install
35
- npx playwright install chromium
27
+ ## Architecture
28
+
29
+ ```mermaid
30
+ graph TD
31
+ A[API/Worker Request] --> B{Acquire User Lock}
32
+ B -- Busy --> C[Throw BUSY Error]
33
+ B -- Acquired --> D[Launch Standard Playwright]
34
+ D --> E{Load Session}
35
+ E -- Valid & Recent --> F[Return Context (Skip Feed)]
36
+ E -- Stale/None --> G[Navigate to Feed]
37
+ G --> H{Is Logged In?}
38
+ H -- Yes --> I[Update Validation Timestamp]
39
+ I --> F
40
+ H -- No --> J[Perform Credential Login]
41
+ J --> K{Checkpoint?}
42
+ K -- Yes --> L[Throw CHECKPOINT_DETECTED]
43
+ K -- No --> M[Login Success]
44
+ M --> I
45
+ F --> N[Release Lock (on Task Completion)]
36
46
  ```
37
47
 
38
- ---
39
-
40
- ## ▶️ Usage: CLI Mode
41
-
42
- You can run the bot directly from the command line.
48
+ ## Usage
43
49
 
44
- ### 1. Quick Start (Single User)
45
- Create a `.env` file with your credentials:
46
- ```env
47
- LINKEDIN_EMAIL=your_email@example.com
48
- LINKEDIN_PASSWORD=your_password
49
- ```
50
- Then run:
51
- ```bash
52
- npm start
53
- ```
50
+ ### Basic Login
54
51
 
55
- ### 2. Multi-User Mode
56
- To manage multiple accounts, create a `users.js` file in the root directory:
52
+ The `loginToLinkedIn` function now handles locking internally. If you try to call it twice for the same user simultaneously, the second call will fail with a `BUSY` error.
57
53
 
58
- **File:** `users.js`
59
54
  ```javascript
60
- module.exports = [
61
- { username: "alice@example.com", password: "password123" },
62
- { username: "bob@company.com", password: "securePass!" }
63
- ];
64
- ```
65
-
66
- Run the bot:
67
- ```bash
68
- npm start
69
- ```
70
- The CLI will prompt you to select which user to log in as:
71
- ```text
72
- Select a user to login:
73
- 1. alice@example.com
74
- 2. bob@company.com
75
- 3. Use Environment Variables (.env)
55
+ const { loginToLinkedIn } = require('@ubaidbinwaris/linkedin');
56
+
57
+ (async () => {
58
+ try {
59
+ const { browser, page } = await loginToLinkedIn({
60
+ headless: true
61
+ }, {
62
+ username: 'user@example.com',
63
+ password: 'secret-password'
64
+ });
65
+
66
+ console.log("Logged in and active!");
67
+
68
+ // Output session status
69
+ console.log(`Needs Validation? ${page.context().needsValidation}`);
70
+
71
+ // Do work...
72
+
73
+ await browser.close();
74
+ } catch (err) {
75
+ if (err.message === 'CHECKPOINT_DETECTED') {
76
+ console.error("Manual intervention required for this account.");
77
+ } else if (err.message.startsWith('BUSY')) {
78
+ console.error("User is currently busy with another task.");
79
+ } else {
80
+ console.error("Login failed:", err);
81
+ }
82
+ }
83
+ })();
76
84
  ```
77
85
 
78
- ---
79
-
80
- ## 🔌 Usage: Module Integration
86
+ ### Custom Session Storage
81
87
 
82
- This package is designed to be embedded in larger systems (e.g., a SaaS backend managing hundreds of accounts).
88
+ You can link this to a database (Redis/Postgres) instead of local files.
83
89
 
84
- ### Import
85
90
  ```javascript
86
- const linkedin = require('@ubaidbinwaris/linkedin');
87
- ```
88
-
89
- ### 1. Custom Session Storage (Database Integration)
90
- By default, sessions are saved as JSON files in `data/linkedin/`. You can override this to save sessions directly in your database.
91
+ const { setSessionStorage } = require('@ubaidbinwaris/linkedin');
91
92
 
92
- ```javascript
93
- // Example: Using a generic database
94
- linkedin.setSessionStorage({
95
- async read(username) {
96
- // Fetch encrypted session string from your DB
97
- const user = await db.users.findOne({ email: username });
98
- return user ? user.linkedinSession : null;
93
+ setSessionStorage({
94
+ read: async (email) => {
95
+ // Fetch encrypted string from DB
96
+ return await db.sessions.findOne({ where: { email } });
99
97
  },
100
- async write(username, data) {
101
- // Save encrypted session string to your DB
102
- await db.users.updateOne(
103
- { email: username },
104
- { $set: { linkedinSession: data } },
105
- { upsert: true }
106
- );
98
+ write: async (email, data) => {
99
+ // Save encrypted string to DB
100
+ await db.sessions.upsert({ email, data });
107
101
  }
108
102
  });
109
103
  ```
110
104
 
111
- ### 2. Custom Logger
112
- Redirect bot logs to your application's logging system (e.g., Winston, Pino, Bunyan).
113
-
114
- ```javascript
115
- linkedin.setLogger({
116
- info: (msg) => console.log(`[BOT INFO] ${msg}`),
117
- error: (msg) => console.error(`[BOT ERROR] ${msg}`),
118
- warn: (msg) => console.warn(`[BOT WARN] ${msg}`),
119
- debug: (msg) => console.debug(`[BOT DEBUG] ${msg}`)
120
- });
121
- ```
122
-
123
- ### 3. Login & Headless Control
124
- When running on a server, you can't use the terminal to solve CAPTCHAs. Use the `onCheckpoint` callback to handle verification requests (e.g., trigger an alert).
125
-
126
- ```javascript
127
- const credentials = {
128
- username: "alice@example.com",
129
- password: "password123"
130
- };
131
-
132
- try {
133
- const { browser, page } = await linkedin.loginToLinkedIn({
134
- headless: true,
135
- // Callback when LinkedIn asks for verification
136
- onCheckpoint: async () => {
137
- console.log("⚠️ Checkpoint detected! Pausing for manual intervention...");
138
-
139
- // Example: Send an email/Slack alert to admin
140
- await sendAdminAlert(`User ${credentials.username} needs verification!`);
141
-
142
- // Wait for admin to signal resolution (e.g. via DB flag or API call)
143
- await waitForAdminResolution();
144
-
145
- console.log("Resuming login...");
146
- }
147
- }, credentials);
148
-
149
- console.log("Login successful!");
150
-
151
- // Do automation tasks...
152
- await browser.close();
153
-
154
- } catch (err) {
155
- console.error("Login failed:", err);
156
- }
157
- ```
158
-
159
- ---
160
-
161
- ## 🛠️ Configuration Options
162
-
163
- | Option | Type | Default | Description |
164
- | :--- | :--- | :--- | :--- |
165
- | `headless` | `boolean` | `false` | Run browser in background. |
166
- | `slowMo` | `number` | `50` | Delay between actions (ms). |
167
- | `proxy` | `string` | `undefined` | Proxy URL (e.g., `http://user:pass@host:port`). |
168
- | `onCheckpoint` | `function` | `null` | Async callback triggered when verification is needed. |
169
-
170
- ---
171
-
172
- ## ❓ Troubleshooting
173
-
174
- ### "Checkpoint Detected"
175
- - **CLI Mode**: The bot will pause and ask you to open a visible browser. Press ENTER to open it, solve the CAPTCHA, and the bot will confirm success and resume.
176
- - **Module Mode**: Ensure you provide an `onCheckpoint` callback to handle this event, otherwise the promise will reject or hang depending on implementation.
177
-
178
- ### Session Files
179
- - Sessions are encrypted and contain timestamps.
180
- - If `setSessionStorage` is NOT used, files are stored in `data/linkedin/<sanitized_username>.json`.
181
-
182
- ---
183
-
184
- ## 📄 License
185
- MIT License.
105
+ ## Directory Structure
106
+
107
+ * `src/session/`:
108
+ * `SessionLock.js`: In-memory concurrency control.
109
+ * `sessionManager.js`: Hashing, encryption, validation logic.
110
+ * `src/login/`:
111
+ * `login.js`: Deterministic login flow.
112
+ * `src/browser/`:
113
+ * `launcher.js`: Standard Playwright launcher (no stealth plugins).
114
+ * `src/auth/`:
115
+ * `checkpoint.js`: Simple detection logic.
package/index.js CHANGED
@@ -1,14 +1,13 @@
1
+ // Main entry point
1
2
  const loginToLinkedIn = require('./src/login/login');
2
- const sessionManager = require('./src/session/sessionManager');
3
+ const { setSessionDir, setSessionStorage } = require('./src/session/sessionManager');
4
+ const { setLogger } = require('./src/utils/logger');
3
5
  const config = require('./src/config');
4
- const logger = require('./src/utils/logger'); // This is now the proxy object
5
- // Exporting the main function and other utilities for library usage
6
+
6
7
  module.exports = {
7
8
  loginToLinkedIn,
8
- sessionManager,
9
- setSessionDir: sessionManager.setSessionDir,
10
- setSessionStorage: sessionManager.setSessionStorage,
11
- setLogger: logger.setLogger,
12
- config,
13
- logger
9
+ setSessionDir,
10
+ setSessionStorage,
11
+ setLogger,
12
+ config
14
13
  };
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "@ubaidbinwaris/linkedin",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
- "bin": {
7
- "linkedin": "cli.js"
8
- },
9
6
  "files": [
10
7
  "src",
11
8
  "index.js",
@@ -37,9 +34,6 @@
37
34
  "dependencies": {
38
35
  "dotenv": "^17.2.4",
39
36
  "playwright": "^1.58.2",
40
- "playwright-extra": "^4.3.6",
41
- "puppeteer-extra-plugin-stealth": "^2.11.2",
42
- "user-agents": "^1.1.669",
43
37
  "winston": "^3.19.0"
44
38
  }
45
39
  }
@@ -0,0 +1,49 @@
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
+ /**
38
+ * Checks if the user is currently logged in (on feed).
39
+ */
40
+ async function isLoggedIn(page) {
41
+ try {
42
+ await page.waitForSelector(".global-nav__search, #global-nav-typeahead", { timeout: 10000 });
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ module.exports = { performCredentialLogin, isLoggedIn };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Detects if a checkpoint/verification is triggered.
3
+ * Returns true if intervention is needed.
4
+ */
5
+ async function detectCheckpoint(page) {
6
+ try {
7
+ const url = page.url();
8
+ if (
9
+ url.includes("checkpoint") ||
10
+ url.includes("challenge") ||
11
+ url.includes("verification") ||
12
+ url.includes("consumer-login/error")
13
+ ) {
14
+ return true;
15
+ }
16
+
17
+ try {
18
+ await page.waitForSelector("h1:has-text('Security Verification')", { timeout: 1000 });
19
+ return true;
20
+ } catch (e) {
21
+ // Element not found
22
+ }
23
+
24
+ return false;
25
+ } catch (error) {
26
+ logger.error(`Error during checkpoint detection: ${error.message}`);
27
+ return false;
28
+ }
29
+ }
30
+
31
+ module.exports = { detectCheckpoint };
@@ -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 };
@@ -101,17 +101,92 @@ async function performCredentialLogin(page, email, password) {
101
101
 
102
102
 
103
103
  async function handleCheckpoint(page, options) {
104
+ // Initial check
104
105
  if (!(await detectCheckpoint(page))) return;
105
106
 
107
+ logger.warn("Checkpoint detected.");
108
+
106
109
  if (options.headless) {
107
- throw new Error("Checkpoint detected in headless mode.");
108
- }
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
+ });
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);
135
+
136
+ // Check if resolved
137
+ if (!(await detectCheckpoint(page))) {
138
+ logger.info("Checkpoint resolved via button click!");
139
+ return;
140
+ }
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
+
157
+ if (isMobileVerif) {
158
+ logger.info("Mobile verification detected. Waiting 2 minutes for manual approval on device...");
159
+
160
+ try {
161
+ // Poll for feed URL for 120 seconds
162
+ // We use a loop or waitForFunction
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
+ }
173
+ }
174
+ } catch (e) {
175
+ logger.warn(`Mobile verification check failed: ${e.message}`);
176
+ }
109
177
 
110
- logger.warn("Verification required. Please complete manually.");
178
+ // Re-check after attempts
179
+ if (await detectCheckpoint(page)) {
180
+ throw new Error("Checkpoint detected in headless mode and auto-resolution failed.");
181
+ }
111
182
 
112
- await waitForUserResume(
113
- "Complete verification in browser, then press ENTER..."
114
- );
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
+ }
115
190
  }
116
191
 
117
192
  async function detectCheckpoint(page) {
@@ -120,7 +195,8 @@ async function detectCheckpoint(page) {
120
195
  return (
121
196
  url.includes("checkpoint") ||
122
197
  url.includes("challenge") ||
123
- url.includes("verification")
198
+ url.includes("verification") ||
199
+ url.includes("consumer-login/error")
124
200
  );
125
201
  }
126
202
 
@@ -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;
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const crypto = require("crypto");
4
4
  const logger = require("../utils/logger");
5
+ const SessionLock = require("./SessionLock"); // Keeping specific import, though widely used via login
5
6
 
6
7
  let sessionDir = path.join(process.cwd(), "data", "linkedin");
7
8
 
@@ -14,11 +15,7 @@ function setSessionDir(dirPath) {
14
15
  }
15
16
 
16
17
  const ALGORITHM = "aes-256-cbc";
17
- // Use a fixed key if not provided in env (for development convenience, but ideally should be in env)
18
- // Ensure the key is 32 bytes.
19
18
  const SECRET_KEY = process.env.SESSION_SECRET || "default_insecure_secret_key_32_bytes_long!!";
20
-
21
- // Ensure key is exactly 32 bytes for aes-256-cbc
22
19
  const key = crypto.createHash("sha256").update(String(SECRET_KEY)).digest();
23
20
 
24
21
  function encrypt(text) {
@@ -39,55 +36,44 @@ function decrypt(text) {
39
36
  decrypted += decipher.final('utf8');
40
37
  return decrypted;
41
38
  } catch (error) {
42
- // If decryption fails, it might be a plain JSON file or invalid key
43
39
  return null;
44
40
  }
45
41
  }
46
42
 
47
43
  /**
48
- * Sanitizes a username to be safe for filenames.
49
- * Replaces special characters with underscores or descriptive text.
50
- * @param {string} username
51
- * @returns {string}
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
52
48
  */
53
- function sanitizeUsername(username) {
54
- if (!username) return "default_session";
55
- return username
56
- .replace(/@/g, "_at_")
57
- .replace(/\./g, "_dot_")
58
- .replace(/[^a-zA-Z0-9_\-]/g, "_");
49
+ function getSessionHash(email) {
50
+ if (!email) return "default";
51
+ return crypto.createHash("sha256").update(email).digest("hex");
59
52
  }
60
53
 
61
- function getSessionPath(username) {
62
- const filename = `${sanitizeUsername(username)}.json`;
54
+ function getSessionPath(email) {
55
+ const filename = `${getSessionHash(email)}.json`;
63
56
  return path.join(sessionDir, filename);
64
57
  }
65
58
 
66
- function sessionExists(username) {
67
- return fs.existsSync(getSessionPath(username));
59
+ function sessionExists(email) {
60
+ return fs.existsSync(getSessionPath(email));
68
61
  }
69
62
 
70
- const { SESSION_MAX_AGE } = require("../config");
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;
71
67
 
72
68
  const defaultStorage = {
73
- /**
74
- * Reads session data for a user.
75
- * @param {string} username
76
- * @returns {Promise<string|null>} Encrypted session string or null
77
- */
78
- async read(username) {
79
- const filePath = getSessionPath(username);
69
+ async read(email) {
70
+ const filePath = getSessionPath(email);
80
71
  if (!fs.existsSync(filePath)) return null;
81
72
  return fs.readFileSync(filePath, "utf-8");
82
73
  },
83
74
 
84
- /**
85
- * Writes session data for a user.
86
- * @param {string} username
87
- * @param {string} data - Encrypted session string
88
- */
89
- async write(username, data) {
90
- const filePath = getSessionPath(username);
75
+ async write(email, data) {
76
+ const filePath = getSessionPath(email);
91
77
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
92
78
  fs.writeFileSync(filePath, data, "utf-8");
93
79
  }
@@ -95,10 +81,6 @@ const defaultStorage = {
95
81
 
96
82
  let currentStorage = defaultStorage;
97
83
 
98
- /**
99
- * Sets a custom session storage adapter.
100
- * @param {Object} adapter - Object with read(username) and write(username, data) methods.
101
- */
102
84
  function setSessionStorage(adapter) {
103
85
  if (adapter && typeof adapter.read === 'function' && typeof adapter.write === 'function') {
104
86
  currentStorage = adapter;
@@ -108,47 +90,43 @@ function setSessionStorage(adapter) {
108
90
  }
109
91
 
110
92
  /**
111
- * Saves the browser context storage state to an encrypted file/storage with a timestamp.
93
+ * Saves the browser context state.
112
94
  * @param {import('playwright').BrowserContext} context
113
- * @param {string} [username] - The username to associate with this session
95
+ * @param {string} email
96
+ * @param {boolean} [isValidated] - If true, updates lastValidatedAt
114
97
  */
115
- async function saveSession(context, username) {
98
+ async function saveSession(context, email, isValidated = false) {
116
99
  try {
117
100
  const state = await context.storageState();
118
101
 
119
- // Wrap state with metadata
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
+
120
106
  const sessionData = {
121
- timestamp: Date.now(),
107
+ timestamp: Date.now(), // Last saved
108
+ lastValidatedAt: isValidated ? Date.now() : undefined,
122
109
  state: state
110
+ // Future: proxy binding here
123
111
  };
124
112
 
125
113
  const jsonString = JSON.stringify(sessionData);
126
114
  const encryptedData = encrypt(jsonString);
127
115
 
128
- await currentStorage.write(username || "default", encryptedData);
116
+ await currentStorage.write(email || "default", encryptedData);
129
117
 
130
- logger.info(`Session saved successfully for ${username || "default"} (Encrypted & Timestamped) 🔒`);
118
+ logger.info(`Session saved for ${email} (Validated: ${isValidated}) 🔒`);
131
119
  } catch (error) {
132
120
  logger.error(`Failed to save session: ${error.message}`);
133
121
  }
134
122
  }
135
123
 
136
124
  /**
137
- * Loads the session from storage and creates a new context.
138
- * Checks for session expiry.
139
- * @param {import('playwright').Browser} browser
140
- * @param {Object} options - Context options
141
- * @param {string} [username] - The username to load session for
142
- * @returns {Promise<import('playwright').BrowserContext>}
125
+ * Loads the session.
126
+ * @returns {Promise<{ context: import('playwright').BrowserContext, needsValidation: boolean }>}
143
127
  */
144
- async function loadSession(browser, options = {}, username) {
145
- const user = username || "default";
146
-
147
- // Check existence if storage supports it, otherwise generic check
148
- // For custom storage, read() returning null implies not found.
149
-
150
- // Note: sessionExists is tied to FS. We should deprecate it or update it.
151
- // But for now, let's rely on read() returning null.
128
+ async function loadSession(browser, options = {}, email) {
129
+ const user = email || "default";
152
130
 
153
131
  try {
154
132
  const fileContent = await currentStorage.read(user);
@@ -158,54 +136,47 @@ async function loadSession(browser, options = {}, username) {
158
136
  return null;
159
137
  }
160
138
 
161
- let sessionData;
162
- let state;
163
-
164
- // Try verifying if it is already JSON (legacy/plain)
165
- try {
166
- const parsed = JSON.parse(fileContent);
167
-
168
- // Check if it matches the new structure { timestamp, state }
169
- if (parsed.timestamp && parsed.state) {
170
- sessionData = parsed; // It was plain JSON but with new structure (unlikely but possible)
171
- } else {
172
- // It's the old structure (just storageState)
173
- state = parsed;
174
- logger.info("Loaded plain JSON session (Legacy).");
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;
175
150
  }
176
- } catch (e) {
177
- // Not JSON, try decrypting
178
- const decrypted = decrypt(fileContent);
179
- if (decrypted) {
151
+ } else {
152
+ // Fallback for legacy plain JSON?
180
153
  try {
181
- const parsedDecrypted = JSON.parse(decrypted);
182
- // Check if it matches the new structure { timestamp, state }
183
- if (parsedDecrypted.timestamp && parsedDecrypted.state) {
184
- sessionData = parsedDecrypted;
185
- } else {
186
- // It's the old encrypted structure (just storageState)
187
- state = parsedDecrypted;
188
- logger.info("Loaded encrypted session (Legacy structure).");
189
- }
190
- } catch (parseError) {
191
- logger.error("Failed to parse decrypted session.");
192
- return null;
193
- }
194
- } else {
195
- logger.error("Failed to decrypt session data. It might be corrupt or key mismatch.");
196
- return null;
197
- }
154
+ sessionData = JSON.parse(fileContent);
155
+ } catch(e) {}
198
156
  }
199
157
 
200
- // Process new structure if found
201
- if (sessionData) {
202
- const age = Date.now() - sessionData.timestamp;
203
- if (age > SESSION_MAX_AGE) {
204
- logger.warn(`Session expired. Age: ${age}ms > Max: ${SESSION_MAX_AGE}ms. Rejecting session.`);
158
+ if (!sessionData || !sessionData.state) {
159
+ logger.warn("Invalid or corrupt session data.");
205
160
  return null;
206
- }
207
- state = sessionData.state;
208
- logger.info(`Loaded active session for ${username || "default"} (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
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
+ }
209
180
  }
210
181
 
211
182
  const context = await browser.newContext({
@@ -213,7 +184,13 @@ async function loadSession(browser, options = {}, username) {
213
184
  ...options
214
185
  });
215
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
+
216
192
  return context;
193
+
217
194
  } catch (error) {
218
195
  logger.error(`Error loading session: ${error.message}`);
219
196
  return null;
@@ -226,5 +203,6 @@ module.exports = {
226
203
  sessionExists,
227
204
  getSessionPath,
228
205
  saveSession,
229
- loadSession
206
+ loadSession,
207
+ SessionLock
230
208
  };
package/cli.js DELETED
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env node
2
- const logger = require("./src/utils/logger");
3
- require("dotenv").config();
4
-
5
- const loginToLinkedIn = require("./src/login/login");
6
- const REPL = require("./src/cli/repl");
7
-
8
- let browserInstance = null;
9
-
10
- // Cleanup function to be called by REPL or SIGINT
11
- async function cleanup() {
12
- if (browserInstance) {
13
- console.log("Closing browser...");
14
- try {
15
- await browserInstance.close();
16
- console.log("Browser closed.");
17
- } catch (err) {
18
- console.error("Error closing browser:", err.message);
19
- }
20
- browserInstance = null;
21
- }
22
- }
23
-
24
- // Handle Ctrl+C (SIGINT) for graceful shutdown fallback
25
- process.on("SIGINT", async () => {
26
- console.log("\nReceived stop signal.");
27
- await cleanup();
28
- process.exit(0);
29
- });
30
-
31
- (async () => {
32
- try {
33
- // Check command line arguments
34
- const args = process.argv.slice(2);
35
-
36
- if (args.includes("--help") || args.includes("-h")) {
37
- console.log(`
38
- LinkedIn Automation CLI
39
-
40
- Usage:
41
- linkedin [options]
42
-
43
- Options:
44
- --visible Run in visible mode (default is headless)
45
- --help, -h Show this help message
46
- --version, -v Show version number
47
- `);
48
- process.exit(0);
49
- }
50
-
51
- if (args.includes("--version") || args.includes("-v")) {
52
- const packageJson = require("./package.json");
53
- console.log(`v${packageJson.version}`);
54
- process.exit(0);
55
- }
56
-
57
- const { selectUser } = require("./src/cli/userSelection");
58
-
59
- const isVisible = args.includes("--visible");
60
- const isHeadless = !isVisible;
61
-
62
- // Select User
63
- const credentials = await selectUser();
64
-
65
- console.log(`Starting bot in ${isVisible ? "Visible" : "Headless"} Mode...`);
66
-
67
- // Pass credentials (or null) to login function
68
- const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless }, credentials);
69
- browserInstance = browser;
70
-
71
- console.log("Login successful.");
72
-
73
- // Start Interactive CLI
74
- const repl = new REPL({
75
- browser: browserInstance,
76
- cleanup: cleanup
77
- });
78
- repl.start();
79
-
80
- } catch (error) {
81
- console.error("Error occurred:", error);
82
- process.exit(1);
83
- }
84
- })();
package/src/cli/repl.js DELETED
@@ -1,68 +0,0 @@
1
- const readline = require("readline");
2
-
3
- class REPL {
4
- constructor(context = {}) {
5
- this.context = context;
6
- this.rl = readline.createInterface({
7
- input: process.stdin,
8
- output: process.stdout,
9
- prompt: "linkedin-bot> ",
10
- });
11
- }
12
-
13
- start() {
14
- console.log("Interactive CLI started. Type 'help' for commands.");
15
- this.rl.prompt();
16
-
17
- this.rl.on("line", async (line) => {
18
- const input = line.trim();
19
-
20
- switch (input) {
21
- case "help":
22
- console.log(`
23
- Available commands:
24
- status - Show bot status
25
- exit - Stop the bot and exit
26
- quit - Alias for exit
27
- help - Show this help message
28
- `);
29
- break;
30
-
31
- case "status":
32
- if (this.context.browser && this.context.browser.isConnected()) {
33
- console.log("Status: 🟢 Connected");
34
- const pages = this.context.browser.contexts()[0]?.pages().length || 0;
35
- console.log(`Open pages: ${pages}`);
36
- } else {
37
- console.log("Status: 🔴 Disconnected");
38
- }
39
- break;
40
-
41
- case "exit":
42
- case "quit":
43
- console.log("Exiting...");
44
- this.rl.close();
45
- if (this.context.cleanup) {
46
- await this.context.cleanup();
47
- }
48
- process.exit(0);
49
- break;
50
-
51
- case "":
52
- break;
53
-
54
- default:
55
- console.log(`Unknown command: '${input}'. Type 'help' for available commands.`);
56
- break;
57
- }
58
- this.rl.prompt();
59
- });
60
-
61
- this.rl.on("close", () => {
62
- console.log("\nCLI session ended.");
63
- process.exit(0);
64
- });
65
- }
66
- }
67
-
68
- module.exports = REPL;
@@ -1,73 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const readline = require('readline');
4
- const logger = require('../utils/logger');
5
-
6
- const USERS_FILE = path.join(process.cwd(), 'users.js');
7
-
8
- /**
9
- * Loads users from users.js and prompts for selection if multiple exist.
10
- * @returns {Promise<Object|null>} Selected credentials { username, password } or null if using env.
11
- */
12
- async function selectUser() {
13
- if (!fs.existsSync(USERS_FILE)) {
14
- logger.info("No users.js file found. Using environment variables.");
15
- return null;
16
- }
17
-
18
- let users = [];
19
- try {
20
- users = require(USERS_FILE);
21
- if (!Array.isArray(users) || users.length === 0) {
22
- logger.warn("users.js exists but exports an empty array or invalid format.");
23
- return null;
24
- }
25
- } catch (error) {
26
- logger.error(`Error loading users.js: ${error.message}`);
27
- return null;
28
- }
29
-
30
- if (users.length === 1) {
31
- logger.info(`Single user found in users.js: ${users[0].username}`);
32
- return users[0];
33
- }
34
-
35
- // Multiple users - Prompt for selection
36
- console.log('\nSelect a user to login:');
37
- users.forEach((u, index) => {
38
- console.log(`${index + 1}. ${u.username}`);
39
- });
40
- console.log(`${users.length + 1}. Use Environment Variables (.env)`);
41
-
42
- const selectedIndex = await new Promise((resolve) => {
43
- const rl = readline.createInterface({
44
- input: process.stdin,
45
- output: process.stdout
46
- });
47
-
48
- const ask = () => {
49
- rl.question('\nEnter number: ', (answer) => {
50
- const num = parseInt(answer);
51
- if (!isNaN(num) && num >= 1 && num <= users.length + 1) {
52
- rl.close();
53
- resolve(num - 1);
54
- } else {
55
- console.log('Invalid selection. Try again.');
56
- ask();
57
- }
58
- });
59
- };
60
- ask();
61
- });
62
-
63
- if (selectedIndex === users.length) {
64
- logger.info("Selected: Environment Variables");
65
- return null; // Fallback to env
66
- }
67
-
68
- const selectedUser = users[selectedIndex];
69
- logger.info(`Selected user: ${selectedUser.username}`);
70
- return selectedUser;
71
- }
72
-
73
- module.exports = { selectUser };