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 +31 -0
- package/index.js +20 -0
- package/lib/safety/FacebookSafety.js +246 -18
- package/package.json +1 -1
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
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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);
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
318
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|