nexus-fca 2.1.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.2] - Unreleased - CONTINUOUS IDLE RECOVERY
4
+ ### Added
5
+ - Soft-stale probing at 2 minutes idle (ping + conditional forced reconnect if no events within 5-8s)
6
+ - Wrapper around `listenMqtt` to automatically feed events into safety heartbeat (`recordEvent`) for precise idle detection
7
+
8
+ ### Improved
9
+ - Faster recovery from silent idle states (previously required >5 min or external trigger)
10
+ - Reduced chance of appearing online but unresponsive after short inactivity
11
+
12
+ ---
13
+
14
+ ## [2.1.1] - 2025-08-27 - ADVANCED SESSION STABILITY
15
+ ### 🛠 Added
16
+ - Adaptive safe session refresh interval (dynamic based on risk level)
17
+ - Heartbeat + watchdog timers to detect stale MQTT connections early
18
+ - Progressive backoff with jitter for MQTT reconnect attempts
19
+ - Layered post-refresh health checks (1s / 10s / 30s) to catch silent drops
20
+ - Abortable refresh with timeout safeguard (25s) to prevent hangs
21
+ - Automatic reconnection trigger if no events within thresholds (2m soft, 15m hard)
22
+ - `destroy()` method to cleanup timers/listeners (prevents memory leaks)
23
+
24
+ ### 🔄 Changed
25
+ - Safe refresh now records in‑flight ID and supersedes outdated checks
26
+ - Reconnect logic centralized in `_reconnectMqttWithBackoff`
27
+
28
+ ### ✅ Improved
29
+ - Stability after long runtimes / multiple token refresh cycles
30
+ - Reduced risk of listener not resuming after refresh
31
+
32
+ ---
33
+
3
34
  ## [2.1.0] - 2025-08-20 - SESSION RELIABILITY & PROMISE LOGIN
4
35
  ### 🚀 Highlights
5
36
  Stability-focused release improving long‑running bot sessions, reducing false `not_logged_in` events, and modernizing the login flow.
package/index.js CHANGED
@@ -239,6 +239,26 @@ function buildAPI(globalOptions, html, jar) {
239
239
  api[v.replace(".js", "")] = require("./src/" + v)(defaultFuncs, api, ctx);
240
240
  });
241
241
  api.listen = api.listenMqtt;
242
+ // Safety wrapper: ensure every inbound MQTT event updates safety lastEvent timestamp
243
+ if (!api._safetyWrappedListen) {
244
+ const _origListen = api.listenMqtt;
245
+ api.listenMqtt = function(callback) {
246
+ const wrapped = (err, evt) => {
247
+ if (!err && evt) {
248
+ try { globalSafety.recordEvent(); } catch(_) {}
249
+ }
250
+ if (typeof callback === 'function') callback(err, evt);
251
+ };
252
+ const emitter = _origListen(wrapped);
253
+ // Redundant defensive hooks
254
+ try {
255
+ emitter.on('message', () => globalSafety.recordEvent());
256
+ emitter.on('error', () => globalSafety.recordEvent());
257
+ } catch(_) {}
258
+ return emitter;
259
+ };
260
+ api._safetyWrappedListen = true;
261
+ }
242
262
  setInterval(async () => {
243
263
  api
244
264
  .refreshFb_dtsg()
@@ -53,6 +53,22 @@ class FacebookSafety {
53
53
  riskLevel: 'low'
54
54
  };
55
55
 
56
+ // Track last incoming event time to detect stale / dead connections
57
+ this._lastEventTs = Date.now();
58
+ this._reconnecting = false;
59
+ this._activeListenerStop = null; // store stop function from listenMqtt if we attach
60
+ this._safeRefreshInterval = null; // guard for multiple intervals
61
+ this._safeRefreshTimer = null; // for dynamic timeout pattern
62
+ // New stability / heartbeat fields
63
+ this._heartbeatTimer = null;
64
+ this._watchdogTimer = null;
65
+ this._backoff = { attempt: 0, next: 0 };
66
+ this._destroyed = false;
67
+ this._postRefreshChecks = [];
68
+ this._inFlightRefreshId = 0;
69
+ // New: probing guard to avoid overlapping soft-stale probes
70
+ this._probing = false;
71
+
56
72
  this.initSafety();
57
73
  }
58
74
 
@@ -220,14 +236,29 @@ class FacebookSafety {
220
236
  * Setup safe token refresh intervals
221
237
  */
222
238
  setupSafeRefresh() {
223
- // Refresh tokens every 45 minutes with randomization
224
- const baseInterval = 45 * 60 * 1000; // 45 minutes
225
- const randomVariation = Math.random() * 10 * 60 * 1000; // ±10 minutes
226
- const interval = baseInterval + randomVariation;
227
-
228
- setInterval(() => {
229
- this.refreshSafeSession();
230
- }, interval);
239
+ // Replace previous interval/timer to avoid stacking
240
+ if (this._safeRefreshInterval) {
241
+ clearInterval(this._safeRefreshInterval);
242
+ this._safeRefreshInterval = null;
243
+ }
244
+ if (this._safeRefreshTimer) {
245
+ clearTimeout(this._safeRefreshTimer);
246
+ this._safeRefreshTimer = null;
247
+ }
248
+ // Use recursive timeout with randomization each cycle (more human-like)
249
+ const schedule = () => {
250
+ if (this._destroyed) return;
251
+ // Adaptive interval: shorter if high risk (to revalidate), longer if stable
252
+ const base = this.sessionMetrics.riskLevel === 'high' ? 25 : this.sessionMetrics.riskLevel === 'medium' ? 35 : 45; // minutes
253
+ const baseInterval = base * 60 * 1000;
254
+ const randomVariation = (Math.random() * 16 - 8) * 60 * 1000; // ±8 min
255
+ const interval = baseInterval + randomVariation;
256
+ this._safeRefreshTimer = setTimeout(async () => {
257
+ await this.refreshSafeSession();
258
+ schedule();
259
+ }, Math.max(10 * 60 * 1000, interval)); // never below 10 min
260
+ };
261
+ schedule();
231
262
  }
232
263
 
233
264
  /**
@@ -265,18 +296,138 @@ class FacebookSafety {
265
296
  if (isError) {
266
297
  this.sessionMetrics.errorCount++;
267
298
  }
299
+ this._lastEventTs = Date.now();
300
+ }
301
+
302
+ // Expose a method for external caller (e.g., main listener) to update last event timestamp
303
+ recordEvent() {
304
+ this._lastEventTs = Date.now();
305
+ }
306
+
307
+ // Internal helper to ensure MQTT connection stays alive / auto-recover if dead after refresh
308
+ async _ensureMqttAlive() {
309
+ if (!this.api || this._destroyed) return;
310
+ try {
311
+ const now = Date.now();
312
+ const disconnected = !this.ctx || !this.ctx.mqttClient || !this.ctx.mqttClient.connected;
313
+ const idle = now - this._lastEventTs;
314
+ const softStale = idle > 2 * 60 * 1000; // >2 min no events
315
+ const hardStale = idle > 5 * 60 * 1000; // >5 min no events (legacy threshold)
316
+ const stale = hardStale; // backwards compat naming
317
+
318
+ // If totally disconnected or hard stale -> reconnect immediately
319
+ if (disconnected || stale) {
320
+ await this._reconnectMqttWithBackoff(disconnected ? 'disconnected' : 'hard-stale');
321
+ return;
322
+ }
323
+
324
+ // Soft-stale probing: connection claims to be open but no events for 2-5 minutes.
325
+ // We issue a ping and if still no events after probe window, we force a reconnect.
326
+ if (softStale && !this._probing) {
327
+ this._probing = true;
328
+ const prevTs = this._lastEventTs;
329
+ try {
330
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
331
+ if (typeof this.ctx.mqttClient.ping === 'function') {
332
+ try { this.ctx.mqttClient.ping(); } catch(_) {}
333
+ }
334
+ }
335
+ } catch(_) {}
336
+ setTimeout(() => {
337
+ if (this._destroyed) return;
338
+ // If no new events arrived since probe start, treat as latent-dead connection
339
+ if (this._lastEventTs <= prevTs) {
340
+ // Reset backoff to allow immediate reconnect (latency sensitive)
341
+ this._backoff.attempt = 0;
342
+ this._reconnectMqttWithBackoff('soft-stale');
343
+ }
344
+ this._probing = false;
345
+ }, 5000 + Math.random() * 3000); // 5-8s probe window
346
+ }
347
+ } catch (_) { /* swallow */ }
348
+ }
349
+
350
+ // Progressive backoff + jitter reconnect
351
+ async _reconnectMqttWithBackoff(reason) {
352
+ if (this._reconnecting || this._destroyed) return;
353
+ this._reconnecting = true;
354
+ try {
355
+ const now = Date.now();
356
+ if (now < this._backoff.next) {
357
+ return; // respect backoff window
358
+ }
359
+ const attempt = ++this._backoff.attempt;
360
+ const baseDelay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5))); // cap 30s
361
+ const jitter = Math.random() * 400;
362
+ const delay = baseDelay + jitter;
363
+ this._backoff.next = now + delay;
364
+ await new Promise(r => setTimeout(r, delay));
365
+ // Graceful stop old listener
366
+ if (this._activeListenerStop && typeof this._activeListenerStop === 'function') {
367
+ try { this._activeListenerStop(); } catch (_) {}
368
+ }
369
+ if (this.api && typeof this.api.listenMqtt === 'function' && !this._destroyed) {
370
+ const stop = this.api.listenMqtt((err, event) => {
371
+ if (!err && event) this.recordEvent();
372
+ });
373
+ this._activeListenerStop = stop;
374
+ if (attempt > 1) this.safetyEmit('mqttBackoff', { attempt, delay, reason });
375
+ else this.safetyEmit('mqttReconnect', { success: true, reason });
376
+ }
377
+ // Reset backoff on success detection soon after
378
+ setTimeout(() => {
379
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
380
+ this._backoff.attempt = 0;
381
+ }
382
+ }, 5000);
383
+ } catch (e) {
384
+ this.safetyEmit('mqttReconnect', { success: false, error: e.message });
385
+ } finally {
386
+ this._reconnecting = false;
387
+ }
388
+ }
389
+
390
+ // Heartbeat ping & watchdog
391
+ _startHeartbeat() {
392
+ if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
393
+ if (this._watchdogTimer) clearInterval(this._watchdogTimer);
394
+ if (this._destroyed) return;
395
+ this._heartbeatTimer = setInterval(() => {
396
+ if (this._destroyed) return;
397
+ try {
398
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
399
+ if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping();
400
+ this.safetyEmit('heartbeat', { ts: Date.now() });
401
+ }
402
+ } catch (_) {}
403
+ }, 60 * 1000 + Math.random() * 5000); // 60s ±5s
404
+ this._watchdogTimer = setInterval(() => {
405
+ if (this._destroyed) return;
406
+ const idle = Date.now() - this._lastEventTs;
407
+ if (idle > 2 * 60 * 1000) { // 2 min no events -> soft check
408
+ this._ensureMqttAlive();
409
+ }
410
+ if (idle > 15 * 60 * 1000) { // 15 min -> force reconnect attempt ignoring backoff
411
+ this._backoff.attempt = 0; // reset to allow immediate
412
+ this._ensureMqttAlive();
413
+ }
414
+ }, 30 * 1000); // watchdog every 30s
268
415
  }
269
416
 
270
417
  /**
271
418
  * Start safety monitoring for session
272
419
  */
273
- startMonitoring(ctx, api) {
420
+ startMonitoring(ctx, api) { // added persistence of ctx/api so refresh can use them
274
421
  if (!ctx || !api) return;
275
-
276
- // Monitor for account issues
277
- setInterval(() => {
422
+ this.ctx = ctx; // persist for later safe refresh
423
+ this.api = api;
424
+ if (this._monitorInterval) clearInterval(this._monitorInterval);
425
+ this._monitorInterval = setInterval(() => {
278
426
  this.checkAccountHealth(ctx, api);
279
- }, 30000); // Check every 30 seconds
427
+ }, 30000);
428
+ // Attach lightweight hook if api emits events to update lastEventTs externally if user wires it
429
+ this.recordEvent();
430
+ this._startHeartbeat();
280
431
  }
281
432
 
282
433
  /**
@@ -290,7 +441,7 @@ class FacebookSafety {
290
441
  const userCookie = cookies.find(c => c.key === 'c_user');
291
442
 
292
443
  if (!userCookie) {
293
- this.emit('accountIssue', {
444
+ this.safetyEmit('accountIssue', {
294
445
  type: 'session_expired',
295
446
  message: 'User session cookie missing'
296
447
  });
@@ -301,7 +452,7 @@ class FacebookSafety {
301
452
 
302
453
  const safetyCheck = this.checkErrorSafety(error);
303
454
  if (!safetyCheck.safe) {
304
- this.emit('accountIssue', {
455
+ this.safetyEmit('accountIssue', {
305
456
  type: safetyCheck.danger,
306
457
  message: error.message,
307
458
  recommendation: safetyCheck.recommendation
@@ -314,8 +465,85 @@ class FacebookSafety {
314
465
  * Refresh session safely
315
466
  */
316
467
  async refreshSafeSession() {
317
- // Implement safe session refresh logic
318
- console.log('🔄 Performing safe session refresh...');
468
+ // Improved safe session refresh implementation
469
+ if (this._refreshing) return; // prevent concurrent refreshes
470
+ this._refreshing = true;
471
+ const refreshId = ++this._inFlightRefreshId;
472
+ const startedAt = Date.now();
473
+ let preMqttConnected = this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected;
474
+ let preLastEvent = this._lastEventTs;
475
+ try {
476
+ console.log('🔄 Performing safe session refresh...');
477
+ if (!this.api || typeof this.api.refreshFb_dtsg !== 'function') {
478
+ console.log('⚠️ Safe refresh skipped: api.refreshFb_dtsg not available');
479
+ return;
480
+ }
481
+ // Abort protection if takes too long (network hang)
482
+ const timeoutMs = 25 * 1000;
483
+ const controller = new AbortController();
484
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
485
+ let res;
486
+ try {
487
+ res = await this.api.refreshFb_dtsg({ signal: controller.signal });
488
+ } finally { clearTimeout(timeout); }
489
+ this.sessionMetrics.errorCount = Math.max(0, this.sessionMetrics.errorCount - 1);
490
+ this.sessionMetrics.lastActivity = Date.now();
491
+ this.safetyEmit('safeRefresh', {
492
+ ok: true,
493
+ fb_dtsg: this.ctx && this.ctx.fb_dtsg,
494
+ jazoest: this.ctx && this.ctx.jazoest,
495
+ durationMs: Date.now() - startedAt,
496
+ message: 'Session tokens refreshed'
497
+ });
498
+ // Immediate MQTT health ensure
499
+ await this._ensureMqttAlive();
500
+ // Schedule layered post-refresh checks (1s, 10s, 30s) to catch silent drops
501
+ const checksAt = [1000, 10000, 30000];
502
+ checksAt.forEach(delay => {
503
+ const handle = setTimeout(() => {
504
+ if (this._destroyed) return;
505
+ if (refreshId !== this._inFlightRefreshId) return; // newer refresh superseded
506
+ this._ensureMqttAlive();
507
+ }, delay);
508
+ this._postRefreshChecks.push(handle);
509
+ });
510
+ // If previously connected and now no events for >1 min after refresh -> reconnect
511
+ setTimeout(() => {
512
+ if (this._destroyed) return;
513
+ if (preMqttConnected && Date.now() - Math.max(this._lastEventTs, preLastEvent) > 60 * 1000) {
514
+ this._backoff.attempt = 0; // reset backoff for immediate action
515
+ this._ensureMqttAlive();
516
+ }
517
+ }, 60 * 1000);
518
+ } catch (e) {
519
+ this.recordRequest(true);
520
+ this.safetyEmit('safeRefresh', {
521
+ ok: false,
522
+ error: e.message,
523
+ durationMs: Date.now() - startedAt
524
+ });
525
+ if (this.sessionMetrics.errorCount > 3) {
526
+ this.sessionMetrics.riskLevel = 'high';
527
+ }
528
+ // Force reconnection attempt if refresh failed & potential token invalidation
529
+ this._backoff.attempt = 0;
530
+ await this._ensureMqttAlive();
531
+ } finally {
532
+ this._refreshing = false;
533
+ }
534
+ }
535
+
536
+ // Cleanup / destroy resources (to prevent dangling timers)
537
+ destroy() {
538
+ this._destroyed = true;
539
+ const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer];
540
+ timers.forEach(t => t && clearTimeout(t));
541
+ if (this._activeListenerStop) {
542
+ try { this._activeListenerStop(); } catch (_) {}
543
+ this._activeListenerStop = null;
544
+ }
545
+ this._postRefreshChecks.forEach(h => clearTimeout(h));
546
+ this._postRefreshChecks = [];
319
547
  }
320
548
 
321
549
  /**
@@ -351,7 +579,7 @@ class FacebookSafety {
351
579
  /**
352
580
  * Emit safety events
353
581
  */
354
- emit(event, data) {
582
+ safetyEmit(event, data) {
355
583
  if (typeof this.onSafetyEvent === 'function') {
356
584
  this.onSafetyEvent(event, data);
357
585
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "A modern, safe, and advanced Facebook Chat API for Node.js with fully integrated Nexus Login System. NPM-ready with ID/password/2FA support, ultra-low ban rate protection, and zero external dependencies.",
5
5
  "main": "index.js",
6
6
  "repository": {