@ubaidbinwaris/linkedin 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UbaidBinWaris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,84 @@
1
+ # LinkedIn Automation Bot
2
+
3
+ A robust Node.js automation tool designed to interact with LinkedIn using [Playwright](https://playwright.dev/). This bot is built with stealth features to mimic human behavior, manage sessions efficiently, and provide an interactive CLI for control.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **Stealth Automation**: Uses `puppeteer-extra-plugin-stealth` with Playwright to minimize detection risks.
8
+ - **Session Management**: Automatically saves and loads session states (cookies & local storage) to prevent frequent re-logins.
9
+ - **Headless & Visible Modes**: configurable execution modes for debugging or background operation.
10
+ - **Interactive CLI**: integrated REPL (Read-Eval-Print Loop) to control the bot instance after initialization.
11
+ - **Smart Validation**: robust checks to verify login status and handle checkpoints/verifications manually if needed.
12
+ - **Human-like Behavior**: Implements random delays, mouse movements, and dynamic user agents.
13
+
14
+ ## 🛠️ Prerequisites
15
+
16
+ - **Node.js** (v14 or higher recommended)
17
+ - **npm** (Node Package Manager)
18
+ - A valid **LinkedIn Account**
19
+
20
+ ## 📦 Installation
21
+
22
+ 1. **Clone the repository:**
23
+ ```bash
24
+ git clone https://github.com/UbaidBinWaris/linkedin.git
25
+ cd linkedin
26
+ ```
27
+
28
+ 2. **Install dependencies:**
29
+ ```bash
30
+ npm install
31
+ ```
32
+
33
+ 3. **Install Playwright browsers:**
34
+ ```bash
35
+ npx playwright install chromium
36
+ ```
37
+
38
+ ## ⚙️ Configuration
39
+
40
+ 1. Create a `.env` file in the root directory.
41
+ 2. Add your LinkedIn credentials:
42
+
43
+ ```env
44
+ LINKEDIN_EMAIL=your_email@example.com
45
+ LINKEDIN_PASSWORD=your_password
46
+ ```
47
+
48
+ ## ▶️ Usage
49
+
50
+ ### Run in Headless Mode (Default)
51
+ Runs the bot in the background without a visible browser window.
52
+
53
+ ```bash
54
+ npm start
55
+ ```
56
+
57
+ ### Run in Visible Mode
58
+ Useful for debugging or if manual intervention (CAPTCHA) is required.
59
+
60
+ ```bash
61
+ npm start -- --visible
62
+ ```
63
+
64
+ ### CLI Commands
65
+ Once the bot is running, you can interact with it through the terminal. (See `src/cli/repl.js` for available commands).
66
+
67
+ ## 📂 Project Structure
68
+
69
+ ```
70
+ ├── src/
71
+ │ ├── cli/ # Command Line Interface logic
72
+ │ ├── login/ # Login automation & checkpoint handling
73
+ │ ├── session/ # Session storage & management
74
+ │ ├── utils/ # Helper functions (logger, timing, etc.)
75
+ │ └── config.js # Configuration constants
76
+ ├── data/ # Stored session data (cookies, etc.)
77
+ ├── logs/ # Application logs
78
+ ├── index.js # Entry point
79
+ └── package.json # Dependencies & scripts
80
+ ```
81
+
82
+ ## ⚠️ Disclaimer
83
+
84
+ 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.
package/cli.js ADDED
@@ -0,0 +1,56 @@
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
+ const isVisible = args.includes("--visible");
36
+ const isHeadless = !isVisible;
37
+
38
+ console.log(`Starting bot in ${isVisible ? "Visible" : "Headless"} Mode...`);
39
+
40
+ const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless });
41
+ browserInstance = browser;
42
+
43
+ console.log("Login successful.");
44
+
45
+ // Start Interactive CLI
46
+ const repl = new REPL({
47
+ browser: browserInstance,
48
+ cleanup: cleanup
49
+ });
50
+ repl.start();
51
+
52
+ } catch (error) {
53
+ console.error("Error occurred:", error);
54
+ process.exit(1);
55
+ }
56
+ })();
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ const loginToLinkedIn = require('./src/login/login');
2
+ const sessionManager = require('./src/session/sessionManager');
3
+ const config = require('./src/config');
4
+ const logger = require('./src/utils/logger');
5
+ // Exporting the main function and other utilities for library usage
6
+ module.exports = {
7
+ loginToLinkedIn,
8
+ sessionManager,
9
+ config,
10
+ logger
11
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@ubaidbinwaris/linkedin",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "linkedin": "./cli.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "index.js",
12
+ "cli.js",
13
+ "Readme.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "test": "echo \"Error: no test specified\" && exit 1",
18
+ "start": "node cli.js"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/UbaidBinWaris/linkedin.git"
23
+ },
24
+ "keywords": [
25
+ "linkedin",
26
+ "automation",
27
+ "bot",
28
+ "playwright"
29
+ ],
30
+ "author": "UbaidBinWaris",
31
+ "license": "MIT",
32
+ "type": "commonjs",
33
+ "bugs": {
34
+ "url": "https://github.com/UbaidBinWaris/linkedin/issues"
35
+ },
36
+ "homepage": "https://github.com/UbaidBinWaris/linkedin#readme",
37
+ "dependencies": {
38
+ "dotenv": "^17.2.4",
39
+ "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
+ "winston": "^3.19.0"
44
+ }
45
+ }
@@ -0,0 +1,68 @@
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;
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: 30 * 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,261 @@
1
+ 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
+ const { waitForUserResume } = require("../utils/terminal");
8
+ const { sessionExists, saveSession, loadSession } = require("../session/sessionManager");
9
+ const { randomDelay } = require("../utils/time");
10
+
11
+ /**
12
+ * Validates that necessary environment variables are set.
13
+ * @throws {Error} If credentials are missing.
14
+ */
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.");
18
+ }
19
+ }
20
+
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) {
27
+ 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;
36
+ }
37
+
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
45
+ }
46
+
47
+ return false;
48
+ } catch (error) {
49
+ logger.error(`Error during checkpoint detection: ${error.message}`);
50
+ return false;
51
+ }
52
+ }
53
+
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
+ }
62
+
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
+
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
+ async function loginToLinkedIn(options = {}) {
80
+ logger.info("Starting LinkedIn login process with stealth mode...");
81
+
82
+ try {
83
+ validateCredentials();
84
+ } catch (error) {
85
+ logger.error(error.message);
86
+ throw error;
87
+ }
88
+
89
+ const launchOptions = {
90
+ headless: options.headless !== undefined ? options.headless : false,
91
+ slowMo: options.slowMo || 50,
92
+ args: [
93
+ "--no-sandbox",
94
+ "--disable-setuid-sandbox",
95
+ "--disable-blink-features=AutomationControlled" // Extra stealth
96
+ ],
97
+ ...options,
98
+ };
99
+
100
+ logger.info(`Launching browser...`);
101
+ const browser = await chromium.launch(launchOptions);
102
+
103
+ let context;
104
+ let page;
105
+
106
+ const contextOptions = {
107
+ userAgent: getRandomUserAgent(),
108
+ viewport: getRandomViewport(),
109
+ locale: 'en-US',
110
+ timezoneId: 'America/New_York', // Align with proxy if used, otherwise standard
111
+ permissions: ['geolocation'],
112
+ ignoreHTTPSErrors: true,
113
+ };
114
+
115
+ try {
116
+ // -----------------------------
117
+ // STEP 1: Try Using Saved Session
118
+ // -----------------------------
119
+ logger.info("Checking for saved session...");
120
+ context = await loadSession(browser, contextOptions);
121
+
122
+ if (context) {
123
+ logger.info("Session stored context created.");
124
+ } else {
125
+ logger.info("No valid session found. Starting fresh context.");
126
+ context = await browser.newContext(contextOptions);
127
+ }
128
+
129
+ page = await context.newPage();
130
+
131
+ // Set a default timeout for all actions
132
+ page.setDefaultTimeout(30000);
133
+
134
+ logger.info("Navigating to LinkedIn feed...");
135
+ await page.goto("https://www.linkedin.com/feed/", {
136
+ waitUntil: "domcontentloaded",
137
+ });
138
+
139
+ // Human-like pause
140
+ await randomDelay(2000, 4000);
141
+
142
+ // Check for checkpoint immediately after navigation
143
+ if (await detectCheckpoint(page)) {
144
+ logger.warn("Checkpoint detected immediately. Manual verification required.");
145
+ await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
146
+ }
147
+
148
+ const { VALIDATION_SELECTORS } = require("../config");
149
+
150
+ // Verify Session Validity
151
+ try {
152
+ // Check for any of the validation selectors
153
+ const isLoggedIn = await Promise.race([
154
+ Promise.any(VALIDATION_SELECTORS.map(selector =>
155
+ page.waitForSelector(selector, { timeout: 15000 }).then(() => true)
156
+ )),
157
+ page.waitForSelector('.login-form, #username, input[name="session_key"]', { timeout: 5000 }).then(() => false)
158
+ ]).catch(() => false);
159
+
160
+ if (isLoggedIn) {
161
+ logger.info("Session is valid. Login successful.");
162
+ return { browser, context, page };
163
+ } else {
164
+ logger.info("Session invalid or redirected to login page.");
165
+ }
166
+ } catch (err) {
167
+ logger.info("Could not verify session state. Proceeding to credential login.");
168
+ }
169
+
170
+ // CRITICAL FIX: If session was invalid, we MUST close the current context
171
+ // and start fresh. Otherwise, retained cookies might cause redirect loops
172
+ // (e.g. we go to /login, but LinkedIn sees cookies and redirects to /feed,
173
+ // so we can't find the email input).
174
+ logger.info("Closing invalid/expired session context...");
175
+ await context.close();
176
+
177
+ logger.info("Starting fresh context for credential login...");
178
+ context = await browser.newContext(contextOptions);
179
+ page = await context.newPage();
180
+ page.setDefaultTimeout(30000);
181
+
182
+ // -----------------------------
183
+ // STEP 2: Credential Login
184
+ // -----------------------------
185
+ logger.info("Proceeding to credential login...");
186
+
187
+ if (!page.url().includes("login") && !page.url().includes("uas/request-password-reset")) {
188
+ await page.goto("https://www.linkedin.com/login", { waitUntil: 'domcontentloaded' });
189
+ await randomDelay(1000, 2000);
190
+ }
191
+
192
+ const email = process.env.LINKEDIN_EMAIL;
193
+ const password = process.env.LINKEDIN_PASSWORD;
194
+
195
+ logger.info("Entering credentials...");
196
+
197
+ // Simulate human typing
198
+ await page.click('input[name="session_key"]');
199
+ await randomDelay(500, 1000);
200
+ await page.type('input[name="session_key"]', email, { delay: 100 }); // Type with delay
201
+
202
+ await randomDelay(1000, 2000);
203
+
204
+ await page.click('input[name="session_password"]');
205
+ await page.type('input[name="session_password"]', password, { delay: 100 });
206
+
207
+ await randomDelay(1000, 2000);
208
+
209
+ logger.info("Submitting login form...");
210
+ await Promise.all([
211
+ page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
212
+ page.click('button[type="submit"]')
213
+ ]);
214
+
215
+ // Check for checkpoint again
216
+ if (await detectCheckpoint(page)) {
217
+ logger.warn("Checkpoint detected after login attempt. Manual verification required.");
218
+ await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
219
+ }
220
+
221
+ // -----------------------------
222
+ // Post-Login Verification
223
+ // -----------------------------
224
+ logger.info("Verifying login success...");
225
+ try {
226
+ await page.waitForURL("**/feed**", { timeout: 20000 });
227
+
228
+ // Wait for at least one validation selector
229
+ await Promise.any(VALIDATION_SELECTORS.map(selector =>
230
+ page.waitForSelector(selector, { timeout: 15000 })
231
+ ));
232
+
233
+ logger.info("Login confirmed ✅");
234
+
235
+ // Save session state
236
+ await saveSession(context);
237
+ logger.info("Session state saved 💾");
238
+
239
+ return { browser, context, page };
240
+
241
+ } catch (err) {
242
+ logger.error("Login failed or timed out waiting for feed.");
243
+ const screenshotPath = `error_login_${Date.now()}.png`;
244
+ try {
245
+ await page.screenshot({ path: screenshotPath });
246
+ logger.info(`Screenshot saved to ${screenshotPath}`);
247
+ } catch (opts) {
248
+ console.error("Failed to take error screenshot");
249
+ }
250
+
251
+ throw new Error("Login failed: Could not reach feed page.");
252
+ }
253
+
254
+ } catch (error) {
255
+ logger.error(`Critical error in loginToLinkedIn: ${error.message}`);
256
+ if (browser) await browser.close();
257
+ throw error;
258
+ }
259
+ }
260
+
261
+ module.exports = loginToLinkedIn;
@@ -0,0 +1,155 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const crypto = require("crypto");
4
+ const logger = require("../utils/logger");
5
+
6
+ const SESSION_PATH = path.join(__dirname, "../../data/session.json");
7
+ const ALGORITHM = "aes-256-cbc";
8
+ // Use a fixed key if not provided in env (for development convenience, but ideally should be in env)
9
+ // Ensure the key is 32 bytes.
10
+ const SECRET_KEY = process.env.SESSION_SECRET || "default_insecure_secret_key_32_bytes_long!!";
11
+
12
+ // Ensure key is exactly 32 bytes for aes-256-cbc
13
+ const key = crypto.createHash("sha256").update(String(SECRET_KEY)).digest();
14
+
15
+ function encrypt(text) {
16
+ const iv = crypto.randomBytes(16);
17
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
18
+ let encrypted = cipher.update(text, 'utf8', 'hex');
19
+ encrypted += cipher.final('hex');
20
+ return iv.toString('hex') + ':' + encrypted;
21
+ }
22
+
23
+ function decrypt(text) {
24
+ try {
25
+ const textParts = text.split(':');
26
+ const iv = Buffer.from(textParts.shift(), 'hex');
27
+ const encryptedText = textParts.join(':');
28
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
29
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
30
+ decrypted += decipher.final('utf8');
31
+ return decrypted;
32
+ } catch (error) {
33
+ // If decryption fails, it might be a plain JSON file or invalid key
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function sessionExists() {
39
+ return fs.existsSync(SESSION_PATH);
40
+ }
41
+
42
+ function getSessionPath() {
43
+ return SESSION_PATH;
44
+ }
45
+
46
+ const { SESSION_MAX_AGE } = require("../config");
47
+
48
+ /**
49
+ * Saves the browser context storage state to an encrypted file with a timestamp.
50
+ * @param {import('playwright').BrowserContext} context
51
+ */
52
+ async function saveSession(context) {
53
+ try {
54
+ const state = await context.storageState();
55
+
56
+ // Wrap state with metadata
57
+ const sessionData = {
58
+ timestamp: Date.now(),
59
+ state: state
60
+ };
61
+
62
+ const jsonString = JSON.stringify(sessionData);
63
+ const encryptedData = encrypt(jsonString);
64
+
65
+ fs.mkdirSync(path.dirname(SESSION_PATH), { recursive: true });
66
+ fs.writeFileSync(SESSION_PATH, encryptedData, "utf-8");
67
+ logger.info("Session saved successfully (Encrypted & Timestamped) 🔒");
68
+ } catch (error) {
69
+ logger.error(`Failed to save session: ${error.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Loads the session from the encrypted file and creates a new context.
75
+ * Checks for session expiry.
76
+ * @param {import('playwright').Browser} browser
77
+ * @param {Object} options - Context options
78
+ * @returns {Promise<import('playwright').BrowserContext>}
79
+ */
80
+ async function loadSession(browser, options = {}) {
81
+ if (!sessionExists()) {
82
+ logger.info("No session file found.");
83
+ return null;
84
+ }
85
+
86
+ try {
87
+ const fileContent = fs.readFileSync(SESSION_PATH, "utf-8");
88
+ let sessionData;
89
+ let state;
90
+
91
+ // Try verifying if it is already JSON (legacy/plain)
92
+ try {
93
+ const parsed = JSON.parse(fileContent);
94
+
95
+ // Check if it matches the new structure { timestamp, state }
96
+ if (parsed.timestamp && parsed.state) {
97
+ sessionData = parsed; // It was plain JSON but with new structure (unlikely but possible)
98
+ } else {
99
+ // It's the old structure (just storageState)
100
+ state = parsed;
101
+ logger.info("Loaded plain JSON session (Legacy).");
102
+ }
103
+ } catch (e) {
104
+ // Not JSON, try decrypting
105
+ const decrypted = decrypt(fileContent);
106
+ if (decrypted) {
107
+ try {
108
+ const parsedDecrypted = JSON.parse(decrypted);
109
+ // Check if it matches the new structure { timestamp, state }
110
+ if (parsedDecrypted.timestamp && parsedDecrypted.state) {
111
+ sessionData = parsedDecrypted;
112
+ } else {
113
+ // It's the old encrypted structure (just storageState)
114
+ state = parsedDecrypted;
115
+ logger.info("Loaded encrypted session (Legacy structure).");
116
+ }
117
+ } catch (parseError) {
118
+ logger.error("Failed to parse decrypted session.");
119
+ return null;
120
+ }
121
+ } else {
122
+ logger.error("Failed to decrypt session file. It might be corrupt or key mismatch.");
123
+ return null;
124
+ }
125
+ }
126
+
127
+ // Process new structure if found
128
+ if (sessionData) {
129
+ const age = Date.now() - sessionData.timestamp;
130
+ if (age > SESSION_MAX_AGE) {
131
+ logger.warn(`Session expired. Age: ${age}ms > Max: ${SESSION_MAX_AGE}ms. Rejecting session.`);
132
+ return null;
133
+ }
134
+ state = sessionData.state;
135
+ logger.info(`Loaded active session (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
136
+ }
137
+
138
+ const context = await browser.newContext({
139
+ storageState: state,
140
+ ...options
141
+ });
142
+
143
+ return context;
144
+ } catch (error) {
145
+ logger.error(`Error loading session: ${error.message}`);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ module.exports = {
151
+ sessionExists,
152
+ getSessionPath,
153
+ saveSession,
154
+ loadSession
155
+ };
@@ -0,0 +1,30 @@
1
+ const { createLogger, format, transports } = require("winston");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ const logDir = path.join(__dirname, "../../logs");
6
+
7
+ if (!fs.existsSync(logDir)) {
8
+ fs.mkdirSync(logDir, { recursive: true });
9
+ }
10
+
11
+ const logger = createLogger({
12
+ level: "info",
13
+ format: format.combine(
14
+ format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
15
+ format.printf(({ timestamp, level, message }) => {
16
+ return `${timestamp} [${level.toUpperCase()}] ${message}`;
17
+ })
18
+ ),
19
+ transports: [
20
+ new transports.File({
21
+ filename: path.join(logDir, "error.log"),
22
+ level: "error",
23
+ }),
24
+ new transports.File({
25
+ filename: path.join(logDir, "combined.log"),
26
+ }),
27
+ ],
28
+ });
29
+
30
+ module.exports = logger;
@@ -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 };