nexus-fca 2.1.0 → 2.1.1
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 +20 -0
- package/lib/safety/FacebookSafety.js +213 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.1] - 2025-08-27 - ADVANCED SESSION STABILITY
|
|
4
|
+
### 🛠 Added
|
|
5
|
+
- Adaptive safe session refresh interval (dynamic based on risk level)
|
|
6
|
+
- Heartbeat + watchdog timers to detect stale MQTT connections early
|
|
7
|
+
- Progressive backoff with jitter for MQTT reconnect attempts
|
|
8
|
+
- Layered post-refresh health checks (1s / 10s / 30s) to catch silent drops
|
|
9
|
+
- Abortable refresh with timeout safeguard (25s) to prevent hangs
|
|
10
|
+
- Automatic reconnection trigger if no events within thresholds (2m soft, 15m hard)
|
|
11
|
+
- `destroy()` method to cleanup timers/listeners (prevents memory leaks)
|
|
12
|
+
|
|
13
|
+
### 🔄 Changed
|
|
14
|
+
- Safe refresh now records in‑flight ID and supersedes outdated checks
|
|
15
|
+
- Reconnect logic centralized in `_reconnectMqttWithBackoff`
|
|
16
|
+
|
|
17
|
+
### ✅ Improved
|
|
18
|
+
- Stability after long runtimes / multiple token refresh cycles
|
|
19
|
+
- Reduced risk of listener not resuming after refresh
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
3
23
|
## [2.1.0] - 2025-08-20 - SESSION RELIABILITY & PROMISE LOGIN
|
|
4
24
|
### 🚀 Highlights
|
|
5
25
|
Stability-focused release improving long‑running bot sessions, reducing false `not_logged_in` events, and modernizing the login flow.
|
|
@@ -53,6 +53,20 @@ 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
|
+
|
|
56
70
|
this.initSafety();
|
|
57
71
|
}
|
|
58
72
|
|
|
@@ -220,14 +234,29 @@ class FacebookSafety {
|
|
|
220
234
|
* Setup safe token refresh intervals
|
|
221
235
|
*/
|
|
222
236
|
setupSafeRefresh() {
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
|
|
237
|
+
// Replace previous interval/timer to avoid stacking
|
|
238
|
+
if (this._safeRefreshInterval) {
|
|
239
|
+
clearInterval(this._safeRefreshInterval);
|
|
240
|
+
this._safeRefreshInterval = null;
|
|
241
|
+
}
|
|
242
|
+
if (this._safeRefreshTimer) {
|
|
243
|
+
clearTimeout(this._safeRefreshTimer);
|
|
244
|
+
this._safeRefreshTimer = null;
|
|
245
|
+
}
|
|
246
|
+
// Use recursive timeout with randomization each cycle (more human-like)
|
|
247
|
+
const schedule = () => {
|
|
248
|
+
if (this._destroyed) return;
|
|
249
|
+
// Adaptive interval: shorter if high risk (to revalidate), longer if stable
|
|
250
|
+
const base = this.sessionMetrics.riskLevel === 'high' ? 25 : this.sessionMetrics.riskLevel === 'medium' ? 35 : 45; // minutes
|
|
251
|
+
const baseInterval = base * 60 * 1000;
|
|
252
|
+
const randomVariation = (Math.random() * 16 - 8) * 60 * 1000; // ±8 min
|
|
253
|
+
const interval = baseInterval + randomVariation;
|
|
254
|
+
this._safeRefreshTimer = setTimeout(async () => {
|
|
255
|
+
await this.refreshSafeSession();
|
|
256
|
+
schedule();
|
|
257
|
+
}, Math.max(10 * 60 * 1000, interval)); // never below 10 min
|
|
258
|
+
};
|
|
259
|
+
schedule();
|
|
231
260
|
}
|
|
232
261
|
|
|
233
262
|
/**
|
|
@@ -265,18 +294,107 @@ class FacebookSafety {
|
|
|
265
294
|
if (isError) {
|
|
266
295
|
this.sessionMetrics.errorCount++;
|
|
267
296
|
}
|
|
297
|
+
this._lastEventTs = Date.now();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Expose a method for external caller (e.g., main listener) to update last event timestamp
|
|
301
|
+
recordEvent() {
|
|
302
|
+
this._lastEventTs = Date.now();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Internal helper to ensure MQTT connection stays alive / auto-recover if dead after refresh
|
|
306
|
+
async _ensureMqttAlive() {
|
|
307
|
+
if (!this.api || this._destroyed) return;
|
|
308
|
+
try {
|
|
309
|
+
const disconnected = !this.ctx || !this.ctx.mqttClient || !this.ctx.mqttClient.connected;
|
|
310
|
+
const stale = Date.now() - this._lastEventTs > 5 * 60 * 1000; // >5 min no events
|
|
311
|
+
if (disconnected || stale) {
|
|
312
|
+
await this._reconnectMqttWithBackoff(disconnected ? 'disconnected' : 'stale');
|
|
313
|
+
}
|
|
314
|
+
} catch (_) { /* swallow */ }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Progressive backoff + jitter reconnect
|
|
318
|
+
async _reconnectMqttWithBackoff(reason) {
|
|
319
|
+
if (this._reconnecting || this._destroyed) return;
|
|
320
|
+
this._reconnecting = true;
|
|
321
|
+
try {
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
if (now < this._backoff.next) {
|
|
324
|
+
return; // respect backoff window
|
|
325
|
+
}
|
|
326
|
+
const attempt = ++this._backoff.attempt;
|
|
327
|
+
const baseDelay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5))); // cap 30s
|
|
328
|
+
const jitter = Math.random() * 400;
|
|
329
|
+
const delay = baseDelay + jitter;
|
|
330
|
+
this._backoff.next = now + delay;
|
|
331
|
+
await new Promise(r => setTimeout(r, delay));
|
|
332
|
+
// Graceful stop old listener
|
|
333
|
+
if (this._activeListenerStop && typeof this._activeListenerStop === 'function') {
|
|
334
|
+
try { this._activeListenerStop(); } catch (_) {}
|
|
335
|
+
}
|
|
336
|
+
if (this.api && typeof this.api.listenMqtt === 'function' && !this._destroyed) {
|
|
337
|
+
const stop = this.api.listenMqtt((err, event) => {
|
|
338
|
+
if (!err && event) this.recordEvent();
|
|
339
|
+
});
|
|
340
|
+
this._activeListenerStop = stop;
|
|
341
|
+
if (attempt > 1) this.safetyEmit('mqttBackoff', { attempt, delay, reason });
|
|
342
|
+
else this.safetyEmit('mqttReconnect', { success: true, reason });
|
|
343
|
+
}
|
|
344
|
+
// Reset backoff on success detection soon after
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
347
|
+
this._backoff.attempt = 0;
|
|
348
|
+
}
|
|
349
|
+
}, 5000);
|
|
350
|
+
} catch (e) {
|
|
351
|
+
this.safetyEmit('mqttReconnect', { success: false, error: e.message });
|
|
352
|
+
} finally {
|
|
353
|
+
this._reconnecting = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Heartbeat ping & watchdog
|
|
358
|
+
_startHeartbeat() {
|
|
359
|
+
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
360
|
+
if (this._watchdogTimer) clearInterval(this._watchdogTimer);
|
|
361
|
+
if (this._destroyed) return;
|
|
362
|
+
this._heartbeatTimer = setInterval(() => {
|
|
363
|
+
if (this._destroyed) return;
|
|
364
|
+
try {
|
|
365
|
+
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
366
|
+
if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping();
|
|
367
|
+
this.safetyEmit('heartbeat', { ts: Date.now() });
|
|
368
|
+
}
|
|
369
|
+
} catch (_) {}
|
|
370
|
+
}, 60 * 1000 + Math.random() * 5000); // 60s ±5s
|
|
371
|
+
this._watchdogTimer = setInterval(() => {
|
|
372
|
+
if (this._destroyed) return;
|
|
373
|
+
const idle = Date.now() - this._lastEventTs;
|
|
374
|
+
if (idle > 2 * 60 * 1000) { // 2 min no events -> soft check
|
|
375
|
+
this._ensureMqttAlive();
|
|
376
|
+
}
|
|
377
|
+
if (idle > 15 * 60 * 1000) { // 15 min -> force reconnect attempt ignoring backoff
|
|
378
|
+
this._backoff.attempt = 0; // reset to allow immediate
|
|
379
|
+
this._ensureMqttAlive();
|
|
380
|
+
}
|
|
381
|
+
}, 30 * 1000); // watchdog every 30s
|
|
268
382
|
}
|
|
269
383
|
|
|
270
384
|
/**
|
|
271
385
|
* Start safety monitoring for session
|
|
272
386
|
*/
|
|
273
|
-
startMonitoring(ctx, api) {
|
|
387
|
+
startMonitoring(ctx, api) { // added persistence of ctx/api so refresh can use them
|
|
274
388
|
if (!ctx || !api) return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
389
|
+
this.ctx = ctx; // persist for later safe refresh
|
|
390
|
+
this.api = api;
|
|
391
|
+
if (this._monitorInterval) clearInterval(this._monitorInterval);
|
|
392
|
+
this._monitorInterval = setInterval(() => {
|
|
278
393
|
this.checkAccountHealth(ctx, api);
|
|
279
|
-
}, 30000);
|
|
394
|
+
}, 30000);
|
|
395
|
+
// Attach lightweight hook if api emits events to update lastEventTs externally if user wires it
|
|
396
|
+
this.recordEvent();
|
|
397
|
+
this._startHeartbeat();
|
|
280
398
|
}
|
|
281
399
|
|
|
282
400
|
/**
|
|
@@ -290,7 +408,7 @@ class FacebookSafety {
|
|
|
290
408
|
const userCookie = cookies.find(c => c.key === 'c_user');
|
|
291
409
|
|
|
292
410
|
if (!userCookie) {
|
|
293
|
-
this.
|
|
411
|
+
this.safetyEmit('accountIssue', {
|
|
294
412
|
type: 'session_expired',
|
|
295
413
|
message: 'User session cookie missing'
|
|
296
414
|
});
|
|
@@ -301,7 +419,7 @@ class FacebookSafety {
|
|
|
301
419
|
|
|
302
420
|
const safetyCheck = this.checkErrorSafety(error);
|
|
303
421
|
if (!safetyCheck.safe) {
|
|
304
|
-
this.
|
|
422
|
+
this.safetyEmit('accountIssue', {
|
|
305
423
|
type: safetyCheck.danger,
|
|
306
424
|
message: error.message,
|
|
307
425
|
recommendation: safetyCheck.recommendation
|
|
@@ -314,8 +432,85 @@ class FacebookSafety {
|
|
|
314
432
|
* Refresh session safely
|
|
315
433
|
*/
|
|
316
434
|
async refreshSafeSession() {
|
|
317
|
-
//
|
|
318
|
-
|
|
435
|
+
// Improved safe session refresh implementation
|
|
436
|
+
if (this._refreshing) return; // prevent concurrent refreshes
|
|
437
|
+
this._refreshing = true;
|
|
438
|
+
const refreshId = ++this._inFlightRefreshId;
|
|
439
|
+
const startedAt = Date.now();
|
|
440
|
+
let preMqttConnected = this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected;
|
|
441
|
+
let preLastEvent = this._lastEventTs;
|
|
442
|
+
try {
|
|
443
|
+
console.log('🔄 Performing safe session refresh...');
|
|
444
|
+
if (!this.api || typeof this.api.refreshFb_dtsg !== 'function') {
|
|
445
|
+
console.log('⚠️ Safe refresh skipped: api.refreshFb_dtsg not available');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Abort protection if takes too long (network hang)
|
|
449
|
+
const timeoutMs = 25 * 1000;
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
452
|
+
let res;
|
|
453
|
+
try {
|
|
454
|
+
res = await this.api.refreshFb_dtsg({ signal: controller.signal });
|
|
455
|
+
} finally { clearTimeout(timeout); }
|
|
456
|
+
this.sessionMetrics.errorCount = Math.max(0, this.sessionMetrics.errorCount - 1);
|
|
457
|
+
this.sessionMetrics.lastActivity = Date.now();
|
|
458
|
+
this.safetyEmit('safeRefresh', {
|
|
459
|
+
ok: true,
|
|
460
|
+
fb_dtsg: this.ctx && this.ctx.fb_dtsg,
|
|
461
|
+
jazoest: this.ctx && this.ctx.jazoest,
|
|
462
|
+
durationMs: Date.now() - startedAt,
|
|
463
|
+
message: 'Session tokens refreshed'
|
|
464
|
+
});
|
|
465
|
+
// Immediate MQTT health ensure
|
|
466
|
+
await this._ensureMqttAlive();
|
|
467
|
+
// Schedule layered post-refresh checks (1s, 10s, 30s) to catch silent drops
|
|
468
|
+
const checksAt = [1000, 10000, 30000];
|
|
469
|
+
checksAt.forEach(delay => {
|
|
470
|
+
const handle = setTimeout(() => {
|
|
471
|
+
if (this._destroyed) return;
|
|
472
|
+
if (refreshId !== this._inFlightRefreshId) return; // newer refresh superseded
|
|
473
|
+
this._ensureMqttAlive();
|
|
474
|
+
}, delay);
|
|
475
|
+
this._postRefreshChecks.push(handle);
|
|
476
|
+
});
|
|
477
|
+
// If previously connected and now no events for >1 min after refresh -> reconnect
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
if (this._destroyed) return;
|
|
480
|
+
if (preMqttConnected && Date.now() - Math.max(this._lastEventTs, preLastEvent) > 60 * 1000) {
|
|
481
|
+
this._backoff.attempt = 0; // reset backoff for immediate action
|
|
482
|
+
this._ensureMqttAlive();
|
|
483
|
+
}
|
|
484
|
+
}, 60 * 1000);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
this.recordRequest(true);
|
|
487
|
+
this.safetyEmit('safeRefresh', {
|
|
488
|
+
ok: false,
|
|
489
|
+
error: e.message,
|
|
490
|
+
durationMs: Date.now() - startedAt
|
|
491
|
+
});
|
|
492
|
+
if (this.sessionMetrics.errorCount > 3) {
|
|
493
|
+
this.sessionMetrics.riskLevel = 'high';
|
|
494
|
+
}
|
|
495
|
+
// Force reconnection attempt if refresh failed & potential token invalidation
|
|
496
|
+
this._backoff.attempt = 0;
|
|
497
|
+
await this._ensureMqttAlive();
|
|
498
|
+
} finally {
|
|
499
|
+
this._refreshing = false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Cleanup / destroy resources (to prevent dangling timers)
|
|
504
|
+
destroy() {
|
|
505
|
+
this._destroyed = true;
|
|
506
|
+
const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer];
|
|
507
|
+
timers.forEach(t => t && clearTimeout(t));
|
|
508
|
+
if (this._activeListenerStop) {
|
|
509
|
+
try { this._activeListenerStop(); } catch (_) {}
|
|
510
|
+
this._activeListenerStop = null;
|
|
511
|
+
}
|
|
512
|
+
this._postRefreshChecks.forEach(h => clearTimeout(h));
|
|
513
|
+
this._postRefreshChecks = [];
|
|
319
514
|
}
|
|
320
515
|
|
|
321
516
|
/**
|
|
@@ -351,7 +546,7 @@ class FacebookSafety {
|
|
|
351
546
|
/**
|
|
352
547
|
* Emit safety events
|
|
353
548
|
*/
|
|
354
|
-
|
|
549
|
+
safetyEmit(event, data) {
|
|
355
550
|
if (typeof this.onSafetyEvent === 'function') {
|
|
356
551
|
this.onSafetyEvent(event, data);
|
|
357
552
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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": {
|