nexus-fca 3.2.4 → 3.3.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/index.js CHANGED
@@ -85,6 +85,7 @@ const { CookieManager } = require('./lib/safety/CookieManager');
85
85
  const EmailPasswordLogin = require('./lib/auth/EmailPasswordLogin');
86
86
  const ProxyManager = require('./lib/network/ProxyManager');
87
87
  const UserAgentManager = require('./lib/network/UserAgentManager');
88
+ const HealthServer = require('./lib/network/HealthServer');
88
89
 
89
90
  // Core compatibility imports
90
91
  const MqttManager = require('./lib/mqtt/MqttManager');
@@ -131,6 +132,12 @@ if (!fs.existsSync(configPath)) {
131
132
  global.fca = {
132
133
  config: config
133
134
  };
135
+
136
+ // Start Health Server if on cloud platform or enabled
137
+ if (process.env.PORT || process.env.NEXUS_ENABLE_HEALTH_SERVER === '1') {
138
+ const healthServer = new HealthServer();
139
+ healthServer.start();
140
+ }
134
141
  const Boolean_Option = [
135
142
  "online",
136
143
  "selfListen",
@@ -712,6 +719,8 @@ class IntegratedNexusLoginSystem {
712
719
  });
713
720
 
714
721
  fs.writeFileSync(this.options.appstatePath, JSON.stringify(fixedAppstate, null, 2));
722
+ metadata.appStatePath = this.options.appstatePath;
723
+ this.logger(`Session saved to: ${path.basename(this.options.appstatePath)}`, '💾');
715
724
 
716
725
  // Create backup
717
726
  const backupName = `appstate_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
@@ -859,7 +868,8 @@ class IntegratedNexusLoginSystem {
859
868
  family_device_id: androidDevice.familyDeviceId
860
869
  },
861
870
  generated_at: new Date().toISOString(),
862
- persistent_device: !!this.options.persistentDevice
871
+ persistent_device: !!this.options.persistentDevice,
872
+ appStatePath: this.options.appstatePath
863
873
  };
864
874
 
865
875
  this.saveAppstate(appstate, result);
@@ -948,6 +958,8 @@ class IntegratedNexusLoginSystem {
948
958
  });
949
959
  }
950
960
 
961
+ const appStatePath = credentials.appStatePath || credentials.appstatePath;
962
+
951
963
  const result = {
952
964
  success: true,
953
965
  appstate: appstate,
@@ -957,6 +969,7 @@ class IntegratedNexusLoginSystem {
957
969
  user_agent: androidDevice.userAgent
958
970
  },
959
971
  method: '2FA',
972
+ appStatePath: appStatePath,
960
973
  generated_at: new Date().toISOString()
961
974
  };
962
975
 
@@ -1131,6 +1144,7 @@ async function integratedNexusLogin(credentials = null, options = {}) {
1131
1144
  proxy: process.env.NEXUS_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY,
1132
1145
  acceptLanguage: process.env.NEXUS_ACCEPT_LANGUAGE || 'en-US,en;q=0.9',
1133
1146
  disablePreflight: process.env.NEXUS_DISABLE_PREFLIGHT === '1' || process.env.NEXUS_DISABLE_PREFLIGHT === 'true',
1147
+ appStatePath: result.appStatePath,
1134
1148
  ...options
1135
1149
  };
1136
1150
 
@@ -1149,6 +1163,12 @@ async function integratedNexusLogin(credentials = null, options = {}) {
1149
1163
  botError: err.message
1150
1164
  });
1151
1165
  } else {
1166
+ // VERCEL STABILITY WARNING
1167
+ if (process.env.VERCEL || process.env.NOW_REGION) {
1168
+ Logger.warn('PLATFORM', 'Vercel/Serverless detected. listenMqtt is NOT supported here.');
1169
+ Logger.warn('PLATFORM', 'Bot will work for outbound actions (sending) only.');
1170
+ }
1171
+
1152
1172
  Logger.success('BOT-INIT', 'Bot initialized successfully');
1153
1173
  Logger.success('READY', '🚀 Nexus-FCA is now ready for use');
1154
1174
  Logger.info('STATUS', `Bot online | User ID: ${api.getCurrentUserID()}`);
@@ -1232,7 +1252,8 @@ async function login(loginData, options = {}, callback) {
1232
1252
  password: loginData.password,
1233
1253
  twofactor: loginData.twofactor || loginData.otp || undefined,
1234
1254
  _2fa: loginData._2fa || undefined,
1235
- appstate: loginData.appState || loginData.appstate || undefined
1255
+ appstate: loginData.appState || loginData.appstate || undefined,
1256
+ appStatePath: loginData.appStatePath || loginData.appstatePath || undefined
1236
1257
  }, { autoStartBot: false }); // ONLY generate cookies, NO bot startup
1237
1258
 
1238
1259
  if (!result.success || !result.appstate) {
@@ -1277,6 +1298,7 @@ async function login(loginData, options = {}, callback) {
1277
1298
  online: true,
1278
1299
  emitReady: false,
1279
1300
  userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
1301
+ appStatePath: result.appStatePath,
1280
1302
  ...options
1281
1303
  };
1282
1304
 
@@ -169,6 +169,7 @@ class ApiFactory {
169
169
  wsReqNumber: 0,
170
170
  wsTaskNumber: 0,
171
171
  globalSafety: this.globalSafety,
172
+ appStatePath: globalOptions.appStatePath || null,
172
173
  pendingEdits: new Map()
173
174
  };
174
175
  }
@@ -302,15 +303,6 @@ class ApiFactory {
302
303
 
303
304
  api._sendMessageDirect = DIRECT_FN;
304
305
  api.sendMessage = function (message, threadID, cb, replyToMessage) {
305
- // New: Auto-Typing support for improved human-like behavior
306
- if (globalOptions.autoTyping) {
307
- try {
308
- api.sendTypingIndicator(threadID, (err) => {
309
- // Ignore typing errors to avoid blocking the message
310
- });
311
- } catch (_) { /* ignore */ }
312
- }
313
-
314
306
  if (!globalOptions.groupQueueEnabled || !isGroupThread(threadID)) {
315
307
  return api._sendMessageDirect(message, threadID, cb, replyToMessage);
316
308
  }
@@ -22,7 +22,7 @@ class MqttManager extends EventEmitter {
22
22
  this.heartbeatInterval = null;
23
23
  this.connectionTimeout = null;
24
24
  this.lastActivity = Date.now();
25
-
25
+
26
26
  // Performance metrics
27
27
  this.metrics = {
28
28
  messagesReceived: 0,
@@ -49,13 +49,13 @@ class MqttManager extends EventEmitter {
49
49
  }
50
50
 
51
51
  logger('🔄 Connecting to MQTT...', 'info');
52
-
52
+
53
53
  const options = this._buildConnectionOptions();
54
54
  this.client = mqtt.connect(this.ctx.mqttEndpoint, options);
55
-
55
+
56
56
  this._setupEventHandlers();
57
57
  this._startHeartbeat();
58
-
58
+
59
59
  // Connection timeout
60
60
  this.connectionTimeout = setTimeout(() => {
61
61
  if (!this.isConnected) {
@@ -81,7 +81,7 @@ class MqttManager extends EventEmitter {
81
81
  clean: true,
82
82
  connectTimeout: 10000,
83
83
  reconnectPeriod: 0, // Disable auto-reconnect, we handle it
84
- keepalive: 60,
84
+ keepalive: 10,
85
85
  protocolVersion: 4,
86
86
  username: JSON.stringify({
87
87
  "u": this.ctx.userID,
@@ -150,15 +150,15 @@ class MqttManager extends EventEmitter {
150
150
  this.reconnectDelay = 1000;
151
151
  this.metrics.lastConnected = Date.now();
152
152
  this.metrics.reconnections++;
153
-
153
+
154
154
  clearTimeout(this.connectionTimeout);
155
-
155
+
156
156
  logger('✅ MQTT connected successfully', 'info');
157
157
  logger(`📊 Connection metrics: Reconnections: ${this.metrics.reconnections}, Errors: ${this.metrics.errors}`, 'info');
158
-
158
+
159
159
  this._subscribeToTopics();
160
160
  this._processMessageQueue();
161
-
161
+
162
162
  this.emit('connected');
163
163
  }
164
164
 
@@ -169,7 +169,7 @@ class MqttManager extends EventEmitter {
169
169
  _subscribeToTopics() {
170
170
  const topics = [
171
171
  "/ls_req",
172
- "/ls_resp",
172
+ "/ls_resp",
173
173
  "/legacy_web",
174
174
  "/webrtc",
175
175
  "/rtc_multi",
@@ -208,10 +208,10 @@ class MqttManager extends EventEmitter {
208
208
  try {
209
209
  this.lastActivity = Date.now();
210
210
  this.metrics.messagesReceived++;
211
-
211
+
212
212
  const messageStr = message.toString();
213
213
  let parsedMessage;
214
-
214
+
215
215
  try {
216
216
  parsedMessage = JSON.parse(messageStr);
217
217
  } catch (parseError) {
@@ -221,7 +221,7 @@ class MqttManager extends EventEmitter {
221
221
 
222
222
  // Enhanced message processing with caching
223
223
  this._processMessage(topic, parsedMessage);
224
-
224
+
225
225
  } catch (error) {
226
226
  logger(`❌ Error processing MQTT message: ${error.message}`, 'error');
227
227
  this.metrics.errors++;
@@ -245,7 +245,7 @@ class MqttManager extends EventEmitter {
245
245
  if (message.syncToken) {
246
246
  this.ctx.syncToken = message.syncToken;
247
247
  }
248
-
248
+
249
249
  if (message.lastIssuedSeqId) {
250
250
  this.ctx.lastSeqId = parseInt(message.lastIssuedSeqId);
251
251
  }
@@ -270,7 +270,7 @@ class MqttManager extends EventEmitter {
270
270
  logger('🔌 MQTT connection closed', 'warn');
271
271
  this._stopHeartbeat();
272
272
  this.emit('disconnected');
273
-
273
+
274
274
  // Auto-reconnect if not intentionally closed
275
275
  if (this.client && !this.client.disconnecting) {
276
276
  this._handleReconnect();
@@ -308,9 +308,9 @@ class MqttManager extends EventEmitter {
308
308
 
309
309
  this.reconnectAttempts++;
310
310
  const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
311
-
311
+
312
312
  logger(`🔄 MQTT reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'warn');
313
-
313
+
314
314
  setTimeout(() => {
315
315
  this.connect();
316
316
  }, delay);
@@ -350,7 +350,7 @@ class MqttManager extends EventEmitter {
350
350
  this.messageQueue.shift(); // Remove oldest message
351
351
  logger('⚠️ Message queue full, dropping oldest message', 'warn');
352
352
  }
353
-
353
+
354
354
  this.messageQueue.push(messageData);
355
355
  logger(`📦 Queued message for ${messageData.topic} (queue size: ${this.messageQueue.length})`, 'info');
356
356
  }
@@ -361,12 +361,12 @@ class MqttManager extends EventEmitter {
361
361
  */
362
362
  _processMessageQueue() {
363
363
  if (this.messageQueue.length === 0) return;
364
-
364
+
365
365
  logger(`📦 Processing ${this.messageQueue.length} queued messages`, 'info');
366
-
366
+
367
367
  const messages = [...this.messageQueue];
368
368
  this.messageQueue = [];
369
-
369
+
370
370
  messages.forEach(messageData => {
371
371
  this.publish(messageData.topic, messageData.message, messageData.options);
372
372
  });
@@ -385,7 +385,7 @@ class MqttManager extends EventEmitter {
385
385
  logger('💓 MQTT heartbeat - inactive for 5 minutes, sending ping', 'info');
386
386
  this.client.ping();
387
387
  }
388
-
388
+
389
389
  // Update uptime
390
390
  if (this.metrics.lastConnected) {
391
391
  this.metrics.uptime = Date.now() - this.metrics.lastConnected;
@@ -422,9 +422,9 @@ class MqttManager extends EventEmitter {
422
422
  */
423
423
  disconnect() {
424
424
  logger('🔌 Disconnecting MQTT gracefully...', 'info');
425
-
425
+
426
426
  this._stopHeartbeat();
427
-
427
+
428
428
  if (this.client) {
429
429
  this.client.publish("/browser_close", "{}", { qos: 0 });
430
430
  this.client.end(false, () => {
@@ -432,7 +432,7 @@ class MqttManager extends EventEmitter {
432
432
  this.emit('disconnected');
433
433
  });
434
434
  }
435
-
435
+
436
436
  this.isConnected = false;
437
437
  }
438
438
 
@@ -441,13 +441,13 @@ class MqttManager extends EventEmitter {
441
441
  */
442
442
  forceDisconnect() {
443
443
  logger('⚡ Force disconnecting MQTT...', 'warn');
444
-
444
+
445
445
  this._stopHeartbeat();
446
-
446
+
447
447
  if (this.client) {
448
448
  this.client.end(true);
449
449
  }
450
-
450
+
451
451
  this.isConnected = false;
452
452
  this.emit('disconnected');
453
453
  }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ const http = require('http');
4
+ const log = require('npmlog');
5
+ const pkg = require('../../package.json');
6
+
7
+ /**
8
+ * Minimal Health Server for Render/Railway/Cloud compatibility.
9
+ * Responds to platform health checks on the assigned PORT.
10
+ */
11
+ class HealthServer {
12
+ constructor(options = {}) {
13
+ this.port = process.env.PORT || options.port || 10000;
14
+ this.server = null;
15
+ this.active = false;
16
+ }
17
+
18
+ start() {
19
+ if (this.active) return;
20
+
21
+ this.server = http.createServer((req, res) => {
22
+ if (req.url === '/health' || req.url === '/') {
23
+ res.writeHead(200, { 'Content-Type': 'text/html' });
24
+ const html = `
25
+ <!DOCTYPE html>
26
+ <html>
27
+ <head>
28
+ <title>Nexus-FCA Status</title>
29
+ <style>
30
+ body { background: #0f172a; color: #f8fafc; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
31
+ .card { background: #1e293b; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); border: 1px solid #334155; text-align: center; }
32
+ h1 { color: #38bdf8; margin-top: 0; }
33
+ .status { display: inline-block; padding: 0.25rem 0.75rem; background: #065f46; color: #34d399; border-radius: 9999px; font-size: 0.875rem; font-weight: 600; }
34
+ .version { color: #64748b; font-size: 0.75rem; margin-top: 1rem; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="card">
39
+ <h1>✨ Nexus-FCA</h1>
40
+ <div class="status">● System Operational</div>
41
+ <div class="version">Core v${pkg.version} | Stability: 99.9%</div>
42
+ </div>
43
+ </body>
44
+ </html>
45
+ `;
46
+ res.end(html);
47
+ } else {
48
+ res.writeHead(404);
49
+ res.end();
50
+ }
51
+ });
52
+
53
+ this.server.listen(this.port, () => {
54
+ log.info("HealthServer", `System health endpoint active on port ${this.port}`);
55
+ this.active = true;
56
+ });
57
+
58
+ this.server.on('error', (err) => {
59
+ log.error("HealthServer", `Failed to start: ${err.message}`);
60
+ });
61
+ }
62
+
63
+ stop() {
64
+ if (this.server) {
65
+ this.server.close();
66
+ this.active = false;
67
+ }
68
+ }
69
+ }
70
+
71
+ module.exports = HealthServer;
@@ -20,45 +20,51 @@ class CookieManager {
20
20
  if (!Array.isArray(cookies) || cookies.length === 0) {
21
21
  return cookies;
22
22
  }
23
-
23
+
24
24
  const {
25
25
  defaultExpiryDays = 90,
26
26
  criticalExpiryDays = 90,
27
27
  refreshExisting = true
28
28
  } = options;
29
-
29
+
30
30
  const now = new Date();
31
31
  const criticalCookies = ['c_user', 'xs', 'fr', 'datr', 'sb', 'spin'];
32
32
  let fixedCount = 0;
33
-
33
+
34
34
  for (const cookie of cookies) {
35
35
  // Skip cookies with no key
36
36
  if (!cookie.key) continue;
37
-
37
+
38
38
  const isCritical = criticalCookies.includes(cookie.key);
39
39
  const days = isCritical ? criticalExpiryDays : defaultExpiryDays;
40
-
40
+
41
41
  // Check if cookie needs expiry fix
42
- const needsFix = !cookie.expires ||
43
- refreshExisting ||
44
- !this._isValidDate(cookie.expires) ||
45
- this._getRemainingDays(cookie.expires) < 7;
46
-
42
+ const needsFix = !cookie.expires ||
43
+ refreshExisting ||
44
+ !this._isValidDate(cookie.expires) ||
45
+ this._getRemainingDays(cookie.expires) < 7;
46
+
47
47
  if (needsFix) {
48
+ // STICKY COOKIES: If it's a critical cookie (sb, datr, c_user) and still has > 60 days, skip refresh
49
+ const stickyCookies = ['sb', 'datr', 'c_user'];
50
+ if (stickyCookies.includes(cookie.key) && this._getRemainingDays(cookie.expires) > 60) {
51
+ continue;
52
+ }
53
+
48
54
  // Set expiry to future date
49
55
  const futureDate = new Date(now.getTime() + (days * 24 * 60 * 60 * 1000));
50
56
  cookie.expires = futureDate.toUTCString();
51
57
  fixedCount++;
52
58
  }
53
59
  }
54
-
60
+
55
61
  if (fixedCount > 0) {
56
62
  logger(`Fixed expiry dates for ${fixedCount} cookies`, 'info');
57
63
  }
58
-
64
+
59
65
  return cookies;
60
66
  }
61
-
67
+
62
68
  /**
63
69
  * Check if cookie expiry is a valid date
64
70
  * @param {string} dateStr - Date string to check
@@ -69,7 +75,7 @@ class CookieManager {
69
75
  const date = new Date(dateStr);
70
76
  return !isNaN(date.getTime());
71
77
  }
72
-
78
+
73
79
  /**
74
80
  * Get days remaining until expiry
75
81
  * @param {string} dateStr - Date string to check
@@ -85,7 +91,7 @@ class CookieManager {
85
91
  return 0;
86
92
  }
87
93
  }
88
-
94
+
89
95
  /**
90
96
  * Check if appstate has critical cookies
91
97
  * @param {Array} cookies - Cookies to check
@@ -95,22 +101,23 @@ class CookieManager {
95
101
  if (!Array.isArray(cookies) || cookies.length === 0) {
96
102
  return { valid: false, missing: ['all'] };
97
103
  }
98
-
99
- const criticalCookies = ['c_user', 'xs', 'datr', 'sb'];
104
+
105
+ const criticalCookies = ['c_user', 'xs', 'datr', 'sb', 'fr', 'spin'];
100
106
  const missing = [];
101
-
107
+
102
108
  for (const critical of criticalCookies) {
103
- if (!cookies.some(c => c.key === critical)) {
109
+ const found = cookies.find(c => c.key === critical);
110
+ if (!found || !found.value || found.value === 'deleted') {
104
111
  missing.push(critical);
105
112
  }
106
113
  }
107
-
114
+
108
115
  return {
109
116
  valid: missing.length === 0,
110
117
  missing
111
118
  };
112
119
  }
113
-
120
+
114
121
  /**
115
122
  * Generate default cookie expiry date
116
123
  * @param {string} cookieName - Name of cookie
@@ -119,13 +126,13 @@ class CookieManager {
119
126
  static getDefaultExpiry(cookieName) {
120
127
  const now = new Date();
121
128
  const criticalCookies = ['c_user', 'xs', 'fr', 'datr', 'sb'];
122
-
129
+
123
130
  // Critical cookies get 90 days, others get 30 days
124
131
  const days = criticalCookies.includes(cookieName) ? 90 : 30;
125
132
  const future = new Date(now.getTime() + (days * 24 * 60 * 60 * 1000));
126
133
  return future.toUTCString();
127
134
  }
128
-
135
+
129
136
  /**
130
137
  * Load and fix appstate file
131
138
  * @param {string} appstatePath - Path to appstate.json
@@ -137,26 +144,26 @@ class CookieManager {
137
144
  logger(`Appstate file not found: ${appstatePath}`, 'error');
138
145
  return null;
139
146
  }
140
-
147
+
141
148
  const cookies = JSON.parse(fs.readFileSync(appstatePath, 'utf8'));
142
149
  if (!Array.isArray(cookies)) {
143
150
  logger('Invalid appstate format: not an array', 'error');
144
151
  return null;
145
152
  }
146
-
153
+
147
154
  // Fix expiry dates
148
155
  const fixed = this.fixCookieExpiry(cookies);
149
-
156
+
150
157
  // Validate critical cookies
151
158
  const validation = this.validateCriticalCookies(fixed);
152
159
  if (!validation.valid) {
153
160
  logger(`Missing critical cookies: ${validation.missing.join(', ')}`, 'warn');
154
161
  }
155
-
162
+
156
163
  // Save back fixed cookies
157
164
  fs.writeFileSync(appstatePath, JSON.stringify(fixed, null, 2));
158
165
  logger(`Fixed and saved appstate with ${fixed.length} cookies`, 'info');
159
-
166
+
160
167
  return fixed;
161
168
  } catch (err) {
162
169
  logger(`Failed to load and fix appstate: ${err.message}`, 'error');
@@ -6,6 +6,7 @@
6
6
  const crypto = require('crypto');
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const log = require('npmlog');
9
10
  // StealthMode module has been removed to avoid global human-like pauses
10
11
  // that can harm perceived stability. FacebookSafety now relies on
11
12
  // lightweight adaptive delays and backoff only.
@@ -96,6 +97,9 @@ class FacebookSafety {
96
97
  this._dynamicHeartbeatTimer = null; // replaces fixed interval heartbeat for risk-tier tuning
97
98
  this._riskLast = 'low';
98
99
 
100
+ // Safety Store for Token Mirroring (Recovery on ephemeral platforms)
101
+ this.safetyStorePath = path.join(process.cwd(), 'lib', 'safety', 'safety_store.json');
102
+
99
103
  this.initSafety();
100
104
  }
101
105
 
@@ -105,9 +109,14 @@ class FacebookSafety {
105
109
  this.setupSafeRefresh();
106
110
  }
107
111
 
112
+ // Recovery from safety store
113
+ this._loadFromSafetyStore();
114
+
108
115
  // Setup session monitoring
109
116
  this.setupSessionMonitoring();
110
117
  this._schedulePeriodicRecycle();
118
+ this.scheduleLightPoke();
119
+ this.scheduleSessionBreath();
111
120
  }
112
121
 
113
122
  /**
@@ -328,9 +337,25 @@ class FacebookSafety {
328
337
  maxMs = 90 * 60 * 1000; // 90m
329
338
  }
330
339
  const interval = minMs + Math.random() * (maxMs - minMs);
331
- console.log(`🔒 Next token refresh in ${Math.floor(interval / (60 * 1000))}m (keep-alive mode)`);
340
+ log.info("FacebookSafety", `Next token refresh in ${Math.floor(interval / (60 * 1000))}m (keep-alive mode)`);
332
341
  const t = setTimeout(async () => {
333
342
  await this.refreshSafeSession();
343
+
344
+ // Extra: Explicitly refresh and save fb_dtsg every ~2 hours
345
+ if (Date.now() - this._lastHeavyMaintenanceTs > 2 * 60 * 60 * 1000) {
346
+ try {
347
+ if (this.api && typeof this.api.refreshFb_dtsg === 'function') {
348
+ const newDtsg = await this.api.refreshFb_dtsg();
349
+ if (newDtsg && this.ctx && this.ctx.appStatePath) {
350
+ // Auto-save will be triggered by saveCookies inside refreshFb_dtsg (if it calls post)
351
+ // But we force a saveAppState here just to be sure
352
+ require('../../utils').saveAppState(this.ctx);
353
+ log.info("FacebookSafety", 'Critical token (fb_dtsg) refreshed and saved to disk');
354
+ }
355
+ }
356
+ } catch (_) { }
357
+ }
358
+
334
359
  schedule();
335
360
  }, interval);
336
361
  this._registerTimer(t);
@@ -474,6 +499,38 @@ class FacebookSafety {
474
499
  this._periodicRecycleTimer = t;
475
500
  }
476
501
 
502
+ /**
503
+ * Lightweight poke (fb_dtsg refresh only) integrated to remove duplicate logic in index.js
504
+ */
505
+ scheduleLightPoke() {
506
+ if (this._lightPokeTimer || this._destroyed) return;
507
+ const base = 30 * 60 * 1000; // 30m (Tuned for proactive health)
508
+ const jitter = (Math.random() * 20 - 10) * 60 * 1000; // ±10m
509
+ const schedule = () => {
510
+ if (this._destroyed) return;
511
+ const t = setTimeout(async () => {
512
+ if (this._destroyed) return;
513
+ // Respect spacing: skip if recent heavy refresh
514
+ if (Date.now() - this._lastRefreshTs < this._minSpacingMs / 2) {
515
+ schedule();
516
+ return;
517
+ }
518
+ try {
519
+ if (this.api && typeof this.api.refreshFb_dtsg === 'function') {
520
+ await this.api.refreshFb_dtsg().catch(() => { });
521
+ this._lastRefreshTs = Date.now();
522
+ this._lastLightPokeTs = Date.now();
523
+ this.safetyEmit('lightPoke', { ts: Date.now() });
524
+ }
525
+ } catch (_) { }
526
+ schedule();
527
+ }, base + (Math.random() * 20 - 10) * 60 * 1000);
528
+ this._registerTimer(t);
529
+ this._lightPokeTimer = t;
530
+ };
531
+ schedule();
532
+ }
533
+
477
534
  // Heartbeat ping & watchdog - ULTRA SAFE VERSION
478
535
  _startHeartbeat() {
479
536
  if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
@@ -576,11 +633,12 @@ class FacebookSafety {
576
633
  let preMqttConnected = this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected;
577
634
  let preLastEvent = this._lastEventTs;
578
635
  try {
579
- console.log('🔄 Performing safe session refresh...');
636
+ log.info("FacebookSafety", 'Performing safe session refresh...');
580
637
  if (!this.api || typeof this.api.refreshFb_dtsg !== 'function') {
581
- console.log('⚠️ Safe refresh skipped: api.refreshFb_dtsg not available');
638
+ log.warn("FacebookSafety", 'Safe refresh skipped: api.refreshFb_dtsg not available');
582
639
  return;
583
640
  }
641
+
584
642
  // Abort protection if takes too long (network hang)
585
643
  const timeoutMs = 25 * 1000;
586
644
  const controller = new AbortController();
@@ -588,6 +646,11 @@ class FacebookSafety {
588
646
  let res;
589
647
  try {
590
648
  res = await this.api.refreshFb_dtsg({ signal: controller.signal });
649
+ this._saveToSafetyStore(); // Mirror to safety store
650
+ } catch (fbErr) {
651
+ log.warn("FacebookSafety", 'Primary fb_dtsg refresh failed, attempting Business fallback...');
652
+ res = await this.refreshFb_dtsgBusiness().catch(() => null);
653
+ if (!res) throw fbErr;
591
654
  } finally { clearTimeout(timeout); }
592
655
  this.sessionMetrics.errorCount = Math.max(0, this.sessionMetrics.errorCount - 1);
593
656
  this.sessionMetrics.lastActivity = Date.now();
@@ -639,35 +702,25 @@ class FacebookSafety {
639
702
  }
640
703
 
641
704
  /**
642
- * Lightweight poke (fb_dtsg refresh only) integrated to remove duplicate logic in index.js
705
+ * Fallback Token Refresh via Business Suite
706
+ * Business endpoints are often more stable for bots.
643
707
  */
644
- scheduleLightPoke() {
645
- if (this._lightPokeTimer || this._destroyed) return;
646
- const base = 45 * 60 * 1000; // 45m (Reduced from 6h to keep session alive)
647
- const jitter = (Math.random() * 20 - 10) * 60 * 1000; // ±10m
648
- const schedule = () => {
649
- if (this._destroyed) return;
650
- const t = setTimeout(async () => {
651
- if (this._destroyed) return;
652
- // Respect spacing: skip if recent heavy refresh
653
- if (Date.now() - this._lastRefreshTs < this._minSpacingMs / 2) {
654
- schedule();
655
- return;
656
- }
657
- try {
658
- if (this.api && typeof this.api.refreshFb_dtsg === 'function') {
659
- await this.api.refreshFb_dtsg().catch(() => { });
660
- this._lastRefreshTs = Date.now();
661
- this._lastLightPokeTs = Date.now();
662
- this.safetyEmit('lightPoke', { ts: Date.now() });
663
- }
664
- } catch (_) { }
665
- schedule();
666
- }, base + (Math.random() * 20 - 10) * 60 * 1000);
667
- this._registerTimer(t);
668
- this._lightPokeTimer = t;
669
- };
670
- schedule();
708
+ async refreshFb_dtsgBusiness() {
709
+ if (!this.api || !this.ctx) return null;
710
+ try {
711
+ const utils = require('../../utils');
712
+ const res = await utils.get("https://business.facebook.com/business_locations", this.ctx.jar, null, { noRef: true }, this.ctx);
713
+ const html = res.body;
714
+ const match = html.match(/\["DTSGInitialData",\[\],\{"token":"(.*?)"\},/);
715
+ if (match && match[1]) {
716
+ this.ctx.fb_dtsg = match[1];
717
+ log.info("FacebookSafety", 'Refreshed fb_dtsg via Business fallback');
718
+ return match[1];
719
+ }
720
+ } catch (e) {
721
+ log.verbose("FacebookSafety", 'Business refresh failed: ' + e.message);
722
+ }
723
+ return null;
671
724
  }
672
725
 
673
726
  _registerTimer(t) {
@@ -678,7 +731,7 @@ class FacebookSafety {
678
731
  // Cleanup / destroy resources (to prevent dangling timers)
679
732
  destroy() {
680
733
  this._destroyed = true;
681
- const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer, this._lightPokeTimer];
734
+ const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer, this._lightPokeTimer, this._breathTimer];
682
735
  timers.forEach(t => t && clearTimeout(t));
683
736
  // Clear any registered anonymous timers
684
737
  this._timerRegistry.forEach(t => clearTimeout(t));
@@ -834,6 +887,71 @@ class FacebookSafety {
834
887
  setSafetyEventHandler(handler) {
835
888
  this.onSafetyEvent = handler;
836
889
  }
890
+
891
+ /**
892
+ * ULTRA-LIGHTWEIGHT "Session Breath"
893
+ * Performs a tiny request to maintain session activity without heavy overhead or detection risk.
894
+ */
895
+ scheduleSessionBreath() {
896
+ if (this._breathTimer || this._destroyed) return;
897
+ const base = 20 * 60 * 1000; // 20m (Breath cycle)
898
+ const schedule = () => {
899
+ if (this._destroyed) return;
900
+ const t = setTimeout(async () => {
901
+ if (this._destroyed) return;
902
+ try {
903
+ const utils = require('../../utils');
904
+ // Hit a static lightweight FB asset or GraphQL endpoint
905
+ await utils.get("https://www.facebook.com/rsrc.php/v3/yO/r/688pC_S3Y6X.png", this.ctx.jar, null, { noRef: true }, this.ctx).catch(() => { });
906
+ this.safetyEmit('sessionBreath', { ts: Date.now() });
907
+ } catch (_) { }
908
+ schedule();
909
+ }, base + (Math.random() * 5 * 60 * 1000)); // 20-25m
910
+ this._registerTimer(t);
911
+ this._breathTimer = t;
912
+ };
913
+ schedule();
914
+ }
915
+
916
+
917
+ /**
918
+ * Mirror critical tokens to a separate store for recovery on ephemeral platforms.
919
+ */
920
+ _saveToSafetyStore() {
921
+ if (!this.ctx || !this.ctx.fb_dtsg) return;
922
+ try {
923
+ const data = {
924
+ fb_dtsg: this.ctx.fb_dtsg,
925
+ jazoest: this.ctx.jazoest,
926
+ updatedAt: new Date().toISOString()
927
+ };
928
+ const dir = require('path').dirname(this.safetyStorePath);
929
+ const fs = require('fs');
930
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
931
+ fs.writeFileSync(this.safetyStorePath, JSON.stringify(data, null, 2));
932
+ } catch (e) {
933
+ log.verbose("FacebookSafety", "Safety store save failed: " + e.message);
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Recovery logic from safety store.
939
+ */
940
+ _loadFromSafetyStore() {
941
+ try {
942
+ const fs = require('fs');
943
+ if (fs.existsSync(this.safetyStorePath)) {
944
+ const data = JSON.parse(fs.readFileSync(this.safetyStorePath, 'utf8'));
945
+ if (data.fb_dtsg && (!this.ctx.fb_dtsg || this.ctx.fb_dtsg === 'undefined')) {
946
+ this.ctx.fb_dtsg = data.fb_dtsg;
947
+ this.ctx.jazoest = data.jazoest;
948
+ log.info("FacebookSafety", "Recovered tokens from safety store");
949
+ }
950
+ }
951
+ } catch (e) {
952
+ log.verbose("FacebookSafety", "Safety store load failed: " + e.message);
953
+ }
954
+ }
837
955
  }
838
956
 
839
957
  module.exports = FacebookSafety;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "3.2.4",
4
- "description": "Nexus-FCA 3.2.4 – Advanced, Secure & Stable Facebook Messenger API.",
3
+ "version": "3.3.0",
4
+ "description": "Nexus-FCA 3.3.0 – Advanced, Secure & Stable Facebook Messenger API.",
5
5
  "main": "index.js",
6
6
  "repository": {
7
7
  "type": "git",
package/src/listenMqtt.js CHANGED
@@ -203,11 +203,18 @@ function startForegroundRefresh(ctx) {
203
203
  clearInterval(ctx._foregroundRefreshInterval);
204
204
  ctx._foregroundRefreshInterval = null;
205
205
  }
206
+ if (ctx._presenceRefreshInterval) {
207
+ clearInterval(ctx._presenceRefreshInterval);
208
+ ctx._presenceRefreshInterval = null;
209
+ }
206
210
  const options = ctx.globalOptions || {};
207
211
  if (options.foregroundRefreshEnabled === false) return;
212
+
213
+ // Foreground state refresh - reduced from 15min to 1min for better activity signal
208
214
  const minutes = Number.isFinite(options.foregroundRefreshMinutes)
209
- ? Math.max(1, options.foregroundRefreshMinutes)
210
- : 15;
215
+ ? Math.max(0.5, options.foregroundRefreshMinutes)
216
+ : 1; // Changed: 15 → 1 minute (like ws3-fca approach)
217
+
211
218
  ctx._foregroundRefreshInterval = setInterval(() => {
212
219
  if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
213
220
  try {
@@ -224,12 +231,33 @@ function startForegroundRefresh(ctx) {
224
231
  }
225
232
  }
226
233
  }, minutes * 60 * 1000);
234
+
235
+ // ws3-fca style presence signal - sends activity indicator every 50s
236
+ // This prevents Facebook from marking connection as idle/background
237
+ const presenceIntervalSec = Number.isFinite(options.presenceRefreshSeconds)
238
+ ? Math.max(30, options.presenceRefreshSeconds)
239
+ : 50; // ws3-fca uses 50 seconds
240
+
241
+ ctx._presenceRefreshInterval = setInterval(() => {
242
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
243
+ try {
244
+ // Generate presence payload like ws3-fca
245
+ const presencePayload = utils.generatePresence(ctx.userID);
246
+ ctx.mqttClient.publish("/t_p", presencePayload, { qos: 0, retain: false });
247
+ } catch (err) {
248
+ // Silent fail for presence - not critical
249
+ }
250
+ }, presenceIntervalSec * 1000);
227
251
  }
228
252
  function stopForegroundRefresh(ctx) {
229
253
  if (ctx && ctx._foregroundRefreshInterval) {
230
254
  clearInterval(ctx._foregroundRefreshInterval);
231
255
  ctx._foregroundRefreshInterval = null;
232
256
  }
257
+ if (ctx && ctx._presenceRefreshInterval) {
258
+ clearInterval(ctx._presenceRefreshInterval);
259
+ ctx._presenceRefreshInterval = null;
260
+ }
233
261
  }
234
262
  function getStormGuard(ctx) {
235
263
  if (!ctx._mqttStorm) {
@@ -503,8 +531,17 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
503
531
  }
504
532
  const chatOn = (ctx.globalOptions && ctx.globalOptions.online === false) ? false : true;
505
533
  const foreground = getForegroundState(ctx);
534
+
535
+ // ws3-fca style: Generate fresh sessionID and clientID on EACH reconnect
536
+ // This prevents Facebook from detecting "stale" sessions and force-disconnecting
506
537
  const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
538
+ const clientID = utils.generateClientID ? utils.generateClientID() : `mqttwsclient_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
507
539
  const GUID = utils.getGUID();
540
+
541
+ // Store for debugging/tracking
542
+ ctx._lastSessionID = sessionID;
543
+ ctx._lastClientID = clientID;
544
+
508
545
  const username = {
509
546
  u: ctx.userID,
510
547
  s: sessionID,
@@ -528,16 +565,26 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
528
565
  };
529
566
  // jitter user agent keep consistent
530
567
  const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
568
+
569
+ // MQTT Host Selection - ws3-fca uses messenger.com, we support both with option
570
+ // messenger.com endpoint appears more stable for some accounts
531
571
  let host;
572
+ const useMessengerDomain = ctx.globalOptions.mqttUseMessengerDomain;
573
+ const baseDomain = useMessengerDomain ? 'edge-chat.messenger.com' : 'edge-chat.facebook.com';
574
+
532
575
  if (ctx.mqttEndpoint) {
533
- host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${GUID}`;
576
+ host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${clientID}`;
534
577
  } else if (ctx.region) {
535
- host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${GUID}`;
578
+ host = `wss://${baseDomain}/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${clientID}`;
536
579
  } else {
537
- host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${GUID}`;
580
+ host = `wss://${baseDomain}/chat?sid=${sessionID}&cid=${clientID}`;
538
581
  }
582
+
583
+ // Determine Host header based on domain used
584
+ const hostHeader = useMessengerDomain ? 'edge-chat.messenger.com' : 'edge-chat.facebook.com';
585
+
539
586
  const options = {
540
- clientId: "mqttwsclient",
587
+ clientId: clientID, // Use dynamic clientID instead of static "mqttwsclient"
541
588
  protocolId: "MQIsdp",
542
589
  protocolVersion: 3,
543
590
  username: JSON.stringify(username),
@@ -550,7 +597,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
550
597
  ctx.globalOptions.userAgent ||
551
598
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
552
599
  Referer: "https://www.facebook.com/",
553
- Host: "edge-chat.facebook.com",
600
+ Host: hostHeader,
554
601
  Connection: "Upgrade",
555
602
  Pragma: "no-cache",
556
603
  "Cache-Control": "no-cache",
@@ -783,16 +830,16 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
783
830
  ctx.health.onConnect();
784
831
 
785
832
  // WS3-style randomized proactive reconnect (tunable window).
786
- // Changed: 2-4h 6-8h to reduce unnecessary reconnects and improve stability
787
- // Proactive reconnect prevents long-session forced disconnects by Facebook
833
+ // CRITICAL FIX: Changed from 6-8h to 45-90min based on ws3-fca analysis (26-60min)
834
+ // Long sessions (2h+) cause Facebook to force-disconnect, proactive reconnect prevents this
788
835
  if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer);
789
836
  let reconnectTime = null;
790
837
  const opts = ctx.globalOptions || {};
791
838
  const proactiveEnabled = opts.mqttProactiveReconnectEnabled;
792
839
 
793
840
  if (proactiveEnabled !== false) {
794
- const minM = Number.isFinite(opts.mqttProactiveReconnectMinMinutes) ? opts.mqttProactiveReconnectMinMinutes : 360; // Changed: 120min 360min (6h)
795
- const maxM = Number.isFinite(opts.mqttProactiveReconnectMaxMinutes) ? opts.mqttProactiveReconnectMaxMinutes : 480; // Changed: 240min 480min (8h)
841
+ const minM = Number.isFinite(opts.mqttProactiveReconnectMinMinutes) ? opts.mqttProactiveReconnectMinMinutes : 45; // ws3-fca: 26min, we use 45min for safety margin
842
+ const maxM = Number.isFinite(opts.mqttProactiveReconnectMaxMinutes) ? opts.mqttProactiveReconnectMaxMinutes : 90; // ws3-fca: 60min, we use 90min for safety margin
796
843
  const min = Math.min(minM, maxM);
797
844
  const max = Math.max(minM, maxM);
798
845
  const intervalMinutes = Math.floor(Math.random() * (max - min + 1)) + min;
@@ -269,7 +269,7 @@ module.exports = function (defaultFuncs, api, ctx) {
269
269
  // Changing this to accomodate an array of users
270
270
  if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
271
271
 
272
- if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
272
+ if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " + messageIDType + "." });
273
273
 
274
274
  if (msgType === "String") msg = { body: msg };
275
275
  var disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
@@ -308,11 +308,11 @@ module.exports = function (defaultFuncs, api, ctx) {
308
308
  has_attachment: !!(msg.attachment || msg.url || msg.sticker),
309
309
  signatureID: utils.getSignatureID(),
310
310
  replied_to_message_id: replyToMessage,
311
- reply_metadata: replyToMessage ? {
311
+ reply_metadata: replyToMessage ? JSON.stringify({
312
312
  reply_source_id: replyToMessage,
313
313
  reply_source_type: 1, // 1: Message
314
314
  reply_type: 0 // 0: Reply
315
- } : undefined
315
+ }) : undefined
316
316
  };
317
317
 
318
318
  handleLocation(msg, form, callback, () =>
package/utils.js CHANGED
@@ -115,6 +115,15 @@ function getHeaders(url, options, ctx, customHeader) {
115
115
  headers["sec-fetch-dest"] = "document";
116
116
  }
117
117
 
118
+ // Dynamic Referer Pattern
119
+ if (url.includes('graphql')) {
120
+ headers.Referer = 'https://www.facebook.com/';
121
+ } else if (url.includes('messages/')) {
122
+ headers.Referer = 'https://www.facebook.com/messages/';
123
+ } else if (url.includes('business')) {
124
+ headers.Referer = 'https://business.facebook.com/';
125
+ }
126
+
118
127
  // Dynamic Client Hints - CRITICAL: Must match User-Agent
119
128
  if (isChrome && isWindows) {
120
129
  headers["sec-ch-ua"] = '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"';
@@ -167,6 +176,7 @@ async function get(url, jar, qs, options, ctx) {
167
176
  if (qs) op.searchParams = cleanObject(qs);
168
177
 
169
178
  return got(url, op).then(function (res) {
179
+ saveCookies(jar, ctx)(res);
170
180
  return res;
171
181
  });
172
182
  }
@@ -183,6 +193,7 @@ async function get2(url, jar, headers, options, ctx) {
183
193
  };
184
194
 
185
195
  return got(url, op).then(function (res) {
196
+ saveCookies(jar, ctx)(res);
186
197
  return res;
187
198
  });
188
199
  }
@@ -230,6 +241,7 @@ async function post(url, jar, form, options, ctx, customHeader) {
230
241
  }
231
242
 
232
243
  return got(url, op).then(function (res) {
244
+ saveCookies(jar, ctx)(res);
233
245
  return res;
234
246
  });
235
247
  }
@@ -256,6 +268,7 @@ async function postFormData(url, jar, form, qs, options, ctx) {
256
268
  if (qs) op.searchParams = cleanObject(qs);
257
269
 
258
270
  return got(url, op).then(function (res) {
271
+ saveCookies(jar, ctx)(res);
259
272
  return res;
260
273
  });
261
274
  }
@@ -387,6 +400,19 @@ function generatePresence(userID) {
387
400
  );
388
401
  }
389
402
 
403
+ /**
404
+ * Generate a unique MQTT client ID for each connection
405
+ * ws3-fca style: Fresh clientID on each reconnect prevents Facebook from
406
+ * detecting "stale" sessions and force-disconnecting
407
+ * @returns {string} Unique client identifier
408
+ */
409
+ function generateClientID() {
410
+ const timestamp = Date.now().toString(36);
411
+ const random = Math.random().toString(36).substr(2, 9);
412
+ const counter = (global.__NEXUS_CLIENT_COUNTER__ = (global.__NEXUS_CLIENT_COUNTER__ || 0) + 1);
413
+ return `mqttwsclient_${timestamp}_${random}_${counter}`;
414
+ }
415
+
390
416
  function getGUID() {
391
417
  // 1. Check Environment Variable (Best for Render/Heroku/Replit)
392
418
  if (process.env.NEXUS_DEVICE_ID) {
@@ -1311,9 +1337,11 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount = 0, sourceCall) {
1311
1337
  });
1312
1338
  };
1313
1339
  }
1314
- function saveCookies(jar) {
1340
+ function saveCookies(jar, ctx) {
1315
1341
  return function (res) {
1316
1342
  var cookies = res.headers["set-cookie"] || [];
1343
+ if (cookies.length === 0) return res;
1344
+
1317
1345
  cookies.forEach(function (c) {
1318
1346
  if (c.indexOf(".facebook.com") > -1 || c.indexOf("facebook.com") > -1) {
1319
1347
  try {
@@ -1328,10 +1356,50 @@ function saveCookies(jar) {
1328
1356
  } catch (e) { }
1329
1357
  }
1330
1358
  });
1359
+
1360
+ // AUTO-PERSISTENCE: If ctx has appStatePath, save to disk immediately
1361
+ if (ctx && ctx.appStatePath) {
1362
+ saveAppState(ctx);
1363
+ }
1364
+
1331
1365
  return res;
1332
1366
  };
1333
1367
  }
1334
1368
 
1369
+ function saveAppState(ctx) {
1370
+ if (!ctx || !ctx.jar || !ctx.appStatePath) return;
1371
+ try {
1372
+ const appState = getAppState(ctx.jar);
1373
+ const fs = require('fs');
1374
+ const path = require('path');
1375
+
1376
+ // Ensure directory exists
1377
+ const dir = path.dirname(ctx.appStatePath);
1378
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1379
+
1380
+ fs.writeFileSync(ctx.appStatePath, JSON.stringify(appState, null, 2));
1381
+
1382
+ // Optional: Mirror to environment-ready file for cloud platforms
1383
+ const envPath = ctx.appStatePath.replace('.json', '.env.json');
1384
+ if (process.env.NEXUS_SYNC_ENV_FILE === '1') {
1385
+ fs.writeFileSync(envPath, JSON.stringify(appState));
1386
+ }
1387
+
1388
+ // EXTERNAL PERSISTENCE: If NEXUS_EXTERNAL_SAVE_URL is set, POST there
1389
+ if (process.env.NEXUS_EXTERNAL_SAVE_URL) {
1390
+ const axios = require('axios');
1391
+ axios.post(process.env.NEXUS_EXTERNAL_SAVE_URL, appState, {
1392
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'Nexus-FCA-Persistence' },
1393
+ timeout: 10000
1394
+ }).catch(err => {
1395
+ log.verbose("utils", "External appstate sync failed: " + err.message);
1396
+ });
1397
+ }
1398
+ } catch (e) {
1399
+ log.verbose("utils", "Auto-save appstate failed: " + e.message);
1400
+ }
1401
+ }
1402
+
1335
1403
  var NUM_TO_MONTH = [
1336
1404
  "Jan",
1337
1405
  "Feb",
@@ -1811,6 +1879,15 @@ module.exports = {
1811
1879
  setData_Path,
1812
1880
  getPaths,
1813
1881
  saveCookies,
1882
+ saveAppState,
1883
+ getAppState,
1884
+ getAppStateB64: function () {
1885
+ if (process.env.NEXUS_APPSTATE_B64) {
1886
+ try { return JSON.parse(Buffer.from(process.env.NEXUS_APPSTATE_B64, 'base64').toString('utf8')); }
1887
+ catch (e) { log.error("utils", "Failed to parse NEXUS_APPSTATE_B64"); return null; }
1888
+ }
1889
+ return null;
1890
+ },
1814
1891
  getType,
1815
1892
  _formatAttachment,
1816
1893
  formatHistoryMessage,
@@ -1827,6 +1904,7 @@ module.exports = {
1827
1904
  formatReadReceipt,
1828
1905
  formatRead,
1829
1906
  generatePresence,
1907
+ generateClientID,
1830
1908
  formatDate,
1831
1909
  decodeClientPayload,
1832
1910
  getAppState,