@ubaidbinwaris/linkedin 1.0.0 → 1.0.8

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,84 +1,185 @@
1
- # LinkedIn Automation Bot
1
+ # LinkedIn Automation Bot & Module 🤖
2
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.
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).
4
4
 
5
- ## 🚀 Features
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.
6
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.
7
+ ---
13
8
 
14
- ## 🛠️ Prerequisites
9
+ ## 🚀 Key Features
15
10
 
16
- - **Node.js** (v14 or higher recommended)
17
- - **npm** (Node Package Manager)
18
- - A valid **LinkedIn Account**
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.
20
+
21
+ ---
19
22
 
20
23
  ## 📦 Installation
21
24
 
22
- 1. **Clone the repository:**
23
- ```bash
24
- git clone https://github.com/UbaidBinWaris/linkedin.git
25
- cd linkedin
26
- ```
25
+ ### 1. Install via NPM
26
+ ```bash
27
+ npm install @ubaidbinwaris/linkedin
28
+ ```
27
29
 
28
- 2. **Install dependencies:**
29
- ```bash
30
- npm install
31
- ```
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
36
+ ```
32
37
 
33
- 3. **Install Playwright browsers:**
34
- ```bash
35
- npx playwright install chromium
36
- ```
38
+ ---
37
39
 
38
- ## ⚙️ Configuration
40
+ ## ▶️ Usage: CLI Mode
39
41
 
40
- 1. Create a `.env` file in the root directory.
41
- 2. Add your LinkedIn credentials:
42
+ You can run the bot directly from the command line.
42
43
 
43
- ```env
44
- LINKEDIN_EMAIL=your_email@example.com
45
- LINKEDIN_PASSWORD=your_password
46
- ```
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
+ ```
47
54
 
48
- ## ▶️ Usage
55
+ ### 2. Multi-User Mode
56
+ To manage multiple accounts, create a `users.js` file in the root directory:
49
57
 
50
- ### Run in Headless Mode (Default)
51
- Runs the bot in the background without a visible browser window.
58
+ **File:** `users.js`
59
+ ```javascript
60
+ module.exports = [
61
+ { username: "alice@example.com", password: "password123" },
62
+ { username: "bob@company.com", password: "securePass!" }
63
+ ];
64
+ ```
52
65
 
66
+ Run the bot:
53
67
  ```bash
54
68
  npm start
55
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)
76
+ ```
56
77
 
57
- ### Run in Visible Mode
58
- Useful for debugging or if manual intervention (CAPTCHA) is required.
78
+ ---
59
79
 
60
- ```bash
61
- npm start -- --visible
80
+ ## 🔌 Usage: Module Integration
81
+
82
+ This package is designed to be embedded in larger systems (e.g., a SaaS backend managing hundreds of accounts).
83
+
84
+ ### Import
85
+ ```javascript
86
+ const linkedin = require('@ubaidbinwaris/linkedin');
62
87
  ```
63
88
 
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).
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
+
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;
99
+ },
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
+ );
107
+ }
108
+ });
109
+ ```
66
110
 
67
- ## 📂 Project Structure
111
+ ### 2. Custom Logger
112
+ Redirect bot logs to your application's logging system (e.g., Winston, Pino, Bunyan).
68
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
+ });
69
121
  ```
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
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
+ }
80
157
  ```
81
158
 
82
- ## ⚠️ Disclaimer
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
+ ---
83
183
 
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.
184
+ ## 📄 License
185
+ MIT License.
package/cli.js CHANGED
@@ -32,12 +32,40 @@ process.on("SIGINT", async () => {
32
32
  try {
33
33
  // Check command line arguments
34
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
+
35
59
  const isVisible = args.includes("--visible");
36
60
  const isHeadless = !isVisible;
37
61
 
62
+ // Select User
63
+ const credentials = await selectUser();
64
+
38
65
  console.log(`Starting bot in ${isVisible ? "Visible" : "Headless"} Mode...`);
39
66
 
40
- const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless });
67
+ // Pass credentials (or null) to login function
68
+ const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless }, credentials);
41
69
  browserInstance = browser;
42
70
 
43
71
  console.log("Login successful.");
package/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  const loginToLinkedIn = require('./src/login/login');
2
2
  const sessionManager = require('./src/session/sessionManager');
3
3
  const config = require('./src/config');
4
- const logger = require('./src/utils/logger');
4
+ const logger = require('./src/utils/logger'); // This is now the proxy object
5
5
  // Exporting the main function and other utilities for library usage
6
6
  module.exports = {
7
7
  loginToLinkedIn,
8
8
  sessionManager,
9
+ setSessionDir: sessionManager.setSessionDir,
10
+ setSessionStorage: sessionManager.setSessionStorage,
11
+ setLogger: logger.setLogger,
9
12
  config,
10
13
  logger
11
14
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ubaidbinwaris/linkedin",
3
- "version": "1.0.0",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "linkedin": "./cli.js"
7
+ "linkedin": "cli.js"
8
8
  },
9
9
  "files": [
10
10
  "src",
@@ -0,0 +1,73 @@
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 };
@@ -76,14 +76,26 @@ function getRandomUserAgent() {
76
76
  * @param {number} [options.slowMo=50] - Slow motion delay in ms.
77
77
  * @param {string} [options.proxy] - Optional proxy server URL.
78
78
  */
79
- async function loginToLinkedIn(options = {}) {
79
+ /**
80
+ * Main login function.
81
+ * @param {Object} options - Launch options for the browser.
82
+ * @param {boolean} [options.headless=false] - Whether to run in headless mode.
83
+ * @param {number} [options.slowMo=50] - Slow motion delay in ms.
84
+ * @param {string} [options.proxy] - Optional proxy server URL.
85
+ * @param {Function} [options.onCheckpoint] - Callback when verification is needed in headless mode.
86
+ * @param {Object} [credentials] - Optional credentials object { username, password }
87
+ */
88
+ async function loginToLinkedIn(options = {}, credentials = null) {
80
89
  logger.info("Starting LinkedIn login process with stealth mode...");
81
90
 
82
- try {
83
- validateCredentials();
84
- } catch (error) {
85
- logger.error(error.message);
86
- throw error;
91
+ // Determine credentials
92
+ const email = credentials?.username || process.env.LINKEDIN_EMAIL;
93
+ const password = credentials?.password || process.env.LINKEDIN_PASSWORD;
94
+
95
+ if (!email || !password) {
96
+ const errorMsg = "Missing credentials. Provide them in arguments or set LINKEDIN_EMAIL/LINKEDIN_PASSWORD env vars.";
97
+ logger.error(errorMsg);
98
+ throw new Error(errorMsg);
87
99
  }
88
100
 
89
101
  const launchOptions = {
@@ -97,7 +109,7 @@ async function loginToLinkedIn(options = {}) {
97
109
  ...options,
98
110
  };
99
111
 
100
- logger.info(`Launching browser...`);
112
+ logger.info(`Launching browser for user: ${email}...`);
101
113
  const browser = await chromium.launch(launchOptions);
102
114
 
103
115
  let context;
@@ -116,8 +128,8 @@ async function loginToLinkedIn(options = {}) {
116
128
  // -----------------------------
117
129
  // STEP 1: Try Using Saved Session
118
130
  // -----------------------------
119
- logger.info("Checking for saved session...");
120
- context = await loadSession(browser, contextOptions);
131
+ logger.info(`Checking for saved session for ${email}...`);
132
+ context = await loadSession(browser, contextOptions, email);
121
133
 
122
134
  if (context) {
123
135
  logger.info("Session stored context created.");
@@ -141,11 +153,50 @@ async function loginToLinkedIn(options = {}) {
141
153
 
142
154
  // Check for checkpoint immediately after navigation
143
155
  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...");
156
+ if (launchOptions.headless) {
157
+ logger.warn("Checkpoint detected in headless mode.");
158
+
159
+ if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
160
+ logger.info("Triggering onCheckpoint callback...");
161
+ await options.onCheckpoint();
162
+ logger.info("onCheckpoint resolved. Retrying login...");
163
+ await browser.close();
164
+ return loginToLinkedIn(options, credentials);
165
+ }
166
+
167
+ logger.info("Switching to visible mode for manual verification...");
168
+
169
+ // await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
170
+ logger.info("Automatically launching visible browser for verification...");
171
+
172
+ logger.info("Closing headless browser...");
173
+ await browser.close();
174
+
175
+ logger.info("Launching visible browser for verification...");
176
+ // Call recursively in visible mode
177
+ // We pass the same options but force headless: false
178
+ const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
179
+
180
+ // Once the visible instance returns, it means login was successful and session is saved.
181
+ logger.info("Verification successful in visible mode.");
182
+ logger.info("Closing visible browser and resuming headless session...");
183
+ await visibleInstance.browser.close();
184
+
185
+ // Restart the original headless request.
186
+ // It should now find the valid session and proceed without checkpoints.
187
+ return loginToLinkedIn(options, { username: email, password });
188
+
189
+ } else {
190
+ logger.warn("Checkpoint detected immediately. Manual verification required.");
191
+ if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
192
+ await options.onCheckpoint();
193
+ } else {
194
+ await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
195
+ }
196
+ }
146
197
  }
147
198
 
148
- const { VALIDATION_SELECTORS } = require("../config");
199
+ const { VALIDATION_SELECTORS } = require("../config");
149
200
 
150
201
  // Verify Session Validity
151
202
  try {
@@ -189,9 +240,6 @@ const { VALIDATION_SELECTORS } = require("../config");
189
240
  await randomDelay(1000, 2000);
190
241
  }
191
242
 
192
- const email = process.env.LINKEDIN_EMAIL;
193
- const password = process.env.LINKEDIN_PASSWORD;
194
-
195
243
  logger.info("Entering credentials...");
196
244
 
197
245
  // Simulate human typing
@@ -214,8 +262,55 @@ const { VALIDATION_SELECTORS } = require("../config");
214
262
 
215
263
  // Check for checkpoint again
216
264
  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...");
265
+ if (launchOptions.headless) {
266
+ logger.warn("Checkpoint detected in headless mode (post-login).");
267
+
268
+ if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
269
+ logger.info("Triggering onCheckpoint callback...");
270
+ await options.onCheckpoint();
271
+ logger.info("onCheckpoint resolved. Retrying login...");
272
+ await browser.close();
273
+ return loginToLinkedIn(options, credentials);
274
+ }
275
+
276
+ logger.info("Switching to visible mode for manual verification...");
277
+
278
+ // await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
279
+ logger.info("Automatically launching visible browser for verification...");
280
+
281
+ logger.info("Closing headless browser...");
282
+ await browser.close();
283
+
284
+ logger.info("Launching visible browser for verification...");
285
+ const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
286
+
287
+ logger.info("Verification successful. Resuming headless session...");
288
+ await visibleInstance.browser.close();
289
+
290
+ return loginToLinkedIn(options, { username: email, password });
291
+ } else {
292
+ logger.warn("Checkpoint detected after login attempt. Manual verification required.");
293
+ if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
294
+ await options.onCheckpoint();
295
+ } else {
296
+ logger.info("Waiting for manual verification in the opened browser...");
297
+ logger.info("Please solve the CAPTCHA/verification. The browser will close automatically when you are redirected to the feed.");
298
+
299
+ try {
300
+ // Wait for URL to include '/feed' OR any validation selector to appear
301
+ await page.waitForFunction(() => {
302
+ return window.location.href.includes("/feed") ||
303
+ document.querySelector('.global-nav__search') ||
304
+ document.querySelector('#global-nav-typeahead');
305
+ }, { timeout: 300000 }); // 5 minutes timeout
306
+
307
+ logger.info("Verification detected! resuming...");
308
+ } catch (err) {
309
+ logger.error("Timeout waiting for manual verification.");
310
+ throw new Error("Manual verification timed out.");
311
+ }
312
+ }
313
+ }
219
314
  }
220
315
 
221
316
  // -----------------------------
@@ -233,7 +328,7 @@ const { VALIDATION_SELECTORS } = require("../config");
233
328
  logger.info("Login confirmed ✅");
234
329
 
235
330
  // Save session state
236
- await saveSession(context);
331
+ await saveSession(context, email);
237
332
  logger.info("Session state saved 💾");
238
333
 
239
334
  return { browser, context, page };
@@ -3,7 +3,16 @@ const path = require("path");
3
3
  const crypto = require("crypto");
4
4
  const logger = require("../utils/logger");
5
5
 
6
- const SESSION_PATH = path.join(__dirname, "../../data/session.json");
6
+ let sessionDir = path.join(process.cwd(), "data", "linkedin");
7
+
8
+ /**
9
+ * Sets the directory where session files are stored.
10
+ * @param {string} dirPath - Absolute path to the session directory.
11
+ */
12
+ function setSessionDir(dirPath) {
13
+ sessionDir = dirPath;
14
+ }
15
+
7
16
  const ALGORITHM = "aes-256-cbc";
8
17
  // Use a fixed key if not provided in env (for development convenience, but ideally should be in env)
9
18
  // Ensure the key is 32 bytes.
@@ -35,21 +44,75 @@ function decrypt(text) {
35
44
  }
36
45
  }
37
46
 
38
- function sessionExists() {
39
- return fs.existsSync(SESSION_PATH);
47
+ /**
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}
52
+ */
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, "_");
59
+ }
60
+
61
+ function getSessionPath(username) {
62
+ const filename = `${sanitizeUsername(username)}.json`;
63
+ return path.join(sessionDir, filename);
40
64
  }
41
65
 
42
- function getSessionPath() {
43
- return SESSION_PATH;
66
+ function sessionExists(username) {
67
+ return fs.existsSync(getSessionPath(username));
44
68
  }
45
69
 
46
70
  const { SESSION_MAX_AGE } = require("../config");
47
71
 
72
+ 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);
80
+ if (!fs.existsSync(filePath)) return null;
81
+ return fs.readFileSync(filePath, "utf-8");
82
+ },
83
+
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);
91
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
92
+ fs.writeFileSync(filePath, data, "utf-8");
93
+ }
94
+ };
95
+
96
+ let currentStorage = defaultStorage;
97
+
48
98
  /**
49
- * Saves the browser context storage state to an encrypted file with a timestamp.
99
+ * Sets a custom session storage adapter.
100
+ * @param {Object} adapter - Object with read(username) and write(username, data) methods.
101
+ */
102
+ function setSessionStorage(adapter) {
103
+ if (adapter && typeof adapter.read === 'function' && typeof adapter.write === 'function') {
104
+ currentStorage = adapter;
105
+ } else {
106
+ logger.warn("Invalid storage adapter provided. Using default file storage.");
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Saves the browser context storage state to an encrypted file/storage with a timestamp.
50
112
  * @param {import('playwright').BrowserContext} context
113
+ * @param {string} [username] - The username to associate with this session
51
114
  */
52
- async function saveSession(context) {
115
+ async function saveSession(context, username) {
53
116
  try {
54
117
  const state = await context.storageState();
55
118
 
@@ -62,29 +125,39 @@ async function saveSession(context) {
62
125
  const jsonString = JSON.stringify(sessionData);
63
126
  const encryptedData = encrypt(jsonString);
64
127
 
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) 🔒");
128
+ await currentStorage.write(username || "default", encryptedData);
129
+
130
+ logger.info(`Session saved successfully for ${username || "default"} (Encrypted & Timestamped) 🔒`);
68
131
  } catch (error) {
69
132
  logger.error(`Failed to save session: ${error.message}`);
70
133
  }
71
134
  }
72
135
 
73
136
  /**
74
- * Loads the session from the encrypted file and creates a new context.
137
+ * Loads the session from storage and creates a new context.
75
138
  * Checks for session expiry.
76
139
  * @param {import('playwright').Browser} browser
77
140
  * @param {Object} options - Context options
141
+ * @param {string} [username] - The username to load session for
78
142
  * @returns {Promise<import('playwright').BrowserContext>}
79
143
  */
80
- async function loadSession(browser, options = {}) {
81
- if (!sessionExists()) {
82
- logger.info("No session file found.");
83
- return null;
84
- }
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.
85
152
 
86
153
  try {
87
- const fileContent = fs.readFileSync(SESSION_PATH, "utf-8");
154
+ const fileContent = await currentStorage.read(user);
155
+
156
+ if (!fileContent) {
157
+ logger.info(`No session found for ${user}.`);
158
+ return null;
159
+ }
160
+
88
161
  let sessionData;
89
162
  let state;
90
163
 
@@ -119,7 +192,7 @@ async function loadSession(browser, options = {}) {
119
192
  return null;
120
193
  }
121
194
  } else {
122
- logger.error("Failed to decrypt session file. It might be corrupt or key mismatch.");
195
+ logger.error("Failed to decrypt session data. It might be corrupt or key mismatch.");
123
196
  return null;
124
197
  }
125
198
  }
@@ -132,7 +205,7 @@ async function loadSession(browser, options = {}) {
132
205
  return null;
133
206
  }
134
207
  state = sessionData.state;
135
- logger.info(`Loaded active session (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
208
+ logger.info(`Loaded active session for ${username || "default"} (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
136
209
  }
137
210
 
138
211
  const context = await browser.newContext({
@@ -148,6 +221,8 @@ async function loadSession(browser, options = {}) {
148
221
  }
149
222
 
150
223
  module.exports = {
224
+ setSessionDir,
225
+ setSessionStorage,
151
226
  sessionExists,
152
227
  getSessionPath,
153
228
  saveSession,
@@ -1,4 +1,5 @@
1
1
  const { createLogger, format, transports } = require("winston");
2
+ const winston = require("winston");
2
3
  const path = require("path");
3
4
  const fs = require("fs");
4
5
 
@@ -8,23 +9,42 @@ if (!fs.existsSync(logDir)) {
8
9
  fs.mkdirSync(logDir, { recursive: true });
9
10
  }
10
11
 
11
- const logger = createLogger({
12
+ // Create default logger
13
+ const defaultLogger = winston.createLogger({
12
14
  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}`;
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}`;
17
19
  })
18
20
  ),
19
21
  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
- }),
22
+ new winston.transports.File({ filename: path.join(logDir, "error.log"), level: "error" }),
23
+ new winston.transports.File({ filename: path.join(logDir, "combined.log") }),
27
24
  ],
28
25
  });
29
26
 
30
- module.exports = logger;
27
+ let currentLogger = defaultLogger;
28
+
29
+ /**
30
+ * Sets a custom logger instance.
31
+ * @param {Object} customLogger - Logger object with info(), error(), warn(), debug() methods.
32
+ */
33
+ function setLogger(customLogger) {
34
+ if (customLogger && typeof customLogger.info === 'function') {
35
+ currentLogger = customLogger;
36
+ } else {
37
+ console.warn("Invalid logger provided. Using default.");
38
+ }
39
+ }
40
+
41
+ // Proxy object to forward calls to the current logger
42
+ const loggerProxy = {
43
+ info: (msg) => currentLogger.info(msg),
44
+ error: (msg) => currentLogger.error(msg),
45
+ warn: (msg) => currentLogger.warn(msg),
46
+ debug: (msg) => currentLogger.debug(msg),
47
+ setLogger // Export configuration method
48
+ };
49
+
50
+ module.exports = loggerProxy;