baileys-antiban 4.6.0 → 4.8.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/README.md +163 -2
- package/dist/antiban.d.ts +19 -0
- package/dist/antiban.js +49 -0
- package/dist/cjs/antiban.js +49 -0
- package/dist/cjs/fleetEventStore.js +162 -0
- package/dist/cjs/humanEntropy.js +342 -0
- package/dist/cjs/index.js +18 -1
- package/dist/cjs/jidCircuitBreaker.js +157 -0
- package/dist/cjs/messageTypeRegistry.js +373 -0
- package/dist/cjs/stateExport.js +153 -0
- package/dist/cjs/warmup.js +6 -0
- package/dist/cjs/wrapper.js +73 -1
- package/dist/fleetEventStore.d.ts +65 -0
- package/dist/fleetEventStore.js +157 -0
- package/dist/humanEntropy.d.ts +133 -0
- package/dist/humanEntropy.js +337 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/jidCircuitBreaker.d.ts +48 -0
- package/dist/jidCircuitBreaker.js +152 -0
- package/dist/messageTypeRegistry.d.ts +145 -0
- package/dist/messageTypeRegistry.js +369 -0
- package/dist/stateExport.d.ts +145 -0
- package/dist/stateExport.js +149 -0
- package/dist/warmup.d.ts +4 -0
- package/dist/warmup.js +6 -0
- package/dist/wrapper.d.ts +12 -0
- package/dist/wrapper.js +73 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
**Drop-in anti-ban middleware for Baileys WhatsApp bots. Free, self-hosted, TypeScript-first. Whapi.Cloud alternative — zero monthly fees.**
|
|
10
10
|
|
|
11
|
-
> Rate limiting with Gaussian jitter, 7-day warmup, session health monitoring, LID resolver, disconnect classification, contact graph enforcement — all in one `npm install`. Works with [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
|
|
11
|
+
> Rate limiting with Gaussian jitter, 7-day warmup, session health monitoring, LID resolver, disconnect classification, contact graph enforcement, device fingerprinting, group operation guards, recovery orchestration, cross-instance coordination — all in one `npm install`. Works with [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
|
|
12
12
|
|
|
13
|
-
> **New in
|
|
13
|
+
> **New in v4.7:** HumanEntropyService — background human-like activity (typing indicators, delayed read receipts, presence cycles) to prevent "too perfect bot" detection. Works with WaSP and any session manager, no socket access needed.
|
|
14
14
|
|
|
15
15
|
## Why Trust This Package
|
|
16
16
|
|
|
@@ -27,6 +27,167 @@ The npm WhatsApp ecosystem has a malware problem. In April 2026, [`lotusbail`](h
|
|
|
27
27
|
|
|
28
28
|
If you can't read the code yourself, lean on these signals: signed releases, public audit trail, no telemetry, and a real product behind it. That's the floor. Everything below is the feature set.
|
|
29
29
|
|
|
30
|
+
## v4.x New Features — Production-Grade Ban Prevention
|
|
31
|
+
|
|
32
|
+
v4.0–v4.7 ship seven major anti-ban modules. All are **auto-wired** by default via `wrapSocket()` or `wrapSocketWithFingerprint()`.
|
|
33
|
+
|
|
34
|
+
### v4.0 — GroupOperationGuard
|
|
35
|
+
|
|
36
|
+
Rate-limits group operations to prevent `account_reachout_restricted` errors. WA limits: ~3 adds/10min, 2 creates/10min.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { wrapSocket } from 'baileys-antiban';
|
|
40
|
+
|
|
41
|
+
const sock = wrapSocket(makeWASocket({ ... }), {
|
|
42
|
+
groupOpGuard: { limits: { add: { max: 3, windowMs: 600_000 } } },
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Classify errors: `classifyGroupOpError(err)` returns `GROUP_OP_ERRORS.REACHOUT_RESTRICTED` | `RATE_OVERLIMIT` | `PRIVACY_BLOCK` | etc.
|
|
47
|
+
Disable: `groupOpGuard: false`
|
|
48
|
+
|
|
49
|
+
### v4.1 — LegitimacySignalInjector
|
|
50
|
+
|
|
51
|
+
Injects realistic imperfections: typos + corrections (2.5% of messages), read gaps, mid-typing pauses. WhatsApp's ML flags accounts that are "too perfect".
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const sock = wrapSocket(makeWASocket({ ... }), {
|
|
55
|
+
legitimacySignals: { typoProbability: 0.03 },
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Disable: `legitimacySignals: false`
|
|
60
|
+
|
|
61
|
+
### v4.2 — BanRecoveryOrchestrator
|
|
62
|
+
|
|
63
|
+
Structured recovery after ban events. Auto-triggers via HealthMonitor at critical risk.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { BanRecoveryOrchestrator } from 'baileys-antiban';
|
|
67
|
+
|
|
68
|
+
const recovery = new BanRecoveryOrchestrator({
|
|
69
|
+
onPhaseChange: (phase, plan) => console.log(`Phase: ${phase}`),
|
|
70
|
+
onHardBan: () => { /* replace SIM */ },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
recovery.triggerRecovery('timelock');
|
|
74
|
+
const status = recovery.getStatus();
|
|
75
|
+
console.log(status.rateMultiplier); // 0.1 = 10% speed
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Recovery plans:** timelock: 24h pause, 10% resume, 15%/week ramp | rate_overlimit: 4h, 25%, 25%/week | soft_ban: 48h, 5%, 10%/week | hard_ban: dead
|
|
79
|
+
Access via: `antiban.recoveryOrchestrator`
|
|
80
|
+
|
|
81
|
+
### v4.3 — wrapSocketWithFingerprint
|
|
82
|
+
|
|
83
|
+
One-call setup with device fingerprint randomization (appVersion, osVersion, deviceModel).
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { wrapSocketWithFingerprint } from 'baileys-antiban';
|
|
87
|
+
|
|
88
|
+
const sock = wrapSocketWithFingerprint(makeWASocket, { auth }, { preset: 'moderate' });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Preset changes:** All presets default `groupProfiles: true`; `aggressive`/`high-volume` now `autoPauseAt: 'high'`
|
|
92
|
+
|
|
93
|
+
### v4.4 — Per-Contact Risk Delays + DeliveryTracker
|
|
94
|
+
|
|
95
|
+
Strangers get 2.5× delay, known contacts 1.0×. Active when `contactGraph.enabled: true`.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const sock = wrapSocket(makeWASocket({ ... }), { contactGraph: { enabled: true } });
|
|
99
|
+
// stranger: 2.5×, handshake_sent: 1.8×, handshake_complete: 1.3×, known: 1.0×
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**DeliveryTracker** tracks double-tick receipts. <60% delivery = soft-ban signal.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { DeliveryTracker } from 'baileys-antiban';
|
|
106
|
+
|
|
107
|
+
const tracker = new DeliveryTracker({
|
|
108
|
+
lowRateThreshold: 0.6,
|
|
109
|
+
onLowDeliveryRate: (rate) => console.error(`Delivery: ${rate * 100}%`),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
sock.ev.on('messages.upsert', ({ messages }) => {
|
|
113
|
+
messages.forEach(m => m.key.fromMe && tracker.onMessageSent(m.key.id));
|
|
114
|
+
});
|
|
115
|
+
sock.ev.on('messages.update', (updates) => {
|
|
116
|
+
updates.forEach(({ key, update }) => {
|
|
117
|
+
if (update.status >= 3) tracker.onDeliveryReceipt(key.id);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### v4.5 — Adaptive Rate Limiting
|
|
123
|
+
|
|
124
|
+
Auto-adjusts rate based on delivery success: ≥85% → 100% speed, <55% → 25% speed. Auto-wired.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { RateLimiter } from 'baileys-antiban';
|
|
128
|
+
|
|
129
|
+
const limiter = new RateLimiter({ maxPerMinute: 10 });
|
|
130
|
+
limiter.adaptLimits(0.5); // manual throttle to 50%
|
|
131
|
+
const factor = limiter.getCurrentFactor();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### v4.6 — Cross-Instance Coordination
|
|
135
|
+
|
|
136
|
+
Shared token bucket across processes. Solves: 5 bots × 8/min = 40/min → flag.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const sock = wrapSocket(makeWASocket({ ... }), {
|
|
140
|
+
instanceCoordinator: '/tmp/wa-pool.json',
|
|
141
|
+
instancePoolMaxPerMinute: 20,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
All instances share 20/min IP-level budget. Atomic writes via rename-swap.
|
|
146
|
+
|
|
147
|
+
### v4.7 — HumanEntropyService
|
|
148
|
+
|
|
149
|
+
Background noise generator that makes a WA session indistinguishable from a real human user during idle periods. Runs independently of your message flow — no socket access required, works with WaSP or any session manager.
|
|
150
|
+
|
|
151
|
+
**The problem it solves:** WhatsApp's ML flags accounts with "too perfect" patterns — instant read receipts, zero typing activity, always-on presence. A listen-only bot that never idles looks like a bot.
|
|
152
|
+
|
|
153
|
+
**What it does every 2-6 hours (randomized):**
|
|
154
|
+
- Sends typing indicator to a recent contact for 3-8 seconds, then stops (mimics "started typing, changed mind")
|
|
155
|
+
- Marks a received message as read with 10-60 min delay (mimics "opened notification, read later")
|
|
156
|
+
- Toggles own presence available → unavailable over 30-120s (mimics "checked phone, put it down")
|
|
157
|
+
|
|
158
|
+
**Safety:** Only contacts people who already messaged you first. Never cold-contacts strangers. All errors caught silently.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { createHumanEntropyService } from 'baileys-antiban';
|
|
162
|
+
|
|
163
|
+
// Works with WaSP (no direct socket access needed)
|
|
164
|
+
const entropy = createHumanEntropyService(wasp, sessionId, {
|
|
165
|
+
enabled: true,
|
|
166
|
+
minIntervalMs: 2 * 60 * 60 * 1000, // 2 hours
|
|
167
|
+
maxIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
entropy.start(); // runs in background
|
|
171
|
+
entropy.stop(); // call on shutdown
|
|
172
|
+
|
|
173
|
+
// Or with direct Baileys socket
|
|
174
|
+
import { HumanEntropyService } from 'baileys-antiban';
|
|
175
|
+
const svc = new HumanEntropyService(socket, { enabled: true });
|
|
176
|
+
svc.start();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Tracking recent contacts:** Feed incoming messages so the service knows who to interact with:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
sock.ev.on('messages.upsert', ({ messages }) => {
|
|
183
|
+
messages.forEach(m => entropy.addRecentContact(m.key.remoteJid, m.key));
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Stats: `entropy.getStats()` returns `{ typingActions, readActions, presenceActions, cycles }`.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
30
191
|
## v2.0 New Features — Session Stability Module
|
|
31
192
|
|
|
32
193
|
### What's New in v2.0
|
package/dist/antiban.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { BanRecoveryOrchestrator, type RecoveryStatus } from './banRecoveryOrche
|
|
|
29
29
|
import { type AntiBanInput, type ResolvedConfig } from './presets.js';
|
|
30
30
|
import { type DeliveryTrackerStats } from './deliveryTracker.js';
|
|
31
31
|
import { type InstanceCoordinatorStats } from './instanceCoordinator.js';
|
|
32
|
+
import { MessageTypeRegistry } from './messageTypeRegistry.js';
|
|
33
|
+
import { type AntibanSnapshot } from './stateExport.js';
|
|
32
34
|
export interface AntiBanConfigLegacy {
|
|
33
35
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
34
36
|
warmUp?: Partial<WarmUpConfig>;
|
|
@@ -76,6 +78,10 @@ export interface AntiBanStats {
|
|
|
76
78
|
banRecovery?: RecoveryStatus | null;
|
|
77
79
|
deliveryTracker: DeliveryTrackerStats;
|
|
78
80
|
instanceCoordinator?: InstanceCoordinatorStats | null;
|
|
81
|
+
messageRegistry?: {
|
|
82
|
+
typeCount: number;
|
|
83
|
+
warningCount: number;
|
|
84
|
+
} | null;
|
|
79
85
|
}
|
|
80
86
|
export declare class AntiBan {
|
|
81
87
|
private rateLimiter;
|
|
@@ -93,6 +99,7 @@ export declare class AntiBan {
|
|
|
93
99
|
private banRecovery;
|
|
94
100
|
private deliveryTracker;
|
|
95
101
|
private instanceCoordinator;
|
|
102
|
+
private messageTypeRegistry;
|
|
96
103
|
private stateManager;
|
|
97
104
|
private resolvedConfig;
|
|
98
105
|
private logging;
|
|
@@ -161,6 +168,8 @@ export declare class AntiBan {
|
|
|
161
168
|
get sessionStability(): SessionHealthMonitor | null;
|
|
162
169
|
/** Get the ban recovery orchestrator for direct access */
|
|
163
170
|
get recoveryOrchestrator(): BanRecoveryOrchestrator;
|
|
171
|
+
/** Get the message type registry for direct access */
|
|
172
|
+
get messageRegistry(): MessageTypeRegistry | null;
|
|
164
173
|
/**
|
|
165
174
|
* Export warm-up state for persistence between restarts
|
|
166
175
|
*/
|
|
@@ -180,6 +189,16 @@ export declare class AntiBan {
|
|
|
180
189
|
private runAdaptiveCheck;
|
|
181
190
|
private persistStateDebounced;
|
|
182
191
|
private persistStateImmediate;
|
|
192
|
+
/**
|
|
193
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
194
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
195
|
+
*/
|
|
196
|
+
exportState(): AntibanSnapshot;
|
|
197
|
+
/**
|
|
198
|
+
* Import unified state snapshot.
|
|
199
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
200
|
+
*/
|
|
201
|
+
importState(snapshot: AntibanSnapshot): void;
|
|
183
202
|
/**
|
|
184
203
|
* Clean up all timers and resources.
|
|
185
204
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
package/dist/antiban.js
CHANGED
|
@@ -31,6 +31,8 @@ import { StateManager } from './persist.js';
|
|
|
31
31
|
import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
|
|
32
32
|
import { DeliveryTracker } from './deliveryTracker.js';
|
|
33
33
|
import { InstanceCoordinator } from './instanceCoordinator.js';
|
|
34
|
+
import { MessageTypeRegistry } from './messageTypeRegistry.js';
|
|
35
|
+
import { exportAntibanState, importAntibanState } from './stateExport.js';
|
|
34
36
|
function isLegacyConfig(cfg) {
|
|
35
37
|
if (typeof cfg !== 'object' || cfg === null)
|
|
36
38
|
return false;
|
|
@@ -109,6 +111,7 @@ export class AntiBan {
|
|
|
109
111
|
banRecovery;
|
|
110
112
|
deliveryTracker;
|
|
111
113
|
instanceCoordinator = null;
|
|
114
|
+
messageTypeRegistry = null;
|
|
112
115
|
stateManager = null;
|
|
113
116
|
resolvedConfig;
|
|
114
117
|
logging;
|
|
@@ -297,6 +300,13 @@ export class AntiBan {
|
|
|
297
300
|
console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
|
|
298
301
|
}
|
|
299
302
|
}
|
|
303
|
+
// Initialize message type registry if configured
|
|
304
|
+
if (cfg.messageTypeRegistry) {
|
|
305
|
+
this.messageTypeRegistry = new MessageTypeRegistry();
|
|
306
|
+
if (this.logging) {
|
|
307
|
+
console.log(`[baileys-antiban] 📝 Message type registry enabled`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
300
310
|
}
|
|
301
311
|
/**
|
|
302
312
|
* Check if a message can be sent and get required delay.
|
|
@@ -597,6 +607,13 @@ export class AntiBan {
|
|
|
597
607
|
if (this.instanceCoordinator) {
|
|
598
608
|
stats.instanceCoordinator = this.instanceCoordinator.getStats();
|
|
599
609
|
}
|
|
610
|
+
if (this.messageTypeRegistry) {
|
|
611
|
+
const warnings = this.messageTypeRegistry.getWarnings();
|
|
612
|
+
stats.messageRegistry = {
|
|
613
|
+
typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
|
|
614
|
+
warningCount: warnings.length,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
600
617
|
return stats;
|
|
601
618
|
}
|
|
602
619
|
/** Get the timelock guard for direct access */
|
|
@@ -639,6 +656,10 @@ export class AntiBan {
|
|
|
639
656
|
get recoveryOrchestrator() {
|
|
640
657
|
return this.banRecovery;
|
|
641
658
|
}
|
|
659
|
+
/** Get the message type registry for direct access */
|
|
660
|
+
get messageRegistry() {
|
|
661
|
+
return this.messageTypeRegistry;
|
|
662
|
+
}
|
|
642
663
|
/**
|
|
643
664
|
* Export warm-up state for persistence between restarts
|
|
644
665
|
*/
|
|
@@ -731,6 +752,33 @@ export class AntiBan {
|
|
|
731
752
|
};
|
|
732
753
|
this.stateManager.saveImmediate(state);
|
|
733
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
757
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
758
|
+
*/
|
|
759
|
+
exportState() {
|
|
760
|
+
return exportAntibanState({
|
|
761
|
+
warmup: this.warmUp,
|
|
762
|
+
health: this.health,
|
|
763
|
+
rateLimiter: this.rateLimiter,
|
|
764
|
+
timelockGuard: this.timelockGuard,
|
|
765
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
766
|
+
instanceId: this.resolvedConfig.instanceId,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Import unified state snapshot.
|
|
771
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
772
|
+
*/
|
|
773
|
+
importState(snapshot) {
|
|
774
|
+
importAntibanState(snapshot, {
|
|
775
|
+
warmup: this.warmUp,
|
|
776
|
+
health: this.health,
|
|
777
|
+
rateLimiter: this.rateLimiter,
|
|
778
|
+
timelockGuard: this.timelockGuard,
|
|
779
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
734
782
|
/**
|
|
735
783
|
* Clean up all timers and resources.
|
|
736
784
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
@@ -746,6 +794,7 @@ export class AntiBan {
|
|
|
746
794
|
this.jidCanonicalizerModule?.destroy();
|
|
747
795
|
this.lidResolverModule?.destroy();
|
|
748
796
|
this.sessionStabilityMonitor?.reset();
|
|
797
|
+
this.messageTypeRegistry?.cleanup();
|
|
749
798
|
if (this.logging) {
|
|
750
799
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
751
800
|
}
|
package/dist/cjs/antiban.js
CHANGED
|
@@ -34,6 +34,8 @@ const persist_js_1 = require("./persist.js");
|
|
|
34
34
|
const profiles_js_1 = require("./profiles.js");
|
|
35
35
|
const deliveryTracker_js_1 = require("./deliveryTracker.js");
|
|
36
36
|
const instanceCoordinator_js_1 = require("./instanceCoordinator.js");
|
|
37
|
+
const messageTypeRegistry_js_1 = require("./messageTypeRegistry.js");
|
|
38
|
+
const stateExport_js_1 = require("./stateExport.js");
|
|
37
39
|
function isLegacyConfig(cfg) {
|
|
38
40
|
if (typeof cfg !== 'object' || cfg === null)
|
|
39
41
|
return false;
|
|
@@ -112,6 +114,7 @@ class AntiBan {
|
|
|
112
114
|
banRecovery;
|
|
113
115
|
deliveryTracker;
|
|
114
116
|
instanceCoordinator = null;
|
|
117
|
+
messageTypeRegistry = null;
|
|
115
118
|
stateManager = null;
|
|
116
119
|
resolvedConfig;
|
|
117
120
|
logging;
|
|
@@ -300,6 +303,13 @@ class AntiBan {
|
|
|
300
303
|
console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
|
|
301
304
|
}
|
|
302
305
|
}
|
|
306
|
+
// Initialize message type registry if configured
|
|
307
|
+
if (cfg.messageTypeRegistry) {
|
|
308
|
+
this.messageTypeRegistry = new messageTypeRegistry_js_1.MessageTypeRegistry();
|
|
309
|
+
if (this.logging) {
|
|
310
|
+
console.log(`[baileys-antiban] 📝 Message type registry enabled`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
303
313
|
}
|
|
304
314
|
/**
|
|
305
315
|
* Check if a message can be sent and get required delay.
|
|
@@ -600,6 +610,13 @@ class AntiBan {
|
|
|
600
610
|
if (this.instanceCoordinator) {
|
|
601
611
|
stats.instanceCoordinator = this.instanceCoordinator.getStats();
|
|
602
612
|
}
|
|
613
|
+
if (this.messageTypeRegistry) {
|
|
614
|
+
const warnings = this.messageTypeRegistry.getWarnings();
|
|
615
|
+
stats.messageRegistry = {
|
|
616
|
+
typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
|
|
617
|
+
warningCount: warnings.length,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
603
620
|
return stats;
|
|
604
621
|
}
|
|
605
622
|
/** Get the timelock guard for direct access */
|
|
@@ -642,6 +659,10 @@ class AntiBan {
|
|
|
642
659
|
get recoveryOrchestrator() {
|
|
643
660
|
return this.banRecovery;
|
|
644
661
|
}
|
|
662
|
+
/** Get the message type registry for direct access */
|
|
663
|
+
get messageRegistry() {
|
|
664
|
+
return this.messageTypeRegistry;
|
|
665
|
+
}
|
|
645
666
|
/**
|
|
646
667
|
* Export warm-up state for persistence between restarts
|
|
647
668
|
*/
|
|
@@ -734,6 +755,33 @@ class AntiBan {
|
|
|
734
755
|
};
|
|
735
756
|
this.stateManager.saveImmediate(state);
|
|
736
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
760
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
761
|
+
*/
|
|
762
|
+
exportState() {
|
|
763
|
+
return (0, stateExport_js_1.exportAntibanState)({
|
|
764
|
+
warmup: this.warmUp,
|
|
765
|
+
health: this.health,
|
|
766
|
+
rateLimiter: this.rateLimiter,
|
|
767
|
+
timelockGuard: this.timelockGuard,
|
|
768
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
769
|
+
instanceId: this.resolvedConfig.instanceId,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Import unified state snapshot.
|
|
774
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
775
|
+
*/
|
|
776
|
+
importState(snapshot) {
|
|
777
|
+
(0, stateExport_js_1.importAntibanState)(snapshot, {
|
|
778
|
+
warmup: this.warmUp,
|
|
779
|
+
health: this.health,
|
|
780
|
+
rateLimiter: this.rateLimiter,
|
|
781
|
+
timelockGuard: this.timelockGuard,
|
|
782
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
737
785
|
/**
|
|
738
786
|
* Clean up all timers and resources.
|
|
739
787
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
@@ -749,6 +797,7 @@ class AntiBan {
|
|
|
749
797
|
this.jidCanonicalizerModule?.destroy();
|
|
750
798
|
this.lidResolverModule?.destroy();
|
|
751
799
|
this.sessionStabilityMonitor?.reset();
|
|
800
|
+
this.messageTypeRegistry?.cleanup();
|
|
752
801
|
if (this.logging) {
|
|
753
802
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
754
803
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fleet Event Store — Multi-instance coordination via shared event log
|
|
4
|
+
*
|
|
5
|
+
* Enables multiple WhatsApp instances to share ban/warn/recovery signals
|
|
6
|
+
* via a pluggable backend (MySQL, in-memory, etc).
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - EventStoreBackend interface — caller provides storage
|
|
10
|
+
* - Two built-in backends:
|
|
11
|
+
* 1. MySQLEventStoreBackend — persistent, multi-instance (peer dep)
|
|
12
|
+
* 2. InMemoryEventStoreBackend — ephemeral, single-instance, testing
|
|
13
|
+
*
|
|
14
|
+
* Usage with MySQL:
|
|
15
|
+
* import mysql from 'mysql2/promise';
|
|
16
|
+
* const pool = mysql.createPool({ ... });
|
|
17
|
+
* const backend = createMySQLEventStoreBackend(pool);
|
|
18
|
+
* const store = createFleetEventStore({
|
|
19
|
+
* connectionId: 'wa-instance-1',
|
|
20
|
+
* backend,
|
|
21
|
+
* pollIntervalMs: 10_000
|
|
22
|
+
* });
|
|
23
|
+
* store.emit('warn', { risk: 'medium' });
|
|
24
|
+
* store.startPolling((events) => console.log('New events:', events));
|
|
25
|
+
*
|
|
26
|
+
* Usage in-memory (testing):
|
|
27
|
+
* const backend = createInMemoryEventStoreBackend();
|
|
28
|
+
* const store = createFleetEventStore({ connectionId: 'test', backend });
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.createFleetEventStore = createFleetEventStore;
|
|
32
|
+
exports.createMySQLEventStoreBackend = createMySQLEventStoreBackend;
|
|
33
|
+
exports.createInMemoryEventStoreBackend = createInMemoryEventStoreBackend;
|
|
34
|
+
function createFleetEventStore(config) {
|
|
35
|
+
const { connectionId, backend, logger } = config;
|
|
36
|
+
const pollIntervalMs = config.pollIntervalMs ?? 10_000;
|
|
37
|
+
let lastSeenEpoch = Date.now();
|
|
38
|
+
let pollTimer = null;
|
|
39
|
+
const emit = async (eventType, payload) => {
|
|
40
|
+
const epoch = Date.now();
|
|
41
|
+
await backend.emit(connectionId, eventType, epoch, payload);
|
|
42
|
+
logger?.info('[fleet-events] emitted', { connectionId, eventType, epoch });
|
|
43
|
+
};
|
|
44
|
+
const startPolling = (onNewEvents) => {
|
|
45
|
+
if (pollTimer)
|
|
46
|
+
return;
|
|
47
|
+
pollTimer = setInterval(() => {
|
|
48
|
+
void (async () => {
|
|
49
|
+
try {
|
|
50
|
+
const events = await backend.poll(connectionId, lastSeenEpoch);
|
|
51
|
+
if (events.length > 0) {
|
|
52
|
+
// Update cursor
|
|
53
|
+
const maxEpoch = Math.max(...events.map((e) => e.epoch));
|
|
54
|
+
lastSeenEpoch = maxEpoch;
|
|
55
|
+
onNewEvents(events);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger?.warn('[fleet-events] poll failed', { connectionId, err: error });
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
}, pollIntervalMs);
|
|
63
|
+
// @ts-ignore — unref exists on NodeJS.Timeout
|
|
64
|
+
pollTimer.unref?.();
|
|
65
|
+
logger?.info('[fleet-events] polling started', { connectionId, pollIntervalMs });
|
|
66
|
+
};
|
|
67
|
+
const stop = () => {
|
|
68
|
+
if (pollTimer) {
|
|
69
|
+
clearInterval(pollTimer);
|
|
70
|
+
pollTimer = null;
|
|
71
|
+
logger?.info('[fleet-events] polling stopped', { connectionId });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return { emit, startPolling, stop };
|
|
75
|
+
}
|
|
76
|
+
function createMySQLEventStoreBackend(pool) {
|
|
77
|
+
// Ensure table exists on first emit (idempotent)
|
|
78
|
+
let tableEnsured = false;
|
|
79
|
+
const ensureTable = async () => {
|
|
80
|
+
if (tableEnsured)
|
|
81
|
+
return;
|
|
82
|
+
const createTableSQL = `
|
|
83
|
+
CREATE TABLE IF NOT EXISTS antiban_events (
|
|
84
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
85
|
+
connection_id VARCHAR(255) NOT NULL,
|
|
86
|
+
event_type VARCHAR(50) NOT NULL,
|
|
87
|
+
epoch BIGINT NOT NULL,
|
|
88
|
+
payload JSON,
|
|
89
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
90
|
+
INDEX idx_conn (connection_id),
|
|
91
|
+
INDEX idx_epoch (epoch)
|
|
92
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
93
|
+
`;
|
|
94
|
+
try {
|
|
95
|
+
await pool.query(createTableSQL);
|
|
96
|
+
tableEnsured = true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Log but don't throw — table might already exist
|
|
100
|
+
console.warn('[mysql-backend] table creation failed (may already exist)', error);
|
|
101
|
+
tableEnsured = true; // Assume it exists
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const emit = async (connectionId, eventType, epoch, payload) => {
|
|
105
|
+
try {
|
|
106
|
+
await ensureTable();
|
|
107
|
+
await pool.execute('INSERT INTO antiban_events (connection_id, event_type, epoch, payload) VALUES (?, ?, ?, ?)', [connectionId, eventType, epoch, payload ? JSON.stringify(payload) : null]);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Never throw — event emission is non-critical
|
|
111
|
+
console.warn('[mysql-backend] emit failed', { connectionId, eventType, error });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const poll = async (connectionId, sinceEpoch) => {
|
|
115
|
+
try {
|
|
116
|
+
await ensureTable();
|
|
117
|
+
const [rows] = (await pool.execute('SELECT id, connection_id, event_type, epoch, payload, created_at FROM antiban_events WHERE connection_id = ? AND epoch > ? ORDER BY epoch ASC LIMIT 50', [connectionId, sinceEpoch]));
|
|
118
|
+
return rows.map((row) => ({
|
|
119
|
+
id: row.id,
|
|
120
|
+
connectionId: row.connection_id,
|
|
121
|
+
eventType: row.event_type,
|
|
122
|
+
epoch: row.epoch,
|
|
123
|
+
payload: row.payload ? JSON.parse(row.payload) : null,
|
|
124
|
+
createdAt: row.created_at,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn('[mysql-backend] poll failed', { connectionId, error });
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return { emit, poll };
|
|
133
|
+
}
|
|
134
|
+
// ===============================
|
|
135
|
+
// Built-in Backend: In-Memory
|
|
136
|
+
// ===============================
|
|
137
|
+
function createInMemoryEventStoreBackend() {
|
|
138
|
+
const events = [];
|
|
139
|
+
let nextId = 1;
|
|
140
|
+
const emit = async (connectionId, eventType, epoch, payload) => {
|
|
141
|
+
const event = {
|
|
142
|
+
id: nextId++,
|
|
143
|
+
connectionId,
|
|
144
|
+
eventType,
|
|
145
|
+
epoch,
|
|
146
|
+
payload: payload || null,
|
|
147
|
+
createdAt: new Date(),
|
|
148
|
+
};
|
|
149
|
+
events.push(event);
|
|
150
|
+
// Evict oldest if over 1000 entries
|
|
151
|
+
if (events.length > 1000) {
|
|
152
|
+
events.shift();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const poll = async (connectionId, sinceEpoch) => {
|
|
156
|
+
return events
|
|
157
|
+
.filter((e) => e.connectionId === connectionId && e.epoch > sinceEpoch)
|
|
158
|
+
.sort((a, b) => a.epoch - b.epoch)
|
|
159
|
+
.slice(0, 50);
|
|
160
|
+
};
|
|
161
|
+
return { emit, poll };
|
|
162
|
+
}
|