baileys-antiban 3.8.11 → 4.6.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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.8.11] - 2026-05-19
9
+
10
+ ### Security
11
+ - **persist.ts**: Resolve state file path to absolute (`path.resolve()`), reject null bytes. Add strict JSON shape validation (version, savedAt, knownChats types) before trusting loaded state — prevents type confusion from corrupt or tampered files.
12
+ - **proxyRotator.ts**: Replace `(0, eval)('require')` and `(0, eval)('import.meta.url')` with `new Function()` on static literal strings in the ESM code path. Not user-controlled, but removes the indirect eval chain for static analysis and CSP compliance.
13
+
14
+ ### Fixed
15
+ - **rateLimiter.ts**: Added LRU size cap (10,000 entries) to `identicalCount` Map. Time-window eviction alone allowed unbounded growth when sending many unique messages; oldest-by-lastSeen entries are now evicted when the cap is exceeded.
16
+ - **antiban.ts**: Extend `mapLegacyToFlat()` to preserve `autoPauseAt`, `groupMultiplier`, `groupProfiles`, `persist` from flat top-level fields when legacy config detection fires — completing the coverage from 3.8.9.
17
+
18
+ ### Changed (3.8.10)
19
+ - README: Expanded v3 flat config example with all `ResolvedConfig` fields; added correct `deafSession` wrapOptions (4th arg) example; marked nested Configuration section as deprecated.
20
+ - Tests: 4 new v3 test cases covering `maxIdenticalMessages`/`burstAllowance` forwarding, mixed legacy+flat preservation, `getConfig()`.
21
+ - CHANGELOG: Added missing 3.8.9 entry.
22
+
8
23
  ## [3.8.9] - 2026-05-19
9
24
 
10
25
  ### Fixed
package/dist/antiban.d.ts CHANGED
@@ -25,7 +25,10 @@ import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThro
25
25
  import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
26
26
  import { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
27
27
  import { SessionHealthMonitor, type SessionHealthStats } from './sessionStability.js';
28
+ import { BanRecoveryOrchestrator, type RecoveryStatus } from './banRecoveryOrchestrator.js';
28
29
  import { type AntiBanInput, type ResolvedConfig } from './presets.js';
30
+ import { type DeliveryTrackerStats } from './deliveryTracker.js';
31
+ import { type InstanceCoordinatorStats } from './instanceCoordinator.js';
29
32
  export interface AntiBanConfigLegacy {
30
33
  rateLimiter?: Partial<RateLimiterConfig>;
31
34
  warmUp?: Partial<WarmUpConfig>;
@@ -70,6 +73,9 @@ export interface AntiBanStats {
70
73
  lidResolver?: LidResolverStats | null;
71
74
  jidCanonicalizer?: JidCanonicalizerStats | null;
72
75
  sessionStability?: SessionHealthStats | null;
76
+ banRecovery?: RecoveryStatus | null;
77
+ deliveryTracker: DeliveryTrackerStats;
78
+ instanceCoordinator?: InstanceCoordinatorStats | null;
73
79
  }
74
80
  export declare class AntiBan {
75
81
  private rateLimiter;
@@ -84,6 +90,9 @@ export declare class AntiBan {
84
90
  private lidResolverModule;
85
91
  private jidCanonicalizerModule;
86
92
  private sessionStabilityMonitor;
93
+ private banRecovery;
94
+ private deliveryTracker;
95
+ private instanceCoordinator;
87
96
  private stateManager;
88
97
  private resolvedConfig;
89
98
  private logging;
@@ -98,7 +107,7 @@ export declare class AntiBan {
98
107
  * Record a successfully sent message.
99
108
  * Call this AFTER every successful sendMessage().
100
109
  */
101
- afterSend(recipient: string, content: string): void;
110
+ afterSend(recipient: string, content: string, msgId?: string): void;
102
111
  /**
103
112
  * Record a failed message send
104
113
  */
@@ -119,6 +128,11 @@ export declare class AntiBan {
119
128
  shouldReply: boolean;
120
129
  suggestedText?: string;
121
130
  };
131
+ /**
132
+ * Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
133
+ * Call from messages.update handler when delivery status is received.
134
+ */
135
+ onDeliveryReceipt(msgId: string): void;
122
136
  /**
123
137
  * Get the resolved configuration
124
138
  */
@@ -145,6 +159,8 @@ export declare class AntiBan {
145
159
  get jidCanonicalizer(): JidCanonicalizer | null;
146
160
  /** Get the session stability monitor for direct access */
147
161
  get sessionStability(): SessionHealthMonitor | null;
162
+ /** Get the ban recovery orchestrator for direct access */
163
+ get recoveryOrchestrator(): BanRecoveryOrchestrator;
148
164
  /**
149
165
  * Export warm-up state for persistence between restarts
150
166
  */
@@ -161,6 +177,7 @@ export declare class AntiBan {
161
177
  * Reset everything (use after a ban period)
162
178
  */
163
179
  reset(): void;
180
+ private runAdaptiveCheck;
164
181
  private persistStateDebounced;
165
182
  private persistStateImmediate;
166
183
  /**
package/dist/antiban.js CHANGED
@@ -25,9 +25,12 @@ import { PostReconnectThrottle } from './reconnectThrottle.js';
25
25
  import { LidResolver } from './lidResolver.js';
26
26
  import { JidCanonicalizer } from './jidCanonicalizer.js';
27
27
  import { SessionHealthMonitor } from './sessionStability.js';
28
+ import { BanRecoveryOrchestrator } from './banRecoveryOrchestrator.js';
28
29
  import { resolveConfig } from './presets.js';
29
30
  import { StateManager } from './persist.js';
30
31
  import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
32
+ import { DeliveryTracker } from './deliveryTracker.js';
33
+ import { InstanceCoordinator } from './instanceCoordinator.js';
31
34
  function isLegacyConfig(cfg) {
32
35
  if (typeof cfg !== 'object' || cfg === null)
33
36
  return false;
@@ -103,6 +106,9 @@ export class AntiBan {
103
106
  lidResolverModule = null;
104
107
  jidCanonicalizerModule = null;
105
108
  sessionStabilityMonitor = null;
109
+ banRecovery;
110
+ deliveryTracker;
111
+ instanceCoordinator = null;
106
112
  stateManager = null;
107
113
  resolvedConfig;
108
114
  logging;
@@ -170,23 +176,54 @@ export class AntiBan {
170
176
  console.log(`[baileys-antiban] ${status.recommendation}`);
171
177
  status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
172
178
  }
173
- // Call original callback if present
179
+ if ((status.risk === 'high' || status.risk === 'critical') && cfg.onAtRisk) {
180
+ cfg.onAtRisk(status);
181
+ }
182
+ // Trigger recovery orchestrator on critical risk
183
+ if (status.risk === 'critical') {
184
+ this.banRecovery.recordBanEvent('soft_ban');
185
+ }
186
+ cfg.onRiskChange?.(status);
174
187
  legacyPassthrough?.health?.onRiskChange?.(status);
175
188
  },
176
189
  });
190
+ // Initialize ban recovery orchestrator
191
+ this.banRecovery = new BanRecoveryOrchestrator({
192
+ onPhaseChange: (phase, plan) => {
193
+ if (this.logging) {
194
+ console.log(`[baileys-antiban] 🔄 Recovery phase: ${phase} — ${plan.description}`);
195
+ }
196
+ },
197
+ onHardBan: () => {
198
+ if (this.logging) {
199
+ console.log(`[baileys-antiban] 💀 HARD BAN detected — account likely dead, replace SIM`);
200
+ }
201
+ },
202
+ });
203
+ // Initialize delivery tracker
204
+ this.deliveryTracker = new DeliveryTracker({
205
+ onLowDeliveryRate: (rate) => {
206
+ if (this.logging) {
207
+ console.log(`[baileys-antiban] ⚠️ Low delivery rate detected: ${Math.round(rate * 100)}% — possible soft ban`);
208
+ }
209
+ },
210
+ });
177
211
  this.timelockGuard = new TimelockGuard({
178
212
  ...(legacyPassthrough?.timelock || {}),
179
213
  onTimelockDetected: (state) => {
180
214
  this.health.recordReachoutTimelock(state.enforcementType);
215
+ this.banRecovery.recordBanEvent('timelock');
181
216
  if (this.logging) {
182
217
  console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
183
218
  }
219
+ cfg.onTimelockDetected?.(state);
184
220
  legacyPassthrough?.timelock?.onTimelockDetected?.(state);
185
221
  },
186
222
  onTimelockLifted: (state) => {
187
223
  if (this.logging) {
188
224
  console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
189
225
  }
226
+ cfg.onTimelockLifted?.(state);
190
227
  legacyPassthrough?.timelock?.onTimelockLifted?.(state);
191
228
  },
192
229
  });
@@ -249,6 +286,17 @@ export class AntiBan {
249
286
  };
250
287
  this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
251
288
  }
289
+ // Initialize instance coordinator if configured
290
+ if (cfg.instanceCoordinator) {
291
+ this.instanceCoordinator = new InstanceCoordinator({
292
+ sharedFilePath: cfg.instanceCoordinator,
293
+ poolMaxPerMinute: cfg.instancePoolMaxPerMinute,
294
+ poolMaxPerHour: cfg.instancePoolMaxPerHour,
295
+ });
296
+ if (this.logging) {
297
+ console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
298
+ }
299
+ }
252
300
  }
253
301
  /**
254
302
  * Check if a message can be sent and get required delay.
@@ -269,6 +317,17 @@ export class AntiBan {
269
317
  health: healthStatus,
270
318
  };
271
319
  }
320
+ // Recovery orchestrator rate multiplier
321
+ const recoveryStatus = this.banRecovery.getStatus();
322
+ if (recoveryStatus.phase === 'paused') {
323
+ this.stats.messagesBlocked++;
324
+ return {
325
+ allowed: false,
326
+ delayMs: recoveryStatus.pauseRemainingMs || 0,
327
+ reason: `Ban recovery: ${recoveryStatus.recommendation}`,
328
+ health: healthStatus,
329
+ };
330
+ }
272
331
  // Timelock guard (allows existing chats, blocks new contacts)
273
332
  const timelockDecision = this.timelockGuard.canSend(recipient);
274
333
  if (!timelockDecision.allowed) {
@@ -340,6 +399,22 @@ export class AntiBan {
340
399
  health: healthStatus,
341
400
  };
342
401
  }
402
+ // Cross-instance coordination — check shared IP-level pool
403
+ if (this.instanceCoordinator) {
404
+ const slot = this.instanceCoordinator.tryAcquireSlot();
405
+ if (!slot.allowed) {
406
+ this.stats.messagesBlocked++;
407
+ if (this.logging) {
408
+ console.log(`[baileys-antiban] 🌐 BLOCKED — instance pool exhausted (shared IP limit), retry in ${slot.retryAfterMs}ms`);
409
+ }
410
+ return {
411
+ allowed: false,
412
+ delayMs: slot.retryAfterMs || 5000,
413
+ reason: 'Cross-instance rate pool exhausted',
414
+ health: healthStatus,
415
+ };
416
+ }
417
+ }
343
418
  // Group profile rate check (runs before rateLimiter.getDelay for timing)
344
419
  if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
345
420
  const groupLimits = applyGroupMultiplier({
@@ -379,6 +454,24 @@ export class AntiBan {
379
454
  const multiplier = Math.min(5, 1 / activityFactor);
380
455
  delay = Math.floor(delay * multiplier);
381
456
  }
457
+ // Per-contact risk multiplier — cold contacts need longer delays
458
+ // Only apply when contact graph is enabled, otherwise all contacts appear as 'stranger'
459
+ if (this.contactGraphWarmer['config']?.enabled) {
460
+ const contactState = this.contactGraphWarmer.getContactState(recipient);
461
+ const coldMultiplier = {
462
+ stranger: 2.5,
463
+ handshake_sent: 1.8,
464
+ handshake_complete: 1.3,
465
+ known: 1.0,
466
+ };
467
+ const contactRiskMult = coldMultiplier[contactState] ?? 1.0;
468
+ if (contactRiskMult > 1.0) {
469
+ delay = Math.floor(delay * contactRiskMult);
470
+ if (this.logging && contactRiskMult >= 2.0) {
471
+ console.log(`[baileys-antiban] ⚠️ Cold contact ${recipient} — ${contactState}, delay ×${contactRiskMult}`);
472
+ }
473
+ }
474
+ }
382
475
  // Roll for distraction pause
383
476
  const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
384
477
  if (distractionCheck.pause) {
@@ -406,11 +499,15 @@ export class AntiBan {
406
499
  * Record a successfully sent message.
407
500
  * Call this AFTER every successful sendMessage().
408
501
  */
409
- afterSend(recipient, content) {
502
+ afterSend(recipient, content, msgId) {
410
503
  this.rateLimiter.record(recipient, content);
411
504
  this.warmUp.record();
412
505
  this.replyRatioGuard.recordSent(recipient);
413
506
  this.stats.messagesAllowed++;
507
+ if (msgId) {
508
+ this.deliveryTracker.onMessageSent(msgId);
509
+ }
510
+ this.runAdaptiveCheck();
414
511
  this.persistStateDebounced();
415
512
  }
416
513
  /**
@@ -436,6 +533,7 @@ export class AntiBan {
436
533
  onReconnect() {
437
534
  this.health.recordReconnect();
438
535
  this.reconnectThrottleModule.onReconnect();
536
+ this.rateLimiter.adaptLimits(1.0);
439
537
  }
440
538
  /**
441
539
  * Handle incoming message — record in reply ratio + contact graph.
@@ -446,6 +544,13 @@ export class AntiBan {
446
544
  this.contactGraphWarmer.onIncomingMessage(jid);
447
545
  return this.replyRatioGuard.suggestReply(jid, msgText);
448
546
  }
547
+ /**
548
+ * Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
549
+ * Call from messages.update handler when delivery status is received.
550
+ */
551
+ onDeliveryReceipt(msgId) {
552
+ this.deliveryTracker.onDeliveryReceipt(msgId);
553
+ }
449
554
  /**
450
555
  * Get the resolved configuration
451
556
  */
@@ -461,6 +566,8 @@ export class AntiBan {
461
566
  health: this.health.getStatus(),
462
567
  warmUp: this.warmUp.getStatus(),
463
568
  rateLimiter: this.rateLimiter.getStats(),
569
+ banRecovery: this.banRecovery.getStatus(),
570
+ deliveryTracker: this.deliveryTracker.getStats(),
464
571
  };
465
572
  // Only include new stats if enabled
466
573
  if (this.replyRatioGuard['config']?.enabled) {
@@ -487,6 +594,9 @@ export class AntiBan {
487
594
  if (this.sessionStabilityMonitor) {
488
595
  stats.sessionStability = this.sessionStabilityMonitor.getStats();
489
596
  }
597
+ if (this.instanceCoordinator) {
598
+ stats.instanceCoordinator = this.instanceCoordinator.getStats();
599
+ }
490
600
  return stats;
491
601
  }
492
602
  /** Get the timelock guard for direct access */
@@ -525,6 +635,10 @@ export class AntiBan {
525
635
  get sessionStability() {
526
636
  return this.sessionStabilityMonitor;
527
637
  }
638
+ /** Get the ban recovery orchestrator for direct access */
639
+ get recoveryOrchestrator() {
640
+ return this.banRecovery;
641
+ }
528
642
  /**
529
643
  * Export warm-up state for persistence between restarts
530
644
  */
@@ -566,6 +680,35 @@ export class AntiBan {
566
680
  console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
567
681
  }
568
682
  }
683
+ runAdaptiveCheck() {
684
+ const delivery = this.deliveryTracker.getStats();
685
+ // Need min sample to be meaningful
686
+ if (delivery.deliveryRate === null)
687
+ return;
688
+ const rate = delivery.deliveryRate;
689
+ let targetFactor;
690
+ if (rate >= 0.85) {
691
+ targetFactor = 1.0; // Excellent — full speed
692
+ }
693
+ else if (rate >= 0.70) {
694
+ targetFactor = 0.75; // Good — slight reduction
695
+ }
696
+ else if (rate >= 0.55) {
697
+ targetFactor = 0.50; // Poor — halve throughput
698
+ }
699
+ else {
700
+ targetFactor = 0.25; // Bad — severe throttle (soft ban likely)
701
+ }
702
+ const current = this.rateLimiter.getCurrentFactor();
703
+ // Only log + adjust when factor changes meaningfully (>5% delta)
704
+ if (Math.abs(targetFactor - current) > 0.05) {
705
+ if (this.logging) {
706
+ const dir = targetFactor > current ? '📈' : '📉';
707
+ console.log(`[baileys-antiban] ${dir} Adaptive rate: delivery=${(rate * 100).toFixed(0)}% → factor ${current.toFixed(2)}→${targetFactor.toFixed(2)}`);
708
+ }
709
+ this.rateLimiter.adaptLimits(targetFactor);
710
+ }
711
+ }
569
712
  persistStateDebounced() {
570
713
  if (!this.stateManager)
571
714
  return;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * BanRecoveryOrchestrator — Structured recovery after ban/restriction events
3
+ *
4
+ * When WhatsApp restricts your account, the worst thing to do is immediately
5
+ * resume normal activity. This module provides a evidence-based recovery protocol:
6
+ *
7
+ * - Timelocked (reachout_restricted): 24h pause, resume at 10% rate, ramp 15%/week
8
+ * - Rate-overlimit (429): 4h pause, resume at 25% rate, ramp 25%/week
9
+ * - Soft-ban (repeated disconnects): 48h pause, resume at 5% rate, ramp 10%/week
10
+ * - Hard-ban (loggedOut): account is dead, signal for replacement
11
+ *
12
+ * Based on observed recovery times from community reports. Not guaranteed —
13
+ * WA's enforcement is non-deterministic. Treat as best-effort guidance.
14
+ */
15
+ export type BanEventType = 'timelock' | 'rate_overlimit' | 'soft_ban' | 'hard_ban';
16
+ export type RecoveryPhase = 'paused' | 'recovering' | 'ramping' | 'graduated' | 'dead';
17
+ export interface RecoveryPlan {
18
+ eventType: BanEventType;
19
+ pauseDurationMs: number;
20
+ resumeRateMultiplier: number;
21
+ weeklyRampPercent: number;
22
+ estimatedRecoveryDays: number;
23
+ description: string;
24
+ }
25
+ export interface BanRecoveryConfig {
26
+ /** Custom recovery plans per event type (overrides defaults) */
27
+ plans?: Partial<Record<BanEventType, Partial<RecoveryPlan>>>;
28
+ /** Called when recovery phase changes */
29
+ onPhaseChange?: (phase: RecoveryPhase, plan: RecoveryPlan) => void;
30
+ /** Called when account appears dead (hard ban) — signal to replace SIM */
31
+ onHardBan?: () => void;
32
+ /** Max weeks before giving up on recovery and declaring dead (default: 8) */
33
+ maxRecoveryWeeks?: number;
34
+ }
35
+ export interface RecoveryState {
36
+ active: boolean;
37
+ eventType?: BanEventType;
38
+ phase: RecoveryPhase;
39
+ banDetectedAt?: number;
40
+ pauseUntil?: number;
41
+ currentRateMultiplier: number;
42
+ weeksSinceResume: number;
43
+ banCount30d: number;
44
+ lastBanAt?: number;
45
+ }
46
+ export interface RecoveryStatus {
47
+ phase: RecoveryPhase;
48
+ rateMultiplier: number;
49
+ pauseRemainingMs?: number;
50
+ estimatedFullRecoveryDate?: number;
51
+ recommendation: string;
52
+ shouldReplaceNumber: boolean;
53
+ }
54
+ export declare class BanRecoveryOrchestrator {
55
+ private config;
56
+ private state;
57
+ private plans;
58
+ constructor(config?: BanRecoveryConfig);
59
+ /**
60
+ * Record a ban event and start recovery protocol
61
+ */
62
+ recordBanEvent(eventType: BanEventType): RecoveryStatus;
63
+ /**
64
+ * Get current recovery status (call before sending to check rate)
65
+ */
66
+ getStatus(): RecoveryStatus;
67
+ /**
68
+ * Current rate multiplier — multiply your normal limits by this
69
+ */
70
+ getRateMultiplier(): number;
71
+ /**
72
+ * Should be called daily/weekly to advance the ramp
73
+ */
74
+ tick(): void;
75
+ /**
76
+ * Classify a raw error into a BanEventType
77
+ */
78
+ static classifyError(err: unknown): BanEventType | null;
79
+ /**
80
+ * Serializable state for persistence
81
+ */
82
+ getState(): RecoveryState;
83
+ /**
84
+ * Restore from persisted state
85
+ */
86
+ static fromState(state: RecoveryState, config?: BanRecoveryConfig): BanRecoveryOrchestrator;
87
+ }