baileys-antiban 2.1.1 → 3.0.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 CHANGED
@@ -5,6 +5,26 @@ 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.0.0] — 2026-04-25
9
+
10
+ ### Breaking Changes
11
+ - Constructor now accepts `string | FlatConfig | undefined` — nested v2 config still works but logs deprecation warning
12
+ - `WarmUpConfig.statePath` removed (use `persist` in AntiBanConfig instead)
13
+
14
+ ### New Features
15
+ - **Zero-config:** `new AntiBan()` works with conservative defaults
16
+ - **Presets:** `conservative` / `moderate` / `aggressive`
17
+ - **State persistence:** `persist: './state.json'` — warmup + knownChats survive restarts
18
+ - **Group profiles:** `groupProfiles: true` — stricter rate limits for @g.us and @newsletter JIDs
19
+ - **Health decay:** Score recovers automatically (2pts/min severe, 5pts/min normal)
20
+ - **CLI:** `npx baileys-antiban status|reset|warmup`
21
+
22
+ ### Bug Fixes
23
+ - `statePath` in WarmUpConfig was declared but never implemented — replaced with working `persist` option
24
+ - Health score never recovered after ban signals — fixed with time-based decay
25
+
26
+ ---
27
+
8
28
  ## [2.1.0] - 2026-04-19
9
29
 
10
30
  ### Added
package/README.md CHANGED
@@ -385,7 +385,47 @@ npm install @oxidezap/baileyrs baileys-antiban
385
385
 
386
386
  Requires Node.js ≥16.
387
387
 
388
- ## Quick Start
388
+ ## Quick Start (v3)
389
+
390
+ ```bash
391
+ npm install baileys-antiban
392
+ ```
393
+
394
+ ```typescript
395
+ import { AntiBan } from 'baileys-antiban';
396
+
397
+ // Zero config — works immediately
398
+ const ab = new AntiBan();
399
+
400
+ // Or pick a preset
401
+ const ab = new AntiBan('moderate');
402
+
403
+ // Full control
404
+ const ab = new AntiBan({
405
+ preset: 'moderate',
406
+ persist: './antiban-state.json', // survives restarts
407
+ groupProfiles: true, // stricter limits for groups
408
+ maxPerMinute: 15, // override any value
409
+ });
410
+
411
+ // Usage unchanged
412
+ const result = await ab.beforeSend(jid, text);
413
+ if (result.allowed) {
414
+ await new Promise(r => setTimeout(r, result.delayMs));
415
+ await sock.sendMessage(jid, { text });
416
+ ab.afterSend(jid, text);
417
+ }
418
+ ```
419
+
420
+ ### CLI
421
+
422
+ ```bash
423
+ npx baileys-antiban status --state ./antiban-state.json
424
+ npx baileys-antiban warmup --simulate 7 --preset moderate
425
+ npx baileys-antiban reset --state ./antiban-state.json
426
+ ```
427
+
428
+ ## Quick Start (Legacy)
389
429
 
390
430
  ### Option 1: Wrap Your Socket (Easiest)
391
431
 
package/dist/antiban.d.ts CHANGED
@@ -25,7 +25,8 @@ 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
- export interface AntiBanConfig {
28
+ import { type AntiBanInput } from './presets.js';
29
+ export interface AntiBanConfigLegacy {
29
30
  rateLimiter?: Partial<RateLimiterConfig>;
30
31
  warmUp?: Partial<WarmUpConfig>;
31
32
  health?: Partial<HealthMonitorConfig>;
@@ -37,21 +38,16 @@ export interface AntiBanConfig {
37
38
  reconnectThrottle?: Partial<ReconnectThrottleConfig>;
38
39
  lidResolver?: LidResolverConfig;
39
40
  jidCanonicalizer?: JidCanonicalizerConfig;
40
- /** Session stability features (v2.0) — default disabled for backward compatibility */
41
41
  sessionStability?: {
42
42
  enabled: boolean;
43
- /** Enable canonical JID normalization before sendMessage (default: true if enabled) */
44
43
  canonicalJidNormalization?: boolean;
45
- /** Enable session health monitoring (default: true if enabled) */
46
44
  healthMonitoring?: boolean;
47
- /** Bad MAC threshold before declaring session degraded (default: 3) */
48
45
  badMacThreshold?: number;
49
- /** Time window for Bad MAC threshold in ms (default: 60000) */
50
46
  badMacWindowMs?: number;
51
47
  };
52
- /** Log warnings and blocks to console (default: true) */
53
48
  logging?: boolean;
54
49
  }
50
+ export type AntiBanConfig = AntiBanInput | AntiBanConfigLegacy;
55
51
  export interface SendDecision {
56
52
  allowed: boolean;
57
53
  delayMs: number;
@@ -88,9 +84,11 @@ export declare class AntiBan {
88
84
  private lidResolverModule;
89
85
  private jidCanonicalizerModule;
90
86
  private sessionStabilityMonitor;
87
+ private stateManager;
88
+ private resolvedConfig;
91
89
  private logging;
92
90
  private stats;
93
- constructor(config?: AntiBanConfig, warmUpState?: WarmUpState);
91
+ constructor(input?: AntiBanInput | AntiBanConfigLegacy, warmUpStateArg?: WarmUpState);
94
92
  /**
95
93
  * Check if a message can be sent and get required delay.
96
94
  * Call this BEFORE every sendMessage().
@@ -159,6 +157,8 @@ export declare class AntiBan {
159
157
  * Reset everything (use after a ban period)
160
158
  */
161
159
  reset(): void;
160
+ private persistStateDebounced;
161
+ private persistStateImmediate;
162
162
  /**
163
163
  * Clean up all timers and resources.
164
164
  * Call this when disposing of the AntiBan instance or when the socket closes.
package/dist/antiban.js CHANGED
@@ -25,6 +25,44 @@ 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 { resolveConfig } from './presets.js';
29
+ import { StateManager } from './persist.js';
30
+ import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
31
+ function isLegacyConfig(cfg) {
32
+ if (typeof cfg !== 'object' || cfg === null)
33
+ return false;
34
+ return 'rateLimiter' in cfg || 'warmUp' in cfg || 'health' in cfg || 'timelock' in cfg ||
35
+ 'replyRatio' in cfg || 'contactGraph' in cfg || 'presence' in cfg || 'retryTracker' in cfg ||
36
+ 'reconnectThrottle' in cfg || 'lidResolver' in cfg || 'jidCanonicalizer' in cfg ||
37
+ 'sessionStability' in cfg;
38
+ }
39
+ function mapLegacyToFlat(legacy) {
40
+ console.warn('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. ' +
41
+ 'Migrate to flat config: new AntiBan({ maxPerMinute: 8 }). ' +
42
+ 'See: https://github.com/kobie3717/baileys-antiban#migration');
43
+ const flat = {};
44
+ if (legacy.rateLimiter?.maxPerMinute !== undefined)
45
+ flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
46
+ if (legacy.rateLimiter?.maxPerHour !== undefined)
47
+ flat.maxPerHour = legacy.rateLimiter.maxPerHour;
48
+ if (legacy.rateLimiter?.maxPerDay !== undefined)
49
+ flat.maxPerDay = legacy.rateLimiter.maxPerDay;
50
+ if (legacy.rateLimiter?.minDelayMs !== undefined)
51
+ flat.minDelayMs = legacy.rateLimiter.minDelayMs;
52
+ if (legacy.rateLimiter?.maxDelayMs !== undefined)
53
+ flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
54
+ if (legacy.rateLimiter?.newChatDelayMs !== undefined)
55
+ flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
56
+ if (legacy.warmUp?.warmUpDays !== undefined)
57
+ flat.warmupDays = legacy.warmUp.warmUpDays;
58
+ if (legacy.warmUp?.day1Limit !== undefined)
59
+ flat.day1Limit = legacy.warmUp.day1Limit;
60
+ if (legacy.warmUp?.growthFactor !== undefined)
61
+ flat.growthFactor = legacy.warmUp.growthFactor;
62
+ if (legacy.logging !== undefined)
63
+ flat.logging = legacy.logging;
64
+ return flat;
65
+ }
28
66
  export class AntiBan {
29
67
  rateLimiter;
30
68
  warmUp;
@@ -38,17 +76,63 @@ export class AntiBan {
38
76
  lidResolverModule = null;
39
77
  jidCanonicalizerModule = null;
40
78
  sessionStabilityMonitor = null;
79
+ stateManager = null;
80
+ resolvedConfig;
41
81
  logging;
42
82
  stats = {
43
83
  messagesAllowed: 0,
44
84
  messagesBlocked: 0,
45
85
  totalDelayMs: 0,
46
86
  };
47
- constructor(config = {}, warmUpState) {
48
- this.rateLimiter = new RateLimiter(config.rateLimiter);
49
- this.warmUp = new WarmUp(config.warmUp, warmUpState);
87
+ constructor(input, warmUpStateArg) {
88
+ let flatConfig;
89
+ let legacyPassthrough = null;
90
+ let warmUpState = warmUpStateArg;
91
+ if (isLegacyConfig(input)) {
92
+ legacyPassthrough = input;
93
+ flatConfig = mapLegacyToFlat(legacyPassthrough);
94
+ }
95
+ else {
96
+ flatConfig = {};
97
+ legacyPassthrough = null;
98
+ }
99
+ const cfg = isLegacyConfig(input)
100
+ ? resolveConfig(flatConfig)
101
+ : resolveConfig(input);
102
+ this.resolvedConfig = cfg;
103
+ // Initialize persistence — load state before constructing modules
104
+ let savedState = null;
105
+ if (cfg.persist) {
106
+ this.stateManager = new StateManager(cfg.persist);
107
+ savedState = this.stateManager.load();
108
+ if (savedState) {
109
+ warmUpState = savedState.warmup;
110
+ }
111
+ }
112
+ this.logging = cfg.logging ?? true;
113
+ this.rateLimiter = new RateLimiter({
114
+ maxPerMinute: cfg.maxPerMinute,
115
+ maxPerHour: cfg.maxPerHour,
116
+ maxPerDay: cfg.maxPerDay,
117
+ minDelayMs: cfg.minDelayMs,
118
+ maxDelayMs: cfg.maxDelayMs,
119
+ newChatDelayMs: cfg.newChatDelayMs,
120
+ ...(legacyPassthrough?.rateLimiter || {}),
121
+ });
122
+ // Restore knownChats from persisted state after rateLimiter is constructed
123
+ if (savedState?.knownChats) {
124
+ this.rateLimiter.restoreKnownChats(savedState.knownChats);
125
+ }
126
+ this.warmUp = new WarmUp({
127
+ warmUpDays: cfg.warmupDays,
128
+ day1Limit: cfg.day1Limit,
129
+ growthFactor: cfg.growthFactor,
130
+ inactivityThresholdHours: cfg.inactivityThresholdHours,
131
+ ...(legacyPassthrough?.warmUp || {}),
132
+ }, warmUpState);
50
133
  this.health = new HealthMonitor({
51
- ...config.health,
134
+ autoPauseAt: cfg.autoPauseAt,
135
+ ...(legacyPassthrough?.health || {}),
52
136
  onRiskChange: (status) => {
53
137
  if (this.logging) {
54
138
  const emoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' };
@@ -56,74 +140,74 @@ export class AntiBan {
56
140
  console.log(`[baileys-antiban] ${status.recommendation}`);
57
141
  status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
58
142
  }
59
- config.health?.onRiskChange?.(status);
143
+ // Call original callback if present
144
+ legacyPassthrough?.health?.onRiskChange?.(status);
60
145
  },
61
146
  });
62
- this.logging = config.logging ?? true;
63
147
  this.timelockGuard = new TimelockGuard({
64
- ...config.timelock,
148
+ ...(legacyPassthrough?.timelock || {}),
65
149
  onTimelockDetected: (state) => {
66
150
  this.health.recordReachoutTimelock(state.enforcementType);
67
151
  if (this.logging) {
68
152
  console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
69
153
  }
70
- config.timelock?.onTimelockDetected?.(state);
154
+ legacyPassthrough?.timelock?.onTimelockDetected?.(state);
71
155
  },
72
156
  onTimelockLifted: (state) => {
73
157
  if (this.logging) {
74
158
  console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
75
159
  }
76
- config.timelock?.onTimelockLifted?.(state);
160
+ legacyPassthrough?.timelock?.onTimelockLifted?.(state);
77
161
  },
78
162
  });
79
- this.replyRatioGuard = new ReplyRatioGuard(config.replyRatio);
80
- this.contactGraphWarmer = new ContactGraphWarmer(config.contactGraph);
81
- this.presenceChoreographer = new PresenceChoreographer(config.presence);
163
+ this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio);
164
+ this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph);
165
+ this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence);
82
166
  this.retryTrackerModule = new RetryReasonTracker({
83
- ...config.retryTracker,
167
+ ...(legacyPassthrough?.retryTracker || {}),
84
168
  onSpiral: (msgId, reason) => {
85
169
  if (this.logging) {
86
170
  console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
87
171
  }
88
- config.retryTracker?.onSpiral?.(msgId, reason);
172
+ legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
89
173
  },
90
174
  });
91
175
  this.reconnectThrottleModule = new PostReconnectThrottle({
92
- ...config.reconnectThrottle,
176
+ ...(legacyPassthrough?.reconnectThrottle || {}),
93
177
  baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
94
178
  });
95
179
  // Initialize LID resolver and canonicalizer if configured
96
180
  // If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
97
- if (config.jidCanonicalizer?.enabled) {
181
+ if (legacyPassthrough?.jidCanonicalizer?.enabled) {
98
182
  // Create or use provided resolver
99
- if (config.jidCanonicalizer.resolver) {
183
+ if (legacyPassthrough.jidCanonicalizer.resolver) {
100
184
  // User provided their own resolver
101
- this.jidCanonicalizerModule = new JidCanonicalizer(config.jidCanonicalizer);
102
- this.lidResolverModule = config.jidCanonicalizer.resolver;
185
+ this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
186
+ this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
103
187
  }
104
188
  else {
105
189
  // Create new resolver using lidResolver config if provided
106
- const resolverConfig = config.lidResolver || config.jidCanonicalizer.resolverConfig;
190
+ const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
107
191
  const resolver = new LidResolver(resolverConfig);
108
192
  this.lidResolverModule = resolver;
109
193
  this.jidCanonicalizerModule = new JidCanonicalizer({
110
- ...config.jidCanonicalizer,
194
+ ...legacyPassthrough.jidCanonicalizer,
111
195
  resolver,
112
196
  });
113
197
  }
114
198
  }
115
- else if (config.lidResolver) {
199
+ else if (legacyPassthrough?.lidResolver) {
116
200
  // Standalone resolver without canonicalizer
117
- this.lidResolverModule = new LidResolver(config.lidResolver);
201
+ this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver);
118
202
  }
119
203
  // Initialize session stability monitor if enabled
120
- if (config.sessionStability?.enabled) {
204
+ if (legacyPassthrough?.sessionStability?.enabled) {
121
205
  const healthConfig = {
122
- badMacThreshold: config.sessionStability.badMacThreshold,
123
- badMacWindowMs: config.sessionStability.badMacWindowMs,
206
+ badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
207
+ badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
124
208
  onDegraded: (stats) => {
125
209
  if (this.logging) {
126
- console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${config.sessionStability?.badMacWindowMs || 60000}ms`);
210
+ console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 60000}ms`);
127
211
  console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
128
212
  }
129
213
  },
@@ -226,6 +310,24 @@ export class AntiBan {
226
310
  health: healthStatus,
227
311
  };
228
312
  }
313
+ // Group profile rate check (runs before rateLimiter.getDelay for timing)
314
+ if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
315
+ const groupLimits = applyGroupMultiplier({
316
+ maxPerMinute: this.resolvedConfig.maxPerMinute,
317
+ maxPerHour: this.resolvedConfig.maxPerHour,
318
+ maxPerDay: this.resolvedConfig.maxPerDay,
319
+ }, this.resolvedConfig.groupMultiplier);
320
+ const stats = this.rateLimiter.getStats();
321
+ if (stats.lastMinute >= groupLimits.maxPerMinute ||
322
+ stats.lastHour >= groupLimits.maxPerHour ||
323
+ stats.lastDay >= groupLimits.maxPerDay) {
324
+ this.stats.messagesBlocked++;
325
+ if (this.logging) {
326
+ console.log(`[baileys-antiban] 🚫 BLOCKED — group rate limit exceeded for ${recipient}`);
327
+ }
328
+ return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
329
+ }
330
+ }
229
331
  // Rate limiter delay
230
332
  let delay = await this.rateLimiter.getDelay(recipient, content);
231
333
  if (delay === -1) {
@@ -279,6 +381,7 @@ export class AntiBan {
279
381
  this.warmUp.record();
280
382
  this.replyRatioGuard.recordSent(recipient);
281
383
  this.stats.messagesAllowed++;
384
+ this.persistStateDebounced();
282
385
  }
283
386
  /**
284
387
  * Record a failed message send
@@ -292,6 +395,10 @@ export class AntiBan {
292
395
  onDisconnect(reason) {
293
396
  this.health.recordDisconnect(reason);
294
397
  this.reconnectThrottleModule.onDisconnect();
398
+ const reasonStr = String(reason);
399
+ if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
400
+ this.persistStateImmediate();
401
+ }
295
402
  }
296
403
  /**
297
404
  * Record a successful reconnection
@@ -423,11 +530,34 @@ export class AntiBan {
423
530
  console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
424
531
  }
425
532
  }
533
+ persistStateDebounced() {
534
+ if (!this.stateManager)
535
+ return;
536
+ const state = {
537
+ warmup: this.warmUp.exportState(),
538
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
539
+ savedAt: Date.now(),
540
+ version: 3,
541
+ };
542
+ this.stateManager.saveDebounced(state);
543
+ }
544
+ persistStateImmediate() {
545
+ if (!this.stateManager)
546
+ return;
547
+ const state = {
548
+ warmup: this.warmUp.exportState(),
549
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
550
+ savedAt: Date.now(),
551
+ version: 3,
552
+ };
553
+ this.stateManager.saveImmediate(state);
554
+ }
426
555
  /**
427
556
  * Clean up all timers and resources.
428
557
  * Call this when disposing of the AntiBan instance or when the socket closes.
429
558
  */
430
559
  destroy() {
560
+ this.stateManager?.destroy();
431
561
  this.timelockGuard.reset(); // Clears the resumeTimer
432
562
  this.replyRatioGuard.reset();
433
563
  this.contactGraphWarmer.reset();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * baileys-antiban CLI
4
+ * Usage: npx baileys-antiban <command> [options]
5
+ */
6
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * baileys-antiban CLI
4
+ * Usage: npx baileys-antiban <command> [options]
5
+ */
6
+ import * as fs from 'fs';
7
+ import { StateManager } from './persist.js';
8
+ import { resolveConfig } from './presets.js';
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+ function parseArgs(argv) {
12
+ const result = {};
13
+ for (let i = 1; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ if (arg.startsWith('--')) {
16
+ const key = arg.slice(2);
17
+ const next = argv[i + 1];
18
+ if (next && !next.startsWith('--')) {
19
+ result[key] = next;
20
+ i++;
21
+ }
22
+ else {
23
+ result[key] = true;
24
+ }
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+ function cmdStatus(opts) {
30
+ const statePath = opts['state'];
31
+ let warmupInfo = 'No state file (in-memory mode)';
32
+ let savedAt = 'N/A';
33
+ if (statePath) {
34
+ const mgr = new StateManager(statePath);
35
+ const state = mgr.load();
36
+ if (state) {
37
+ const now = Date.now();
38
+ const dayMs = 86400000;
39
+ const currentDay = Math.floor((now - state.warmup.startedAt) / dayMs);
40
+ warmupInfo = state.warmup.graduated
41
+ ? 'Graduated (warmup complete)'
42
+ : `Day ${currentDay + 1}, sent today: ${state.warmup.dailyCounts[currentDay] || 0}`;
43
+ savedAt = new Date(state.savedAt).toISOString();
44
+ }
45
+ else {
46
+ warmupInfo = 'State file missing or corrupt';
47
+ }
48
+ }
49
+ const output = {
50
+ warmup: warmupInfo,
51
+ savedAt,
52
+ statePath: statePath || null,
53
+ };
54
+ if (opts['json']) {
55
+ console.log(JSON.stringify(output, null, 2));
56
+ }
57
+ else {
58
+ console.log('═══ baileys-antiban status ═══');
59
+ console.log(`Warmup: ${output.warmup}`);
60
+ console.log(`Saved: ${output.savedAt}`);
61
+ console.log(`State: ${output.statePath || 'none'}`);
62
+ }
63
+ }
64
+ function cmdReset(opts) {
65
+ const statePath = opts['state'];
66
+ if (!statePath) {
67
+ console.error('Error: --state <path> required for reset');
68
+ process.exit(1);
69
+ }
70
+ if (!fs.existsSync(statePath)) {
71
+ console.log('State file does not exist — nothing to reset');
72
+ return;
73
+ }
74
+ fs.unlinkSync(statePath);
75
+ console.log(`✅ State file deleted: ${statePath}`);
76
+ }
77
+ function cmdWarmupSimulate(opts) {
78
+ const days = parseInt(opts['simulate'] || '7', 10);
79
+ const presetName = opts['preset'] || 'conservative';
80
+ const cfg = resolveConfig(presetName);
81
+ console.log(`\nWarmup simulation — preset: ${presetName}, days: ${days}`);
82
+ console.log('─'.repeat(50));
83
+ const startDate = new Date();
84
+ for (let day = 0; day < days; day++) {
85
+ const date = new Date(startDate);
86
+ date.setDate(date.getDate() + day);
87
+ const dayName = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
88
+ const limit = Math.round(cfg.day1Limit * Math.pow(cfg.growthFactor, day));
89
+ const bar = '█'.repeat(Math.min(30, Math.round(limit / 10)));
90
+ console.log(`Day ${String(day + 1).padStart(2)} ${dayName.padEnd(15)} ${String(limit).padStart(5)} msgs/day ${bar}`);
91
+ }
92
+ console.log('─'.repeat(50));
93
+ console.log(`Day ${days + 1}+: graduated (unlimited by warmup)\n`);
94
+ }
95
+ // Main
96
+ const opts = parseArgs(args);
97
+ switch (command) {
98
+ case 'status':
99
+ cmdStatus(opts);
100
+ break;
101
+ case 'reset':
102
+ cmdReset(opts);
103
+ break;
104
+ case 'warmup':
105
+ if (opts['simulate']) {
106
+ cmdWarmupSimulate(opts);
107
+ }
108
+ else {
109
+ console.error('Usage: npx baileys-antiban warmup --simulate <days> [--preset conservative|moderate|aggressive]');
110
+ process.exit(1);
111
+ }
112
+ break;
113
+ default:
114
+ console.log('baileys-antiban v3.0');
115
+ console.log('');
116
+ console.log('Commands:');
117
+ console.log(' status [--state <path>] [--json] Show warmup and health status');
118
+ console.log(' reset --state <path> Delete state file');
119
+ console.log(' warmup --simulate <days> [--preset] Show warmup schedule');
120
+ console.log('');
121
+ console.log('Examples:');
122
+ console.log(' npx baileys-antiban status --state ./antiban-state.json');
123
+ console.log(' npx baileys-antiban warmup --simulate 7 --preset moderate');
124
+ console.log(' npx baileys-antiban reset --state ./antiban-state.json');
125
+ }
package/dist/health.d.ts CHANGED
@@ -45,6 +45,8 @@ export declare class HealthMonitor {
45
45
  private startTime;
46
46
  private paused;
47
47
  private lastRisk;
48
+ private lastBadEventTime;
49
+ private lastEventWasSevere;
48
50
  constructor(config?: Partial<HealthMonitorConfig>);
49
51
  /**
50
52
  * Record a disconnection event
package/dist/health.js CHANGED
@@ -24,6 +24,8 @@ export class HealthMonitor {
24
24
  startTime = Date.now();
25
25
  paused = false;
26
26
  lastRisk = 'low';
27
+ lastBadEventTime = Date.now();
28
+ lastEventWasSevere = false;
27
29
  constructor(config = {}) {
28
30
  this.config = { ...DEFAULT_CONFIG, ...config };
29
31
  }
@@ -32,16 +34,20 @@ export class HealthMonitor {
32
34
  */
33
35
  recordDisconnect(reason) {
34
36
  const reasonStr = String(reason);
35
- // 403 = Forbidden (WhatsApp blocking)
36
37
  if (reasonStr === '403' || reasonStr === 'forbidden') {
37
38
  this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr });
39
+ this.lastBadEventTime = Date.now();
40
+ this.lastEventWasSevere = true;
38
41
  }
39
- // 401 = Logged out (possible temp ban)
40
42
  else if (reasonStr === '401' || reasonStr === 'loggedOut') {
41
43
  this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr });
44
+ this.lastBadEventTime = Date.now();
45
+ this.lastEventWasSevere = true;
42
46
  }
43
47
  else {
44
48
  this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr });
49
+ this.lastBadEventTime = Date.now();
50
+ this.lastEventWasSevere = false;
45
51
  }
46
52
  this.checkAndNotify();
47
53
  }
@@ -56,6 +62,8 @@ export class HealthMonitor {
56
62
  */
57
63
  recordMessageFailed(error) {
58
64
  this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error });
65
+ this.lastBadEventTime = Date.now();
66
+ this.lastEventWasSevere = false;
59
67
  this.checkAndNotify();
60
68
  }
61
69
  /**
@@ -63,6 +71,8 @@ export class HealthMonitor {
63
71
  */
64
72
  recordReachoutTimelock(detail) {
65
73
  this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail });
74
+ this.lastBadEventTime = Date.now();
75
+ this.lastEventWasSevere = false;
66
76
  this.checkAndNotify();
67
77
  }
68
78
  /**
@@ -100,7 +110,7 @@ export class HealthMonitor {
100
110
  reasons.push(`${disconnects} disconnects in last hour (critical threshold)`);
101
111
  }
102
112
  else if (disconnects >= this.config.disconnectWarningThreshold) {
103
- score += 15;
113
+ score += 30;
104
114
  reasons.push(`${disconnects} disconnects in last hour`);
105
115
  }
106
116
  // Failed messages
@@ -110,12 +120,18 @@ export class HealthMonitor {
110
120
  }
111
121
  // Determine risk level
112
122
  score = Math.min(100, score);
123
+ // Tiered decay: recover based on time since last bad event
124
+ // Severe (403/401): 2pts/min — ~50min to clear 100pts
125
+ // Normal: 5pts/min — ~20min to clear 100pts
126
+ const minutesSinceLastBad = (now - this.lastBadEventTime) / 60000;
127
+ const decayRate = this.lastEventWasSevere ? 2 : 5;
128
+ score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate));
113
129
  let risk;
114
- if (score >= 85)
130
+ if (score >= 80)
115
131
  risk = 'critical';
116
- else if (score >= 60)
132
+ else if (score >= 40)
117
133
  risk = 'high';
118
- else if (score >= 30)
134
+ else if (score >= 15)
119
135
  risk = 'medium';
120
136
  else
121
137
  risk = 'low';
@@ -174,6 +190,8 @@ export class HealthMonitor {
174
190
  this.startTime = Date.now();
175
191
  this.paused = false;
176
192
  this.lastRisk = 'low';
193
+ this.lastBadEventTime = Date.now();
194
+ this.lastEventWasSevere = false;
177
195
  }
178
196
  cleanup(now) {
179
197
  // Keep last 6 hours of events
package/dist/index.d.ts CHANGED
@@ -28,3 +28,6 @@ export { ContentVariator, type VariatorConfig } from './contentVariator.js';
28
28
  export { WebhookAlerts, type WebhookConfig } from './webhooks.js';
29
29
  export { Scheduler, type SchedulerConfig } from './scheduler.js';
30
30
  export { type StateAdapter, FileStateAdapter } from './stateAdapter.js';
31
+ export { resolveConfig, PRESETS, type AntiBanInput, type ResolvedConfig, type PresetName } from './presets.js';
32
+ export { StateManager, type PersistedState } from './persist.js';
33
+ export { isGroup, isNewsletter, isBroadcast, shouldUseGroupProfile, applyGroupMultiplier, type RateLimits } from './profiles.js';
package/dist/index.js CHANGED
@@ -37,3 +37,7 @@ export { WebhookAlerts } from './webhooks.js';
37
37
  export { Scheduler } from './scheduler.js';
38
38
  // State persistence
39
39
  export { FileStateAdapter } from './stateAdapter.js';
40
+ // v3.0 new modules
41
+ export { resolveConfig, PRESETS } from './presets.js';
42
+ export { StateManager } from './persist.js';
43
+ export { isGroup, isNewsletter, isBroadcast, shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
@@ -0,0 +1,28 @@
1
+ import type { WarmUpState } from './warmup.js';
2
+ export interface PersistedState {
3
+ warmup: WarmUpState;
4
+ knownChats: string[];
5
+ savedAt: number;
6
+ version: 3;
7
+ }
8
+ /**
9
+ * Manages persisted state for a single baileys-antiban instance.
10
+ *
11
+ * **Single-writer assumption:** No file lock is used. Two processes sharing
12
+ * the same state file will race on concurrent writes. Use separate state
13
+ * files per process to avoid data corruption.
14
+ */
15
+ export declare class StateManager {
16
+ private path;
17
+ private debounceTimer;
18
+ constructor(filePath: string);
19
+ load(): PersistedState | null;
20
+ /** Debounced save — called after every send (5s delay) */
21
+ saveDebounced(state: PersistedState): void;
22
+ /** Immediate save — called after health events (ban/restriction) */
23
+ saveImmediate(state: PersistedState): void;
24
+ /** Flush/cancel pending debounced write (for tests and process exit) */
25
+ flush(): void;
26
+ destroy(): void;
27
+ private writeFile;
28
+ }
@@ -0,0 +1,79 @@
1
+ import * as fs from 'fs';
2
+ const KNOWN_CHATS_MAX = 1000;
3
+ const DEBOUNCE_MS = 5000;
4
+ /**
5
+ * Manages persisted state for a single baileys-antiban instance.
6
+ *
7
+ * **Single-writer assumption:** No file lock is used. Two processes sharing
8
+ * the same state file will race on concurrent writes. Use separate state
9
+ * files per process to avoid data corruption.
10
+ */
11
+ export class StateManager {
12
+ path;
13
+ debounceTimer = null;
14
+ constructor(filePath) {
15
+ this.path = filePath;
16
+ }
17
+ load() {
18
+ try {
19
+ const raw = fs.readFileSync(this.path, 'utf-8');
20
+ const parsed = JSON.parse(raw);
21
+ if (parsed.version !== 3) {
22
+ console.warn('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh');
23
+ return null;
24
+ }
25
+ return parsed;
26
+ }
27
+ catch {
28
+ // Missing file = silent null. Corrupt JSON = warn.
29
+ if (fs.existsSync(this.path)) {
30
+ console.warn('[baileys-antiban] WARN: corrupt state file, starting fresh');
31
+ }
32
+ return null;
33
+ }
34
+ }
35
+ /** Debounced save — called after every send (5s delay) */
36
+ saveDebounced(state) {
37
+ if (this.debounceTimer) {
38
+ clearTimeout(this.debounceTimer);
39
+ }
40
+ this.debounceTimer = setTimeout(() => {
41
+ this.writeFile(state);
42
+ this.debounceTimer = null;
43
+ }, DEBOUNCE_MS);
44
+ }
45
+ /** Immediate save — called after health events (ban/restriction) */
46
+ saveImmediate(state) {
47
+ if (this.debounceTimer) {
48
+ clearTimeout(this.debounceTimer);
49
+ this.debounceTimer = null;
50
+ }
51
+ this.writeFile(state);
52
+ }
53
+ /** Flush/cancel pending debounced write (for tests and process exit) */
54
+ flush() {
55
+ if (this.debounceTimer) {
56
+ clearTimeout(this.debounceTimer);
57
+ this.debounceTimer = null;
58
+ }
59
+ }
60
+ destroy() {
61
+ this.flush();
62
+ }
63
+ writeFile(state) {
64
+ const toSave = {
65
+ ...state,
66
+ savedAt: Date.now(),
67
+ // LRU eviction: keep last KNOWN_CHATS_MAX entries
68
+ knownChats: state.knownChats.length > KNOWN_CHATS_MAX
69
+ ? state.knownChats.slice(-KNOWN_CHATS_MAX)
70
+ : state.knownChats,
71
+ };
72
+ try {
73
+ fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8');
74
+ }
75
+ catch (err) {
76
+ console.warn(`[baileys-antiban] WARN: failed to write state to ${this.path}:`, err);
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,24 @@
1
+ import type { BanRiskLevel } from './health.js';
2
+ export interface ResolvedConfig {
3
+ maxPerMinute: number;
4
+ maxPerHour: number;
5
+ maxPerDay: number;
6
+ minDelayMs: number;
7
+ maxDelayMs: number;
8
+ newChatDelayMs: number;
9
+ warmupDays: number;
10
+ day1Limit: number;
11
+ growthFactor: number;
12
+ inactivityThresholdHours: number;
13
+ autoPauseAt: BanRiskLevel;
14
+ groupMultiplier: number;
15
+ groupProfiles: boolean;
16
+ persist?: string;
17
+ logging: boolean;
18
+ }
19
+ export type PresetName = 'conservative' | 'moderate' | 'aggressive';
20
+ export type AntiBanInput = PresetName | Partial<ResolvedConfig & {
21
+ preset?: PresetName;
22
+ }> | undefined;
23
+ export declare const PRESETS: Record<PresetName, ResolvedConfig>;
24
+ export declare function resolveConfig(input: AntiBanInput): ResolvedConfig;
@@ -0,0 +1,67 @@
1
+ export const PRESETS = {
2
+ conservative: {
3
+ maxPerMinute: 5,
4
+ maxPerHour: 100,
5
+ maxPerDay: 800,
6
+ minDelayMs: 2500,
7
+ maxDelayMs: 7000,
8
+ newChatDelayMs: 4000,
9
+ warmupDays: 10,
10
+ day1Limit: 15,
11
+ growthFactor: 1.8,
12
+ inactivityThresholdHours: 72,
13
+ autoPauseAt: 'medium',
14
+ groupMultiplier: 0.5,
15
+ groupProfiles: false,
16
+ logging: true,
17
+ },
18
+ moderate: {
19
+ maxPerMinute: 10,
20
+ maxPerHour: 300,
21
+ maxPerDay: 1500,
22
+ minDelayMs: 1500,
23
+ maxDelayMs: 5000,
24
+ newChatDelayMs: 3000,
25
+ warmupDays: 7,
26
+ day1Limit: 20,
27
+ growthFactor: 1.8,
28
+ inactivityThresholdHours: 72,
29
+ autoPauseAt: 'high',
30
+ groupMultiplier: 0.7,
31
+ groupProfiles: false,
32
+ logging: true,
33
+ },
34
+ aggressive: {
35
+ maxPerMinute: 20,
36
+ maxPerHour: 800,
37
+ maxPerDay: 4000,
38
+ minDelayMs: 800,
39
+ maxDelayMs: 3000,
40
+ newChatDelayMs: 2000,
41
+ warmupDays: 4,
42
+ day1Limit: 35,
43
+ growthFactor: 2.0,
44
+ inactivityThresholdHours: 48,
45
+ autoPauseAt: 'critical',
46
+ groupMultiplier: 0.9,
47
+ groupProfiles: false,
48
+ logging: true,
49
+ },
50
+ };
51
+ export function resolveConfig(input) {
52
+ if (input === undefined) {
53
+ return { ...PRESETS.conservative };
54
+ }
55
+ if (typeof input === 'string') {
56
+ if (!(input in PRESETS)) {
57
+ throw new Error(`Unknown preset "${input}". Valid: ${Object.keys(PRESETS).join(', ')}`);
58
+ }
59
+ return { ...PRESETS[input] };
60
+ }
61
+ // Object form — extract preset base, merge overrides
62
+ const { preset = 'conservative', ...overrides } = input;
63
+ if (!(preset in PRESETS)) {
64
+ throw new Error(`Unknown preset "${preset}". Valid: ${Object.keys(PRESETS).join(', ')}`);
65
+ }
66
+ return { ...PRESETS[preset], ...overrides };
67
+ }
@@ -0,0 +1,22 @@
1
+ export interface RateLimits {
2
+ maxPerMinute: number;
3
+ maxPerHour: number;
4
+ maxPerDay: number;
5
+ }
6
+ /** @g.us = WhatsApp group */
7
+ export declare function isGroup(jid: string): boolean;
8
+ /** @newsletter = WhatsApp newsletter/channel */
9
+ export declare function isNewsletter(jid: string): boolean;
10
+ /** status@broadcast = broadcast list */
11
+ export declare function isBroadcast(jid: string): boolean;
12
+ /**
13
+ * Returns true if the JID should use stricter (group) rate limits.
14
+ * Groups and newsletters both get the group multiplier in v3.
15
+ * v4: separate newsletter profile.
16
+ */
17
+ export declare function shouldUseGroupProfile(jid: string): boolean;
18
+ /**
19
+ * Scale rate limits by multiplier for group/newsletter JIDs.
20
+ * Floors to integer, minimum 1 per limit.
21
+ */
22
+ export declare function applyGroupMultiplier(limits: RateLimits, multiplier: number): RateLimits;
@@ -0,0 +1,31 @@
1
+ /** @g.us = WhatsApp group */
2
+ export function isGroup(jid) {
3
+ return jid.endsWith('@g.us');
4
+ }
5
+ /** @newsletter = WhatsApp newsletter/channel */
6
+ export function isNewsletter(jid) {
7
+ return jid.endsWith('@newsletter');
8
+ }
9
+ /** status@broadcast = broadcast list */
10
+ export function isBroadcast(jid) {
11
+ return jid === 'status@broadcast' || jid.endsWith('@broadcast');
12
+ }
13
+ /**
14
+ * Returns true if the JID should use stricter (group) rate limits.
15
+ * Groups and newsletters both get the group multiplier in v3.
16
+ * v4: separate newsletter profile.
17
+ */
18
+ export function shouldUseGroupProfile(jid) {
19
+ return isGroup(jid) || isNewsletter(jid);
20
+ }
21
+ /**
22
+ * Scale rate limits by multiplier for group/newsletter JIDs.
23
+ * Floors to integer, minimum 1 per limit.
24
+ */
25
+ export function applyGroupMultiplier(limits, multiplier) {
26
+ return {
27
+ maxPerMinute: Math.max(1, Math.floor(limits.maxPerMinute * multiplier)),
28
+ maxPerHour: Math.max(1, Math.floor(limits.maxPerHour * multiplier)),
29
+ maxPerDay: Math.max(1, Math.floor(limits.maxPerDay * multiplier)),
30
+ };
31
+ }
@@ -61,6 +61,10 @@ export declare class RateLimiter {
61
61
  * Get current usage stats
62
62
  */
63
63
  getStats(): RateLimiterStats;
64
+ /** Get the set of known chat JIDs (for state persistence) */
65
+ getKnownChats(): Set<string>;
66
+ /** Restore known chats from persisted state */
67
+ restoreKnownChats(chats: string[]): void;
64
68
  private cleanup;
65
69
  /** Random delay between min and max (gaussian-ish distribution) */
66
70
  private jitter;
@@ -154,6 +154,16 @@ export class RateLimiter {
154
154
  knownChats: this.knownChats.size,
155
155
  };
156
156
  }
157
+ /** Get the set of known chat JIDs (for state persistence) */
158
+ getKnownChats() {
159
+ return this.knownChats;
160
+ }
161
+ /** Restore known chats from persisted state */
162
+ restoreKnownChats(chats) {
163
+ for (const jid of chats) {
164
+ this.knownChats.add(jid);
165
+ }
166
+ }
157
167
  cleanup(now) {
158
168
  // Remove messages older than 24 hours
159
169
  this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
@@ -134,7 +134,7 @@ export class PostReconnectThrottle {
134
134
  }
135
135
  // Calculate budget for current window
136
136
  const baselineRate = this.config.baselineRatePerMinute ? this.config.baselineRatePerMinute() : 8;
137
- const allowedInWindow = Math.floor(baselineRate * multiplier);
137
+ const allowedInWindow = Math.max(1, Math.floor(baselineRate * multiplier));
138
138
  // Check if we're over budget
139
139
  if (this.sendsInCurrentWindow >= allowedInWindow) {
140
140
  const windowRemaining = this.WINDOW_DURATION_MS - (now - this.currentWindowStart);
@@ -37,6 +37,10 @@ export class TimelockGuard {
37
37
  this.config.onTimelockDetected?.(this.getState());
38
38
  this.scheduleResume();
39
39
  }
40
+ else if (this.state.isActive && wasActive) {
41
+ // Already locked but expiry updated — reschedule timer with new expiry
42
+ this.scheduleResume();
43
+ }
40
44
  if (!this.state.isActive && wasActive) {
41
45
  this.clearResumeTimer();
42
46
  this.config.onTimelockLifted?.(this.getState());
package/dist/warmup.d.ts CHANGED
@@ -18,8 +18,6 @@ export interface WarmUpConfig {
18
18
  growthFactor: number;
19
19
  /** Hours of inactivity before re-entering warm-up (default: 72) */
20
20
  inactivityThresholdHours: number;
21
- /** Persist state to this file path (optional) */
22
- statePath?: string;
23
21
  }
24
22
  export interface WarmUpState {
25
23
  /** When warm-up started */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "2.1.1",
3
+ "version": "3.0.1",
4
4
  "description": "Anti-ban middleware for Baileys WhatsApp bots. Rate limiting, warmup, health monitor, LID resolver, disconnect classifier. Free Whapi.Cloud alternative.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,9 @@
27
27
  "test:check": "tsc --noEmit && npm test",
28
28
  "prepublishOnly": "npm run build"
29
29
  },
30
+ "bin": {
31
+ "baileys-antiban": "./dist/cli.js"
32
+ },
30
33
  "keywords": [
31
34
  "baileys",
32
35
  "baileyrs",