nexus-fca 3.0.1 → 3.0.2

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
@@ -147,6 +147,7 @@ const login = require('nexus-fca');
147
147
  |----------|----------|
148
148
  | Full API Reference | `DOCS.md` |
149
149
  | Feature Guides | `docs/*.md` |
150
+ | Configuration Reference | `docs/configuration-reference.md` |
150
151
  | Safety Details | `docs/account-safety.md` |
151
152
  | Examples | `examples/` |
152
153
 
package/docs/README.md CHANGED
@@ -22,6 +22,9 @@ This folder contains comprehensive documentation for all features of Nexus-FCA,
22
22
  ### 🔧 Core API Documentation
23
23
  Each `.md` file documents a single core API feature with usage examples, parameters, and safety notes.
24
24
 
25
+ Additional references:
26
+ - [Configuration Reference](./configuration-reference.md) — all env vars, programmatic options, and config file keys
27
+
25
28
  ### 🔗 Migration Guides
26
29
  - **[Migration-fca-unofficial.md](./Migration-fca-unofficial.md)** - Migrate from fca-unofficial
27
30
  - **[Migration-ws3-fca.md](./Migration-ws3-fca.md)** - Migrate from ws3-fca
@@ -0,0 +1,195 @@
1
+ # Configuration Reference (Nexus-FCA 3.0)
2
+
3
+ This document lists every supported configuration surface across the project: programmatic options, config file keys, and environment variables. Use this as a single source of truth when deploying locally or to PaaS.
4
+
5
+ ---
6
+ ## 1) Programmatic options (api.setOptions / login options)
7
+
8
+ These map to `globalOptions` and are available via `api.setOptions({ ... })` or by passing as the second argument to `login(credentials, options)`.
9
+
10
+ - Booleans
11
+ - `online` (default: true) – Advertise chat availability when connected
12
+ - `selfListen` – Receive your own sent messages
13
+ - `listenEvents` – Emit non-message events (nicknames, pins, joins, etc.)
14
+ - `listenTyping` – Emit typing events
15
+ - `updatePresence` – Enable presence updates processing
16
+ - `forceLogin` – Force legacy login behavior where applicable
17
+ - `autoMarkDelivery` – Auto mark delivery for inbound messages
18
+ - `autoMarkRead` – Auto mark read for inbound messages
19
+ - `autoReconnect` (default: true) – Reconnect MQTT after disconnect
20
+ - `emitReady` – Emit a synthetic `ready` event after connect
21
+
22
+ - Strings
23
+ - `logLevel`: 'silent' | 'error' | 'warn' | 'info' | 'verbose'
24
+ - `pageID`: string – Page scope for actions
25
+ - `userAgent`: string – Overrides UA for outbound requests
26
+ - `proxy`: string – HTTP(S) proxy URL
27
+ - `acceptLanguage`: string – Accept-Language header for MQTT/Web requests
28
+
29
+ - Numbers
30
+ - `logRecordSize` – npmlog ring buffer size
31
+
32
+ - Advanced (set via helper methods)
33
+ - `api.setBackoffOptions({ base, factor, max, jitter })` – Adaptive MQTT reconnect backoff tuning
34
+ - `api.setEditOptions({ maxPendingEdits, editTTLms, ackTimeoutMs, maxResendAttempts })` – Message edit safety controls
35
+ - `api.enableLazyPreflight(enable=true)` – When true, preflight is lighter/skipped if recent successful connect
36
+ - Group queue controls for large group threads:
37
+ - `api.enableGroupQueue(enable=true)`
38
+ - `api.setGroupQueueCapacity(n)`
39
+ - Internals: `groupQueueIdleMs` (default 30m) – idle purge window
40
+
41
+ Notes:
42
+ - Unknown keys passed to `setOptions` are warned and ignored.
43
+ - `api.listen` is an alias of `api.listenMqtt`.
44
+
45
+ ---
46
+ ## 2) Config file: fca-config.json
47
+
48
+ Auto-created in project root on first run and merged with defaults. Good for project-wide non-secret settings.
49
+
50
+ Example keys (merge-safe):
51
+
52
+ ```json
53
+ {
54
+ "autoUpdate": true,
55
+ "mqtt": { "enabled": true, "reconnectInterval": 3600 },
56
+ "logLevel": "warn",
57
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
58
+ "proxy": "http://user:pass@host:port"
59
+ }
60
+ ```
61
+
62
+ For secrets and per-deploy toggles, prefer environment variables.
63
+
64
+ ---
65
+ ## 3) Environment variables
66
+
67
+ Environment variables are the primary way to configure behavior in production. They override defaults and complement programmatic options.
68
+
69
+ ### 3.1 Storage & persistence
70
+ - `NEXUS_DATA_DIR` – Base directory for persistent files (fallback: `RENDER_DATA_DIR` or CWD)
71
+ - `NEXUS_APPSTATE_PATH` – Path to appstate.json
72
+ - `NEXUS_CREDENTIALS_PATH` – Path to credentials.json (integrated login)
73
+ - `NEXUS_BACKUP_PATH` – Directory for appstate backups
74
+ - `NEXUS_DEVICE_FILE` – Path to persistent-device.json
75
+ - `NEXUS_PERSISTENT_DEVICE` = true|false – Keep stable device fingerprint
76
+
77
+ ### 3.2 Networking & headers
78
+ - `NEXUS_PROXY` – HTTP(S) proxy URL (also respects `HTTPS_PROXY` / `HTTP_PROXY`)
79
+ - `NEXUS_ACCEPT_LANGUAGE` – Example: `en-US,en;q=0.9`
80
+ - `NEXUS_UA` – Override User-Agent
81
+ - `NEXUS_REGION` – Force MQTT region (e.g., `HIL`)
82
+
83
+ ### 3.3 Stability & preflight
84
+ - `NEXUS_DISABLE_PREFLIGHT` = true|false – Skip heavy preflight validation
85
+ - `NEXUS_ONLINE` = true|false – Override chat availability flag
86
+ - `NEXUS_VERBOSE_MQTT` = true|false – Extra MQTT diagnostics logging
87
+
88
+ ### 3.4 Single-session guard (prevents concurrent runs)
89
+ - `NEXUS_SESSION_LOCK_PATH` – Path for session lock file (default: `<DATA_DIR>/session.lock`)
90
+ - `NEXUS_SESSION_TTL_MS` – Lock stale timeout (default: 900000 = 15m)
91
+ - `NEXUS_FORCE_LOCK` = true|false – Force takeover if lock present
92
+
93
+ ### 3.5 Safety layer & allow/block lists
94
+ - `NEXUS_FCA_ULTRA_SAFE_MODE` = '1' – Maximum protection presets
95
+ - `NEXUS_FCA_SAFE_MODE` = '1' – Safe mode presets
96
+ - `NEXUS_FCA_ALLOW_LIST` – Comma-separated userIDs allowed
97
+ - `NEXUS_FCA_BLOCK_LIST` – Comma-separated userIDs blocked
98
+
99
+ ### 3.6 Example-only envs (for quick scripts)
100
+ Used in example snippets and README demos; not used by the core library itself:
101
+ - `FB_EMAIL` – Facebook username/email
102
+ - `FB_PASS` / `FB_PASSWORD` – Facebook password
103
+ - `FB_2FA_SECRET` – TOTP secret for 2FA (if used)
104
+ - `EMAIL` / `PASSWORD` – Alternative names used in example scripts
105
+ - `DEBUG` – e.g. `nexus-fca:*` to enable debug output in certain examples
106
+
107
+ ---
108
+ ## 4) Integrated Nexus Login System options
109
+
110
+ Used internally for credential-based login; also exported for direct use via `IntegratedNexusLoginSystem`. Options can be provided in code or via the env overrides above.
111
+
112
+ - Paths & persistence
113
+ - `appstatePath` (env: `NEXUS_APPSTATE_PATH`)
114
+ - `credentialsPath` (env: `NEXUS_CREDENTIALS_PATH`)
115
+ - `backupPath` (env: `NEXUS_BACKUP_PATH`)
116
+ - `persistentDevice` (env: `NEXUS_PERSISTENT_DEVICE`)
117
+ - `persistentDeviceFile` (env: `NEXUS_DEVICE_FILE`)
118
+
119
+ - Behavior
120
+ - `autoLogin` (default: true)
121
+ - `autoSave` (default: true)
122
+ - `safeMode` (default: true)
123
+ - `maxRetries` (default: 3)
124
+ - `retryDelay` (default: 5000 ms)
125
+
126
+ ---
127
+ ## 5) MQTT & connection behavior
128
+
129
+ Controlled via programmatic options and envs:
130
+
131
+ - `autoReconnect` (default true) – Reconnect on close/error
132
+ - Backoff: `api.setBackoffOptions({ base, factor, max, jitter })`
133
+ - Keepalives & diagnostics: `NEXUS_VERBOSE_MQTT` for extra logs
134
+ - Headers: `acceptLanguage`, `userAgent`, `proxy`
135
+ - Region override: `NEXUS_REGION`
136
+
137
+ ---
138
+ ## 6) Delivery & read behavior
139
+
140
+ - `autoMarkDelivery` – Calls `markAsDelivered` for inbound messages
141
+ - `autoMarkRead` – Optional follow-up read marking
142
+ - Adaptive suppression & retries are built-in; metrics accessible via `api.getHealthMetrics()`
143
+
144
+ ---
145
+ ## 7) Health and memory metrics
146
+
147
+ - `api.getHealthMetrics()` – ACKs, reconnect counts, delivery stats, last connect timestamps, etc.
148
+ - `api.getMemoryMetrics()` – Pending edits, outbound queue depth, group queue prunes, memory guard actions
149
+
150
+ ---
151
+ ## 8) Logging
152
+
153
+ - `logLevel`: 'silent' | 'error' | 'warn' | 'info' | 'verbose'
154
+ - `logRecordSize`: ring buffer size
155
+ - Temporarily increase MQTT verbosity with `NEXUS_VERBOSE_MQTT=true` when debugging
156
+
157
+ ---
158
+ ## 9) Quick matrix (where to set)
159
+
160
+ | Setting | setOptions | login options | fca-config.json | ENV |
161
+ |--------------------------------|------------|---------------|-----------------|-----|
162
+ | online | ✅ | ✅ | | ✅ (`NEXUS_ONLINE`) |
163
+ | selfListen / listenEvents | ✅ | ✅ | | |
164
+ | updatePresence | ✅ | ✅ | | |
165
+ | autoMarkDelivery / autoMarkRead| ✅ | ✅ | | |
166
+ | autoReconnect | ✅ | ✅ | | |
167
+ | userAgent | ✅ | ✅ | ✅ | ✅ (`NEXUS_UA`) |
168
+ | proxy | ✅ | ✅ | ✅ | ✅ (`NEXUS_PROXY`/`HTTPS_PROXY`/`HTTP_PROXY`) |
169
+ | acceptLanguage | ✅ | ✅ | | ✅ (`NEXUS_ACCEPT_LANGUAGE`) |
170
+ | disablePreflight | via `enableLazyPreflight(false)` | ✅ | | ✅ (`NEXUS_DISABLE_PREFLIGHT`) |
171
+ | pageID | ✅ | ✅ | | |
172
+ | backoff/edit settings | helper fns | | | |
173
+ | data/appstate/backup/device | | via Integrated Login | | ✅ (`NEXUS_*` paths) |
174
+ | session guard (lock/ttl/force) | | | | ✅ |
175
+ | safety modes / lists | | | | ✅ |
176
+
177
+ ---
178
+ ## 10) Recommended PaaS baseline
179
+
180
+ Set at minimum:
181
+
182
+ ```
183
+ NEXUS_DATA_DIR=/var/data/nexus-fca
184
+ NEXUS_APPSTATE_PATH=/var/data/nexus-fca/appstate.json
185
+ NEXUS_DEVICE_FILE=/var/data/nexus-fca/persistent-device.json
186
+ NEXUS_PERSISTENT_DEVICE=true
187
+ NEXUS_ACCEPT_LANGUAGE=en-US,en;q=0.9
188
+ # Optional hardening
189
+ NEXUS_DISABLE_PREFLIGHT=true
190
+ # Optional proxy/region
191
+ # NEXUS_PROXY=http://user:pass@host:port
192
+ # NEXUS_REGION=HIL
193
+ ```
194
+
195
+ See also: `docs/deployment-config.md` for end-to-end deployment steps.
@@ -0,0 +1,178 @@
1
+ # Deployment & Configuration Guide (Nexus-FCA 3.0)
2
+
3
+ This guide shows how to use Nexus-FCA as an npm package, configure it via file and environment variables, deploy safely on platforms like Render, and avoid the “you couldn't create multiple sessions” warning.
4
+
5
+ ---
6
+ ## 1) Using as an npm package
7
+
8
+ Install:
9
+
10
+ ```bash
11
+ npm install nexus-fca
12
+ ```
13
+
14
+ Basic appstate usage:
15
+
16
+ ```js
17
+ const login = require('nexus-fca');
18
+
19
+ (async () => {
20
+ const api = await login({ appState: require('./appstate.json') });
21
+ api.listen((err, evt) => {
22
+ if (err) return console.error(err);
23
+ if (evt.body) api.sendMessage('Echo: ' + evt.body, evt.threadID);
24
+ });
25
+ })();
26
+ ```
27
+
28
+ ---
29
+ ## 2) Configuration options
30
+
31
+ You can control behavior via:
32
+ - Programmatic options: `api.setOptions({ ... })`
33
+ - Config file: `fca-config.json` (auto-created on first run)
34
+ - Environment variables (recommended for hosting)
35
+
36
+ ### 2.1 Programmatic options (common)
37
+ ```js
38
+ api.setOptions({
39
+ autoReconnect: true,
40
+ updatePresence: false,
41
+ selfListen: false,
42
+ logLevel: 'warn', // npmlog level: silly|verbose|info|warn|error
43
+ userAgent: '...Chrome/...',
44
+ proxy: 'http://user:pass@host:port',
45
+ });
46
+ ```
47
+
48
+ ### 2.2 Config file: `fca-config.json`
49
+ A default file is stored at project root and merged with internal defaults. You can keep project-wide settings here:
50
+
51
+ ```json
52
+ {
53
+ "autoUpdate": true,
54
+ "mqtt": { "enabled": true, "reconnectInterval": 3600 },
55
+ "logLevel": "warn",
56
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
57
+ "proxy": "http://user:pass@host:port"
58
+ }
59
+ ```
60
+
61
+ Note: For secrets and deployment toggles, prefer environment variables below.
62
+
63
+ ### 2.3 Environment variables (hosting-friendly)
64
+ These are read at runtime and override defaults:
65
+
66
+ - Storage & persistence
67
+ - `NEXUS_DATA_DIR` → base directory for persistent files
68
+ - `NEXUS_APPSTATE_PATH` → path to `appstate.json`
69
+ - `NEXUS_DEVICE_FILE` → path to `persistent-device.json`
70
+ - `NEXUS_BACKUP_PATH` → directory for appstate backups
71
+ - `NEXUS_PERSISTENT_DEVICE` → `true|false` (keep same device fingerprint)
72
+
73
+ - Networking & headers
74
+ - `NEXUS_PROXY` (or `HTTPS_PROXY` / `HTTP_PROXY`) → Proxy URL
75
+ - `NEXUS_ACCEPT_LANGUAGE` → e.g. `en-US,en;q=0.9`
76
+ - `NEXUS_UA` → override User-Agent
77
+ - `NEXUS_REGION` → force MQTT region (e.g., `HIL`)
78
+
79
+ - Stability & preflight
80
+ - `NEXUS_DISABLE_PREFLIGHT` → `true|false` (skip heavy session preflight)
81
+ - `NEXUS_ONLINE` → `true|false`
82
+ - `NEXUS_VERBOSE_MQTT` → `true|false` (extra MQTT logs only when needed)
83
+
84
+ Example (Render):
85
+ ```
86
+ NEXUS_DATA_DIR=/var/data/nexus-fca
87
+ NEXUS_APPSTATE_PATH=/var/data/nexus-fca/appstate.json
88
+ NEXUS_DEVICE_FILE=/var/data/nexus-fca/persistent-device.json
89
+ NEXUS_BACKUP_PATH=/var/data/nexus-fca/backups
90
+ NEXUS_PERSISTENT_DEVICE=true
91
+
92
+ NEXUS_ACCEPT_LANGUAGE=en-US,en;q=0.9
93
+ NEXUS_UA=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
94
+ NEXUS_DISABLE_PREFLIGHT=true
95
+ NEXUS_ONLINE=false
96
+ # Optional region & proxy
97
+ # NEXUS_REGION=HIL
98
+ # NEXUS_PROXY=http://user:pass@host:port
99
+ ```
100
+
101
+ ---
102
+ ## 3) Deploying on Render (or other PaaS)
103
+
104
+ 1) Persistent disk
105
+ - Mount a disk and point `NEXUS_DATA_DIR` to it.
106
+ - Ensure `appstate.json` and `persistent-device.json` live on this disk.
107
+
108
+ 2) Single instance per account
109
+ - Run only one service instance for a given account.
110
+ - Disable autoscaling/replicas for this worker.
111
+
112
+ 3) Environment hardening
113
+ - Set `NEXUS_PERSISTENT_DEVICE=true` to anchor device fingerprint.
114
+ - Set `NEXUS_DISABLE_PREFLIGHT=true` to reduce heavy checks.
115
+ - Optionally use a reputable residential/mobile proxy via `NEXUS_PROXY`.
116
+
117
+ 4) Keep your appstate fresh
118
+ - Generate appstate locally on a trusted network, then deploy it.
119
+ - Avoid frequent credential logins from the server itself.
120
+
121
+ ---
122
+ ## 4) "You couldn't create multiple sessions" – what it means and fixes
123
+
124
+ This warning typically appears when Facebook detects concurrent or rapidly rotating logins for the same account, often from:
125
+ - Multiple bots/instances using the same account at the same time
126
+ - Different IPs or device fingerprints in quick succession
127
+ - Frequent logouts/re-logins (or unstable appstate)
128
+
129
+ How to avoid:
130
+ - Run exactly one bot instance per account.
131
+ - Reuse the same `appstate.json` and `persistent-device.json` (set `NEXUS_PERSISTENT_DEVICE=true`).
132
+ - Avoid logging in with username/password from multiple places—generate appstate once and reuse it.
133
+ - Use a stable IP. If your PaaS IPs rotate or look like a datacenter bot, use a reputable residential/mobile proxy.
134
+ - Keep preflight light on PaaS (`NEXUS_DISABLE_PREFLIGHT=true`).
135
+
136
+ Recovery steps when you see the warning:
137
+ - Stop all other running instances for the same account.
138
+ - Log in manually in a browser, pass any checkpoint, verify recent logins.
139
+ - Regenerate a fresh appstate (locally), deploy, and keep it persistent.
140
+
141
+ ---
142
+ ## 5) Minimal config-first example
143
+
144
+ `fca-config.json` (project root):
145
+ ```json
146
+ {
147
+ "logLevel": "warn",
148
+ "mqtt": { "enabled": true, "reconnectInterval": 3600 },
149
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/121.0.0.0 Safari/537.36"
150
+ }
151
+ ```
152
+
153
+ Bot code:
154
+ ```js
155
+ const login = require('nexus-fca');
156
+ (async () => {
157
+ const api = await login({ appState: require(process.env.NEXUS_APPSTATE_PATH || './appstate.json') });
158
+ // Optional fine-tuning
159
+ api.setOptions({
160
+ autoReconnect: true,
161
+ updatePresence: false,
162
+ proxy: process.env.NEXUS_PROXY,
163
+ });
164
+ api.listen((err, evt) => {
165
+ if (err) return console.error(err);
166
+ if (evt.body) api.sendMessage('Echo: ' + evt.body, evt.threadID);
167
+ });
168
+ })();
169
+ ```
170
+
171
+ ---
172
+ ## 6) Troubleshooting quick tips
173
+ - Rapid cookie expiry: ensure persistence paths are on mounted disk; set `NEXUS_PERSISTENT_DEVICE=true`.
174
+ - Frequent reconnect warnings: let adaptive backoff work; keep `autoReconnect=true`.
175
+ - No replies sent: check proxy/network egress; use delivery metrics via `api.getHealthMetrics()`.
176
+ - Noisy logs: avoid `NEXUS_VERBOSE_MQTT` unless debugging; prefer `logLevel=warn`.
177
+
178
+ For full API details, see `DOCS.md` and other files in `docs/`.
package/index.js CHANGED
@@ -51,6 +51,7 @@ const { User } = require('./lib/message/User');
51
51
 
52
52
  // Advanced Safety Module - Minimizes ban/lock/checkpoint rates
53
53
  const FacebookSafety = require('./lib/safety/FacebookSafety');
54
+ const { SingleSessionGuard } = require('./lib/safety/SingleSessionGuard');
54
55
 
55
56
  // Core compatibility imports
56
57
  const MqttManager = require('./lib/mqtt/MqttManager');
@@ -195,6 +196,10 @@ function buildAPI(globalOptions, html, jar) {
195
196
  } catch (e) {
196
197
  log.warning("login", "Not MQTT endpoint");
197
198
  }
199
+ // Allow environment override for region (useful on PaaS where HTML may omit region or mismatch)
200
+ if (process.env.NEXUS_REGION) {
201
+ try { region = process.env.NEXUS_REGION.toUpperCase(); } catch(_) {}
202
+ }
198
203
  const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
199
204
  if (tokenMatch) {
200
205
  fb_dtsg = tokenMatch[1];
@@ -557,20 +562,24 @@ const crypto = require('crypto');
557
562
 
558
563
  class IntegratedNexusLoginSystem {
559
564
  constructor(options = {}) {
560
- this.options = {
561
- appstatePath: options.appstatePath || path.join(process.cwd(), 'appstate.json'),
562
- credentialsPath: options.credentialsPath || path.join(process.cwd(), 'credentials.json'),
563
- backupPath: options.backupPath || path.join(process.cwd(), 'backups'),
564
- autoLogin: options.autoLogin !== false,
565
- autoSave: options.autoSave !== false,
566
- safeMode: options.safeMode !== false,
567
- maxRetries: options.maxRetries || 3,
568
- retryDelay: options.retryDelay || 5000,
569
- // New: persistentDevice disables random device rotation
570
- persistentDevice: options.persistentDevice !== false,
571
- persistentDeviceFile: options.persistentDeviceFile || path.join(process.cwd(), 'persistent-device.json'),
572
- ...options
573
- };
565
+ const dataDir = process.env.NEXUS_DATA_DIR || process.env.RENDER_DATA_DIR || process.cwd();
566
+ const envPersistent = (v) => (v === '0' || v === 'false') ? false : (v === '1' || v === 'true') ? true : undefined;
567
+ const envPD = envPersistent(process.env.NEXUS_PERSISTENT_DEVICE);
568
+
569
+ this.options = {
570
+ appstatePath: options.appstatePath || process.env.NEXUS_APPSTATE_PATH || path.join(dataDir, 'appstate.json'),
571
+ credentialsPath: options.credentialsPath || process.env.NEXUS_CREDENTIALS_PATH || path.join(dataDir, 'credentials.json'),
572
+ backupPath: options.backupPath || process.env.NEXUS_BACKUP_PATH || path.join(dataDir, 'backups'),
573
+ autoLogin: options.autoLogin !== false,
574
+ autoSave: options.autoSave !== false,
575
+ safeMode: options.safeMode !== false,
576
+ maxRetries: options.maxRetries || 3,
577
+ retryDelay: options.retryDelay || 5000,
578
+ // New: persistentDevice disables random device rotation
579
+ persistentDevice: typeof envPD === 'boolean' ? envPD : (options.persistentDevice !== false),
580
+ persistentDeviceFile: options.persistentDeviceFile || process.env.NEXUS_DEVICE_FILE || path.join(dataDir, 'persistent-device.json'),
581
+ ...options
582
+ };
574
583
 
575
584
  this.deviceCache = new Map();
576
585
  this.loginAttempts = 0;
@@ -578,10 +587,30 @@ class IntegratedNexusLoginSystem {
578
587
  // New: load previously persisted device if any
579
588
  this.fixedDeviceProfile = this.loadPersistentDevice();
580
589
 
581
- this.ensureDirectories();
590
+ this.ensureDirectories();
582
591
  this.logger('Login system ready', '🚀');
583
592
  }
584
593
 
594
+ ensureDirectories() {
595
+ try {
596
+ // backups dir
597
+ if (this.options.backupPath && !fs.existsSync(this.options.backupPath)) {
598
+ fs.mkdirSync(this.options.backupPath, { recursive: true });
599
+ }
600
+ // parent dir for appstate
601
+ const appstateDir = path.dirname(this.options.appstatePath);
602
+ if (!fs.existsSync(appstateDir)) fs.mkdirSync(appstateDir, { recursive: true });
603
+ // parent dir for credentials
604
+ const credDir = path.dirname(this.options.credentialsPath);
605
+ if (!fs.existsSync(credDir)) fs.mkdirSync(credDir, { recursive: true });
606
+ // parent dir for persistent device
607
+ const pdDir = path.dirname(this.options.persistentDeviceFile);
608
+ if (!fs.existsSync(pdDir)) fs.mkdirSync(pdDir, { recursive: true });
609
+ } catch (e) {
610
+ this.logger('Failed to ensure directories: ' + e.message, '⚠️');
611
+ }
612
+ }
613
+
585
614
  loadPersistentDevice() {
586
615
  try {
587
616
  if (!this.options.persistentDevice) return null;
@@ -1102,7 +1131,7 @@ async function integratedNexusLogin(credentials = null, options = {}) {
1102
1131
 
1103
1132
  try {
1104
1133
  // Prepare global options for bot system
1105
- const globalOptions = {
1134
+ const globalOptions = {
1106
1135
  selfListen: false,
1107
1136
  selfListenEvent: false,
1108
1137
  listenEvents: false,
@@ -1113,9 +1142,12 @@ async function integratedNexusLogin(credentials = null, options = {}) {
1113
1142
  autoMarkRead: false,
1114
1143
  autoReconnect: true,
1115
1144
  logRecordSize: defaultLogRecordSize,
1116
- online: true,
1145
+ online: (process.env.NEXUS_ONLINE ? (process.env.NEXUS_ONLINE === '1' || process.env.NEXUS_ONLINE === 'true') : true),
1117
1146
  emitReady: false,
1118
- userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
1147
+ userAgent: process.env.NEXUS_UA || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
1148
+ proxy: process.env.NEXUS_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY,
1149
+ acceptLanguage: process.env.NEXUS_ACCEPT_LANGUAGE || 'en-US,en;q=0.9',
1150
+ disablePreflight: process.env.NEXUS_DISABLE_PREFLIGHT === '1' || process.env.NEXUS_DISABLE_PREFLIGHT === 'true',
1119
1151
  ...options
1120
1152
  };
1121
1153
 
@@ -1229,7 +1261,18 @@ async function login(loginData, options = {}, callback) {
1229
1261
  mainLogger.info('✅ Session generated successfully');
1230
1262
  mainLogger.info('🔄 Starting bot with generated session (old system)');
1231
1263
 
1232
- // STEP 2: ALWAYS use OLD system for actual login/session/bot
1264
+ // STEP 2: Single session guard before starting bot
1265
+ try {
1266
+ const ssg = new SingleSessionGuard({ dataDir: process.env.NEXUS_DATA_DIR });
1267
+ ssg.acquire();
1268
+ // keep guard reference to release on exit
1269
+ global.__NEXUS_SSG__ = ssg;
1270
+ } catch (e) {
1271
+ mainLogger.error('⚠️ Single session guard blocked start', e.message);
1272
+ if (callback) callback(e);
1273
+ return usePromise ? promise : undefined;
1274
+ }
1275
+ // STEP 3: ALWAYS use OLD system for actual login/session/bot
1233
1276
  const globalOptions = {
1234
1277
  selfListen: false,
1235
1278
  selfListenEvent: false,
@@ -1271,7 +1314,16 @@ async function login(loginData, options = {}, callback) {
1271
1314
  return usePromise ? promise : undefined;
1272
1315
  }
1273
1316
 
1274
- // Direct session authentication using appstate
1317
+ // Direct session authentication using appstate (with single session guard)
1318
+ try {
1319
+ const ssg = new SingleSessionGuard({ dataDir: process.env.NEXUS_DATA_DIR });
1320
+ ssg.acquire();
1321
+ global.__NEXUS_SSG__ = ssg;
1322
+ } catch (e) {
1323
+ mainLogger.error('⚠️ Single session guard blocked start', e.message);
1324
+ if (callback) callback(e);
1325
+ return usePromise ? promise : undefined;
1326
+ }
1275
1327
  mainLogger.info('🔄 Starting session authentication');
1276
1328
 
1277
1329
  const globalOptions = {
@@ -1285,9 +1337,12 @@ async function login(loginData, options = {}, callback) {
1285
1337
  autoMarkRead: false,
1286
1338
  autoReconnect: true,
1287
1339
  logRecordSize: defaultLogRecordSize,
1288
- online: true,
1340
+ online: (process.env.NEXUS_ONLINE ? (process.env.NEXUS_ONLINE === '1' || process.env.NEXUS_ONLINE === 'true') : true),
1289
1341
  emitReady: false,
1290
- userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
1342
+ userAgent: process.env.NEXUS_UA || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
1343
+ proxy: process.env.NEXUS_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY,
1344
+ acceptLanguage: process.env.NEXUS_ACCEPT_LANGUAGE || 'en-US,en;q=0.9',
1345
+ disablePreflight: process.env.NEXUS_DISABLE_PREFLIGHT === '1' || process.env.NEXUS_DISABLE_PREFLIGHT === 'true',
1291
1346
  ...options
1292
1347
  };
1293
1348
 
package/lib/logger.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const chalk = require('chalk');
2
2
  const gradient = require('gradient-string');
3
3
 
4
- // Use a professional gradient for info logs
5
4
  const infoGradient = gradient(['#00c6ff', '#0072ff']); // blue-cyan gradient
6
5
  const warnColor = chalk.yellow.bold;
7
6
  const errorColor = chalk.red.bold;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ function nowIso(){ return new Date().toISOString(); }
7
+
8
+ function readJsonSafe(file){
9
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch(_) { return null; }
10
+ }
11
+
12
+ function writeJsonSafe(file, obj){
13
+ try { fs.writeFileSync(file, JSON.stringify(obj, null, 2)); } catch(_) {}
14
+ }
15
+
16
+ class SingleSessionGuard {
17
+ constructor(options={}){
18
+ const baseDir = options.dataDir || process.env.NEXUS_DATA_DIR || process.env.RENDER_DATA_DIR || process.cwd();
19
+ this.lockPath = options.lockPath || process.env.NEXUS_SESSION_LOCK_PATH || path.join(baseDir, 'session.lock');
20
+ this.ttlMs = options.ttlMs || parseInt(process.env.NEXUS_SESSION_TTL_MS || '900000', 10); // default 15m
21
+ this.force = options.force || (process.env.NEXUS_FORCE_LOCK === '1' || process.env.NEXUS_FORCE_LOCK === 'true');
22
+ this.heartbeatMs = options.heartbeatMs || 60000; // 1m
23
+ this.sessionId = (options.sessionId) || `${process.pid}-${Math.random().toString(36).slice(2,10)}`;
24
+ this._hbTimer = null;
25
+ this._acquired = false;
26
+ }
27
+
28
+ isStale(lock){
29
+ if(!lock || !lock.updatedAt) return true;
30
+ const age = Date.now() - new Date(lock.updatedAt).getTime();
31
+ return age > this.ttlMs;
32
+ }
33
+
34
+ acquire(){
35
+ // Ensure parent dir exists
36
+ try { fs.mkdirSync(path.dirname(this.lockPath), { recursive: true }); } catch(_) {}
37
+ if (fs.existsSync(this.lockPath)){
38
+ const existing = readJsonSafe(this.lockPath);
39
+ if (this.isStale(existing) || this.force){
40
+ // Take over
41
+ try { fs.unlinkSync(this.lockPath); } catch(_) {}
42
+ } else {
43
+ const msg = `Another Nexus-FCA session appears active (lock: ${this.lockPath}). Set NEXUS_FORCE_LOCK=true to override or wait until stale.`;
44
+ const error = new Error(msg);
45
+ error.code = 'NEXUS_MULTIPLE_SESSIONS';
46
+ throw error;
47
+ }
48
+ }
49
+ const payload = {
50
+ pid: process.pid,
51
+ host: os.hostname(),
52
+ sessionId: this.sessionId,
53
+ startedAt: nowIso(),
54
+ updatedAt: nowIso()
55
+ };
56
+ writeJsonSafe(this.lockPath, payload);
57
+ this._acquired = true;
58
+ this._startHeartbeat();
59
+ this._installExitHooks();
60
+ return true;
61
+ }
62
+
63
+ _startHeartbeat(){
64
+ this._clearHeartbeat();
65
+ this._hbTimer = setInterval(()=>{
66
+ try {
67
+ if(!this._acquired) return;
68
+ const cur = readJsonSafe(this.lockPath) || {};
69
+ cur.updatedAt = nowIso();
70
+ cur.pid = process.pid;
71
+ cur.sessionId = this.sessionId;
72
+ cur.host = cur.host || os.hostname();
73
+ writeJsonSafe(this.lockPath, cur);
74
+ } catch(_){}
75
+ }, this.heartbeatMs).unref();
76
+ }
77
+
78
+ _clearHeartbeat(){
79
+ if(this._hbTimer){ try { clearInterval(this._hbTimer); } catch(_){} this._hbTimer = null; }
80
+ }
81
+
82
+ _installExitHooks(){
83
+ if (this._exitHooksInstalled) return;
84
+ this._exitHooksInstalled = true;
85
+ const release = () => { try { this.release(); } catch(_){} };
86
+ process.on('SIGINT', release);
87
+ process.on('SIGTERM', release);
88
+ process.on('exit', release);
89
+ }
90
+
91
+ release(){
92
+ this._clearHeartbeat();
93
+ if(!this._acquired) return;
94
+ this._acquired = false;
95
+ try { if(fs.existsSync(this.lockPath)) fs.unlinkSync(this.lockPath); } catch(_){}
96
+ }
97
+ }
98
+
99
+ module.exports = { SingleSessionGuard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Nexus-FCA 3.0 – stable, low-risk Facebook Messenger automation API with integrated secure login (ID / Password / 2FA), adaptive MQTT core, safety orchestration, metrics, and TypeScript support.",
5
5
  "main": "index.js",
6
6
  "repository": {
package/src/listenMqtt.js CHANGED
@@ -68,6 +68,7 @@ function resetBackoff(state){
68
68
  // Build lazy preflight gating
69
69
  function shouldRunPreflight(ctx){
70
70
  if(ctx.globalOptions.disablePreflight) return false;
71
+ if(process.env.NEXUS_DISABLE_PREFLIGHT === '1' || process.env.NEXUS_DISABLE_PREFLIGHT === 'true') return false;
71
72
  // If we connected successfully within last 10 minutes, skip heavy preflight to reduce surface.
72
73
  const now = Date.now();
73
74
  const metrics = ctx.health;
@@ -174,8 +175,9 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
174
175
  const backoff = getBackoffState(ctx);
175
176
  if(!ctx._mqttDiag) ctx._mqttDiag = { attempts:0, events:[] };
176
177
  ctx._mqttDiag.attempts++;
177
- // Suppress previously noisy test info log (kept only if verbose flag enabled)
178
- if (ctx.globalOptions && ctx.globalOptions.verboseMqtt) {
178
+ // Suppress previously noisy test info log (visible only if verbose flag enabled or env toggled)
179
+ const verboseMqtt = (ctx.globalOptions && ctx.globalOptions.verboseMqtt) || process.env.NEXUS_VERBOSE_MQTT === '1' || process.env.NEXUS_VERBOSE_MQTT === 'true';
180
+ if (verboseMqtt) {
179
181
  log.info('listenMqtt', `Starting Nexus MQTT bridge (attempt=${ctx._mqttDiag.attempts}, backoff=${backoff.current||0}ms)`);
180
182
  }
181
183
  const runPreflight = shouldRunPreflight(ctx);
@@ -251,7 +253,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
251
253
  Upgrade: "websocket",
252
254
  "Sec-WebSocket-Version": "13",
253
255
  "Accept-Encoding": "gzip, deflate, br",
254
- "Accept-Language": "vi,en;q=0.9",
256
+ "Accept-Language": (ctx.globalOptions && ctx.globalOptions.acceptLanguage) || process.env.NEXUS_ACCEPT_LANGUAGE || "en-US,en;q=0.9",
255
257
  "Sec-WebSocket-Extensions":
256
258
  "permessage-deflate; client_max_window_bits",
257
259
  },
@@ -264,6 +266,11 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
264
266
  reconnectPeriod: 1000,
265
267
  connectTimeout: 5000,
266
268
  };
269
+ // Proxy support via option or environment
270
+ if (ctx.globalOptions.proxy === undefined) {
271
+ const envProxy = process.env.NEXUS_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
272
+ if (envProxy) ctx.globalOptions.proxy = envProxy;
273
+ }
267
274
  if (ctx.globalOptions.proxy !== undefined) {
268
275
  try {
269
276
  const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
@@ -279,7 +286,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
279
286
  () => buildStream(options, rawWs, buildProxy()),
280
287
  options
281
288
  );
282
- if (ctx.globalOptions && ctx.globalOptions.verboseMqtt) {
289
+ if (verboseMqtt) {
283
290
  log.info('listenMqtt', `MQTT bridge dialing ${host}`);
284
291
  }
285
292
  const mqttClient = ctx.mqttClient;
@@ -331,7 +338,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
331
338
  mqttClient.on("connect", function () {
332
339
  resetBackoff(backoff);
333
340
  ctx.health.onConnect();
334
- if (ctx.globalOptions && ctx.globalOptions.verboseMqtt) {
341
+ if (verboseMqtt) {
335
342
  log.info('listenMqtt', `Nexus MQTT bridge established in ${(Date.now()-attemptStartTs)}ms (attempt=${ctx._mqttDiag.attempts}).`);
336
343
  }
337
344
  if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
package/lib/login.js DELETED
File without changes