baileys-antiban 1.3.1 → 1.5.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,46 @@ 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
+ ## [1.5.0] - 2026-04-18
9
+
10
+ ### Added
11
+ - **RetryReasonTracker** module: Track message retry reasons and detect retry spirals
12
+ - Classifies 10 retry reason types (no_session, invalid_key, bad_mac, decryption_failure, server_error_463, server_error_429, timeout, no_route, node_malformed, unknown)
13
+ - Detects retry spirals when same message retries exceed threshold (default: 3)
14
+ - Provides stats on total retries, retries by reason, spirals detected, and active retries
15
+ - Auto-integrates with messages.update events in wrapper
16
+ - Inspired by whatsapp-rust's protocol/retry.rs module
17
+ - **PostReconnectThrottle** module: Throttle outbound messages after reconnection
18
+ - Prevents burst-floods on reconnect that trigger WhatsApp rate limits
19
+ - Configurable ramp-up from initial rate multiplier (default: 10%) to full rate over ramp duration (default: 60s)
20
+ - Linear ramp with configurable steps (default: 6 steps)
21
+ - Auto-integrates with connection.update events
22
+ - Inspired by whatsapp-rust's client/sessions.rs semaphore swap pattern
23
+ - Both modules are opt-in (enabled: false by default) for backward compatibility
24
+
25
+ ### Changed
26
+ - `AntiBan.beforeSend()` now also consults reconnect throttle
27
+ - `AntiBan.onReconnect()` triggers reconnect throttle window
28
+ - `AntiBan.getStats()` includes retry tracker and reconnect throttle stats when enabled
29
+ - Wrapper now tracks message updates for retry classification and clears on successful send
30
+
31
+ ## [1.4.0] - 2026-04-18
32
+
33
+ ### Added
34
+ - **Transport-agnostic support** — works with both `baileys` and `@oxidezap/baileyrs` (Rust/WASM WhatsApp library)
35
+ - Both transports now listed as optional peer dependencies
36
+ - GitHub Actions CI workflow with dual-transport matrix testing (Node 18.x + 20.x × baileys + baileyrs)
37
+ - New test suite: `tests/transport-agnostic.test.ts` for duck-typed socket validation
38
+ - Updated JSDoc examples showing usage with both transports
39
+
40
+ ### Changed
41
+ - `peerDependencies` now includes both `baileys` and `@oxidezap/baileyrs` as optional
42
+ - Package description updated to mention transport-agnostic support
43
+ - Wrapper comments clarify baileyrs timelock behavior (no `reachoutTimeLock` events in v0.0.8 — operates in detection-only mode)
44
+
45
+ ### Why
46
+ Positions baileys-antiban as "Switzerland" of WhatsApp anti-ban — works with any Baileys-compatible transport layer. No breaking changes for existing baileys users.
47
+
8
48
  ## [1.3.1] - 2026-04-16
9
49
 
10
50
  ### Changed
package/README.md CHANGED
@@ -4,9 +4,64 @@
4
4
  [![Node.js Version](https://img.shields.io/node/v/baileys-antiban.svg)](https://nodejs.org/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Anti-ban middleware for [Baileys](https://github.com/WhiskeySockets/Baileys) — protect your WhatsApp number with human-like messaging patterns.
7
+ **Transport-agnostic** anti-ban middleware — protect your WhatsApp number with human-like messaging patterns. Works with both [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
8
8
 
9
- ## v1.3 New Features
9
+ ## v1.5 New Features
10
+
11
+ ### RetryReasonTracker
12
+ Tracks message retry reasons and detects retry spirals (when the same message keeps failing). Inspired by whatsapp-rust's protocol/retry.rs module.
13
+
14
+ ```typescript
15
+ import { AntiBan } from 'baileys-antiban';
16
+
17
+ const antiban = new AntiBan({
18
+ retryTracker: {
19
+ enabled: true,
20
+ maxRetries: 5, // Max retries before considering a message failed
21
+ spiralThreshold: 3, // Retries before warning about retry spiral
22
+ onSpiral: (msgId, reason) => {
23
+ console.warn(`Message ${msgId} stuck in retry spiral: ${reason}`);
24
+ },
25
+ },
26
+ });
27
+
28
+ // Stats show retry patterns
29
+ const stats = antiban.getStats().retryTracker;
30
+ console.log(stats.totalRetries); // Total retries across all messages
31
+ console.log(stats.byReason.timeout); // Retries due to timeout
32
+ console.log(stats.spiralsDetected); // Messages stuck in retry loops
33
+ console.log(stats.activeRetries); // Messages currently retrying
34
+ ```
35
+
36
+ **Retry reasons tracked**: no_session, invalid_key, bad_mac, decryption_failure, server_error_463, server_error_429, timeout, no_route, node_malformed, unknown
37
+
38
+ ### PostReconnectThrottle
39
+ Throttles outbound messages after reconnection to prevent burst-floods that trigger rate limits. Inspired by whatsapp-rust's client/sessions.rs semaphore swap pattern.
40
+
41
+ ```typescript
42
+ const antiban = new AntiBan({
43
+ reconnectThrottle: {
44
+ enabled: true,
45
+ rampDurationMs: 60_000, // 60s ramp-up to full rate
46
+ initialRateMultiplier: 0.1, // Start at 10% of normal rate
47
+ rampSteps: 6, // 10% → 25% → 50% → 75% → 90% → 100%
48
+ },
49
+ });
50
+
51
+ // After reconnect, sends are automatically throttled for 60 seconds
52
+ // Ramps from 10% rate to 100% rate linearly over 6 steps
53
+
54
+ // Stats show throttle state
55
+ const stats = antiban.getStats().reconnectThrottle;
56
+ console.log(stats.isThrottled); // Currently throttled?
57
+ console.log(stats.currentMultiplier); // 0.1 to 1.0
58
+ console.log(stats.remainingMs); // Time until full rate
59
+ console.log(stats.throttledSendCount); // Sends gated since reconnect
60
+ ```
61
+
62
+ **Why?** When WhatsApp reconnects after a disconnection, sending messages at full rate immediately can trigger rate limit alarms. The reconnect throttle gradually ramps up sending rate over 60 seconds, mimicking how a human would resume messaging after their internet came back.
63
+
64
+ ## v1.3 Features
10
65
 
11
66
  ### ReplyRatioGuard
12
67
  Tracks outbound:inbound message ratio per contact. Blocks sends to non-responsive contacts to avoid "spray-and-pray" ban patterns. Optionally suggests auto-replies to maintain healthy engagement.
@@ -92,21 +147,46 @@ WhatsApp bans numbers that behave like bots. This library makes your Baileys bot
92
147
  - **Reply ratio tracking** (v1.3) — blocks sends to non-responsive contacts
93
148
  - **Contact graph enforcement** (v1.3) — requires handshakes before bulk/group sends
94
149
  - **Circadian rhythm** (v1.3) — realistic time-of-day activity patterns
150
+ - **Retry tracking** (v1.5) — detect retry spirals and classify retry reasons
151
+ - **Reconnect throttle** (v1.5) — prevent burst-floods after reconnection
152
+
153
+ ## Supported Transports
154
+
155
+ **v1.4+** is transport-agnostic and works with any Baileys-compatible WhatsApp library:
156
+
157
+ - **[Baileys](https://github.com/WhiskeySockets/Baileys)** (Node.js, JavaScript/TypeScript)
158
+ - **[@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs)** (Rust/WASM, Baileys-compatible API)
159
+
160
+ Both use the same `wrapSocket()` integration. Zero code changes needed.
95
161
 
96
162
  ## Installation
97
163
 
164
+ ### With Baileys (Node.js)
165
+
166
+ ```bash
167
+ npm install baileys baileys-antiban
168
+ ```
169
+
170
+ ### With baileyrs (Rust/WASM)
171
+
98
172
  ```bash
99
- npm install baileys-antiban
173
+ npm install @oxidezap/baileyrs baileys-antiban
100
174
  ```
101
175
 
102
- Requires Node.js ≥16 and Baileys ≥6.0.0.
176
+ Requires Node.js ≥16.
103
177
 
104
178
  ## Quick Start
105
179
 
106
180
  ### Option 1: Wrap Your Socket (Easiest)
107
181
 
182
+ Works with both baileys and baileyrs — same code:
183
+
108
184
  ```typescript
185
+ // With baileys:
109
186
  import makeWASocket from 'baileys';
187
+ // OR with baileyrs:
188
+ // import { makeWASocket } from '@oxidezap/baileyrs';
189
+
110
190
  import { wrapSocket } from 'baileys-antiban';
111
191
 
112
192
  const sock = makeWASocket({ /* your config */ });
@@ -200,6 +280,17 @@ const antiban = new AntiBan({
200
280
  console.log('Timelock lifted — resuming normal operation');
201
281
  },
202
282
  },
283
+ retryTracker: {
284
+ enabled: false, // Opt-in (default: false)
285
+ maxRetries: 5,
286
+ spiralThreshold: 3,
287
+ },
288
+ reconnectThrottle: {
289
+ enabled: false, // Opt-in (default: false)
290
+ rampDurationMs: 60_000,
291
+ initialRateMultiplier: 0.1,
292
+ rampSteps: 6,
293
+ },
203
294
  logging: true, // Console logging (default: true)
204
295
  });
205
296
  ```
package/dist/antiban.d.ts CHANGED
@@ -20,6 +20,8 @@ import { TimelockGuard, type TimelockGuardConfig } from './timelockGuard.js';
20
20
  import { ReplyRatioGuard, type ReplyRatioConfig, type ReplyRatioStats } from './replyRatio.js';
21
21
  import { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats } from './contactGraph.js';
22
22
  import { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
23
+ import { RetryReasonTracker, type RetryTrackerConfig, type RetryStats } from './retryTracker.js';
24
+ import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
23
25
  export interface AntiBanConfig {
24
26
  rateLimiter?: Partial<RateLimiterConfig>;
25
27
  warmUp?: Partial<WarmUpConfig>;
@@ -28,6 +30,8 @@ export interface AntiBanConfig {
28
30
  replyRatio?: Partial<ReplyRatioConfig>;
29
31
  contactGraph?: Partial<ContactGraphConfig>;
30
32
  presence?: Partial<PresenceChoreographerConfig>;
33
+ retryTracker?: Partial<RetryTrackerConfig>;
34
+ reconnectThrottle?: Partial<ReconnectThrottleConfig>;
31
35
  /** Log warnings and blocks to console (default: true) */
32
36
  logging?: boolean;
33
37
  }
@@ -48,6 +52,8 @@ export interface AntiBanStats {
48
52
  replyRatio?: ReplyRatioStats;
49
53
  contactGraph?: ContactGraphStats;
50
54
  presence?: PresenceChoreographerStats;
55
+ retryTracker?: RetryStats | null;
56
+ reconnectThrottle?: ReconnectThrottleStats | null;
51
57
  }
52
58
  export declare class AntiBan {
53
59
  private rateLimiter;
@@ -57,6 +63,8 @@ export declare class AntiBan {
57
63
  private replyRatioGuard;
58
64
  private contactGraphWarmer;
59
65
  private presenceChoreographer;
66
+ private retryTrackerModule;
67
+ private reconnectThrottleModule;
60
68
  private logging;
61
69
  private stats;
62
70
  constructor(config?: AntiBanConfig, warmUpState?: WarmUpState);
@@ -102,6 +110,10 @@ export declare class AntiBan {
102
110
  get contactGraph(): ContactGraphWarmer;
103
111
  /** Get the presence choreographer for direct access */
104
112
  get presence(): PresenceChoreographer;
113
+ /** Get the retry tracker for direct access */
114
+ get retryTracker(): RetryReasonTracker;
115
+ /** Get the reconnect throttle for direct access */
116
+ get reconnectThrottle(): PostReconnectThrottle;
105
117
  /**
106
118
  * Export warm-up state for persistence between restarts
107
119
  */
package/dist/antiban.js CHANGED
@@ -20,6 +20,8 @@ import { TimelockGuard } from './timelockGuard.js';
20
20
  import { ReplyRatioGuard } from './replyRatio.js';
21
21
  import { ContactGraphWarmer } from './contactGraph.js';
22
22
  import { PresenceChoreographer } from './presenceChoreographer.js';
23
+ import { RetryReasonTracker } from './retryTracker.js';
24
+ import { PostReconnectThrottle } from './reconnectThrottle.js';
23
25
  export class AntiBan {
24
26
  rateLimiter;
25
27
  warmUp;
@@ -28,6 +30,8 @@ export class AntiBan {
28
30
  replyRatioGuard;
29
31
  contactGraphWarmer;
30
32
  presenceChoreographer;
33
+ retryTrackerModule;
34
+ reconnectThrottleModule;
31
35
  logging;
32
36
  stats = {
33
37
  messagesAllowed: 0,
@@ -69,6 +73,19 @@ export class AntiBan {
69
73
  this.replyRatioGuard = new ReplyRatioGuard(config.replyRatio);
70
74
  this.contactGraphWarmer = new ContactGraphWarmer(config.contactGraph);
71
75
  this.presenceChoreographer = new PresenceChoreographer(config.presence);
76
+ this.retryTrackerModule = new RetryReasonTracker({
77
+ ...config.retryTracker,
78
+ onSpiral: (msgId, reason) => {
79
+ if (this.logging) {
80
+ console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
81
+ }
82
+ config.retryTracker?.onSpiral?.(msgId, reason);
83
+ },
84
+ });
85
+ this.reconnectThrottleModule = new PostReconnectThrottle({
86
+ ...config.reconnectThrottle,
87
+ baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
88
+ });
72
89
  }
73
90
  /**
74
91
  * Check if a message can be sent and get required delay.
@@ -146,6 +163,20 @@ export class AntiBan {
146
163
  health: healthStatus,
147
164
  };
148
165
  }
166
+ // Reconnect throttle check
167
+ const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend();
168
+ if (!reconnectThrottleDecision.allowed) {
169
+ this.stats.messagesBlocked++;
170
+ if (this.logging) {
171
+ console.log(`[baileys-antiban] 🔄 BLOCKED — reconnect throttle: ${reconnectThrottleDecision.reason}`);
172
+ }
173
+ return {
174
+ allowed: false,
175
+ delayMs: reconnectThrottleDecision.retryAfterMs || 0,
176
+ reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
177
+ health: healthStatus,
178
+ };
179
+ }
149
180
  // Rate limiter delay
150
181
  let delay = await this.rateLimiter.getDelay(recipient, content);
151
182
  if (delay === -1) {
@@ -211,12 +242,14 @@ export class AntiBan {
211
242
  */
212
243
  onDisconnect(reason) {
213
244
  this.health.recordDisconnect(reason);
245
+ this.reconnectThrottleModule.onDisconnect();
214
246
  }
215
247
  /**
216
248
  * Record a successful reconnection
217
249
  */
218
250
  onReconnect() {
219
251
  this.health.recordReconnect();
252
+ this.reconnectThrottleModule.onReconnect();
220
253
  }
221
254
  /**
222
255
  * Handle incoming message — record in reply ratio + contact graph.
@@ -247,6 +280,12 @@ export class AntiBan {
247
280
  if (this.presenceChoreographer['config']?.enabled) {
248
281
  stats.presence = this.presenceChoreographer.getStats();
249
282
  }
283
+ if (this.retryTrackerModule['config']?.enabled) {
284
+ stats.retryTracker = this.retryTrackerModule.getStats();
285
+ }
286
+ if (this.reconnectThrottleModule['config']?.enabled) {
287
+ stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
288
+ }
250
289
  return stats;
251
290
  }
252
291
  /** Get the timelock guard for direct access */
@@ -265,6 +304,14 @@ export class AntiBan {
265
304
  get presence() {
266
305
  return this.presenceChoreographer;
267
306
  }
307
+ /** Get the retry tracker for direct access */
308
+ get retryTracker() {
309
+ return this.retryTrackerModule;
310
+ }
311
+ /** Get the reconnect throttle for direct access */
312
+ get reconnectThrottle() {
313
+ return this.reconnectThrottleModule;
314
+ }
268
315
  /**
269
316
  * Export warm-up state for persistence between restarts
270
317
  */
@@ -299,6 +346,8 @@ export class AntiBan {
299
346
  this.replyRatioGuard.reset();
300
347
  this.contactGraphWarmer.reset();
301
348
  this.presenceChoreographer.reset();
349
+ this.retryTrackerModule.destroy();
350
+ this.reconnectThrottleModule.destroy();
302
351
  this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
303
352
  if (this.logging) {
304
353
  console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
@@ -313,6 +362,8 @@ export class AntiBan {
313
362
  this.replyRatioGuard.reset();
314
363
  this.contactGraphWarmer.reset();
315
364
  this.presenceChoreographer.reset();
365
+ this.retryTrackerModule.destroy();
366
+ this.reconnectThrottleModule.destroy();
316
367
  if (this.logging) {
317
368
  console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
318
369
  }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export { TimelockGuard, type TimelockGuardConfig, type TimelockState } from './t
15
15
  export { ReplyRatioGuard, type ReplyRatioConfig, type ReplyRatioStats } from './replyRatio.js';
16
16
  export { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats, type ContactState } from './contactGraph.js';
17
17
  export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
18
+ export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type RetryReason } from './retryTracker.js';
19
+ export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
18
20
  export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
19
21
  export { MessageQueue, type QueuedMessage, type MessageQueueConfig } from './messageQueue.js';
20
22
  export { ContentVariator, type VariatorConfig } from './contentVariator.js';
package/dist/index.js CHANGED
@@ -17,6 +17,9 @@ export { TimelockGuard } from './timelockGuard.js';
17
17
  export { ReplyRatioGuard } from './replyRatio.js';
18
18
  export { ContactGraphWarmer } from './contactGraph.js';
19
19
  export { PresenceChoreographer } from './presenceChoreographer.js';
20
+ // v1.5 new modules
21
+ export { RetryReasonTracker } from './retryTracker.js';
22
+ export { PostReconnectThrottle } from './reconnectThrottle.js';
20
23
  // Socket wrapper
21
24
  export { wrapSocket } from './wrapper.js';
22
25
  // Optional features
@@ -0,0 +1,89 @@
1
+ /**
2
+ * PostReconnectThrottle — Throttle outbound messages after reconnection
3
+ *
4
+ * Inspired by whatsapp-rust's client/sessions.rs which uses semaphore=1 during
5
+ * offline sync (serializes all processing), then swaps to semaphore=64 when sync
6
+ * completes. This prevents burst-floods on reconnect that trigger WA rate limits.
7
+ *
8
+ * In middleware layer: on reconnect, enter a throttled window where beforeSend()
9
+ * gates outbound messages to an artificially low rate, then ramps back to normal
10
+ * over a configurable period.
11
+ *
12
+ * Usage:
13
+ * const throttle = new PostReconnectThrottle({
14
+ * enabled: true,
15
+ * rampDurationMs: 60_000,
16
+ * initialRateMultiplier: 0.1,
17
+ * });
18
+ *
19
+ * // On connection.update with connection === 'open':
20
+ * throttle.onReconnect();
21
+ *
22
+ * // Before sending:
23
+ * const decision = throttle.beforeSend();
24
+ * if (!decision.allowed) {
25
+ * // Wait decision.retryAfterMs
26
+ * }
27
+ *
28
+ * // Get current throttle multiplier (1.0 = no throttle):
29
+ * const multiplier = throttle.getCurrentMultiplier();
30
+ */
31
+ export interface ReconnectThrottleConfig {
32
+ enabled?: boolean;
33
+ rampDurationMs?: number;
34
+ initialRateMultiplier?: number;
35
+ rampSteps?: number;
36
+ baselineRatePerMinute?: () => number;
37
+ }
38
+ export interface ReconnectThrottleStats {
39
+ isThrottled: boolean;
40
+ currentMultiplier: number;
41
+ throttledSinceMs: number | null;
42
+ remainingMs: number;
43
+ throttledSendCount: number;
44
+ lifetimeReconnects: number;
45
+ }
46
+ export declare class PostReconnectThrottle {
47
+ private config;
48
+ private throttledSince;
49
+ private throttledSendCount;
50
+ private lifetimeReconnects;
51
+ private rampTimer;
52
+ private currentStep;
53
+ private sendsInCurrentWindow;
54
+ private currentWindowStart;
55
+ private readonly WINDOW_DURATION_MS;
56
+ constructor(config?: ReconnectThrottleConfig);
57
+ /**
58
+ * Call when connection is re-established. Starts throttle window.
59
+ */
60
+ onReconnect(): void;
61
+ /**
62
+ * Call when connection drops (optional — reset state).
63
+ */
64
+ onDisconnect(): void;
65
+ /**
66
+ * Schedule the next ramp step
67
+ */
68
+ private scheduleNextRampStep;
69
+ /**
70
+ * Returns current rate multiplier (1.0 = no throttle)
71
+ */
72
+ getCurrentMultiplier(): number;
73
+ /**
74
+ * Checks if a send should be gated. Returns {allowed, reason, retryAfterMs?}
75
+ */
76
+ beforeSend(): {
77
+ allowed: boolean;
78
+ reason?: string;
79
+ retryAfterMs?: number;
80
+ };
81
+ /**
82
+ * Get current stats
83
+ */
84
+ getStats(): ReconnectThrottleStats;
85
+ /**
86
+ * Destroy and clean up timers
87
+ */
88
+ destroy(): void;
89
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * PostReconnectThrottle — Throttle outbound messages after reconnection
3
+ *
4
+ * Inspired by whatsapp-rust's client/sessions.rs which uses semaphore=1 during
5
+ * offline sync (serializes all processing), then swaps to semaphore=64 when sync
6
+ * completes. This prevents burst-floods on reconnect that trigger WA rate limits.
7
+ *
8
+ * In middleware layer: on reconnect, enter a throttled window where beforeSend()
9
+ * gates outbound messages to an artificially low rate, then ramps back to normal
10
+ * over a configurable period.
11
+ *
12
+ * Usage:
13
+ * const throttle = new PostReconnectThrottle({
14
+ * enabled: true,
15
+ * rampDurationMs: 60_000,
16
+ * initialRateMultiplier: 0.1,
17
+ * });
18
+ *
19
+ * // On connection.update with connection === 'open':
20
+ * throttle.onReconnect();
21
+ *
22
+ * // Before sending:
23
+ * const decision = throttle.beforeSend();
24
+ * if (!decision.allowed) {
25
+ * // Wait decision.retryAfterMs
26
+ * }
27
+ *
28
+ * // Get current throttle multiplier (1.0 = no throttle):
29
+ * const multiplier = throttle.getCurrentMultiplier();
30
+ */
31
+ const DEFAULT_CONFIG = {
32
+ enabled: false,
33
+ rampDurationMs: 60_000,
34
+ initialRateMultiplier: 0.1,
35
+ rampSteps: 6,
36
+ baselineRatePerMinute: null,
37
+ };
38
+ export class PostReconnectThrottle {
39
+ config;
40
+ throttledSince = null;
41
+ throttledSendCount = 0;
42
+ lifetimeReconnects = 0;
43
+ rampTimer = null;
44
+ currentStep = 0;
45
+ // Tracking sends in current window
46
+ sendsInCurrentWindow = 0;
47
+ currentWindowStart = 0;
48
+ WINDOW_DURATION_MS = 60_000; // 1 minute window
49
+ constructor(config) {
50
+ this.config = {
51
+ ...DEFAULT_CONFIG,
52
+ ...config,
53
+ baselineRatePerMinute: config?.baselineRatePerMinute || null,
54
+ };
55
+ }
56
+ /**
57
+ * Call when connection is re-established. Starts throttle window.
58
+ */
59
+ onReconnect() {
60
+ if (!this.config.enabled)
61
+ return;
62
+ this.throttledSince = Date.now();
63
+ this.currentStep = 0;
64
+ this.throttledSendCount = 0;
65
+ this.lifetimeReconnects++;
66
+ this.sendsInCurrentWindow = 0;
67
+ this.currentWindowStart = Date.now();
68
+ // Clear any existing ramp timer
69
+ if (this.rampTimer) {
70
+ clearTimeout(this.rampTimer);
71
+ }
72
+ // Set up ramp schedule
73
+ this.scheduleNextRampStep();
74
+ }
75
+ /**
76
+ * Call when connection drops (optional — reset state).
77
+ */
78
+ onDisconnect() {
79
+ // Keep throttle state for now — it will expire naturally
80
+ // This prevents rapid reconnect/disconnect cycles from resetting throttle too early
81
+ }
82
+ /**
83
+ * Schedule the next ramp step
84
+ */
85
+ scheduleNextRampStep() {
86
+ if (this.currentStep >= this.config.rampSteps) {
87
+ // Ramp complete — no longer throttled
88
+ this.throttledSince = null;
89
+ this.rampTimer = null;
90
+ return;
91
+ }
92
+ const stepDuration = this.config.rampDurationMs / this.config.rampSteps;
93
+ this.rampTimer = setTimeout(() => {
94
+ this.currentStep++;
95
+ this.scheduleNextRampStep();
96
+ }, stepDuration);
97
+ }
98
+ /**
99
+ * Returns current rate multiplier (1.0 = no throttle)
100
+ */
101
+ getCurrentMultiplier() {
102
+ if (!this.config.enabled || !this.throttledSince) {
103
+ return 1.0;
104
+ }
105
+ const elapsed = Date.now() - this.throttledSince;
106
+ if (elapsed >= this.config.rampDurationMs) {
107
+ // Ramp complete
108
+ return 1.0;
109
+ }
110
+ // Linear ramp from initialRateMultiplier to 1.0 across rampSteps
111
+ const progress = this.currentStep / this.config.rampSteps;
112
+ const multiplier = this.config.initialRateMultiplier +
113
+ (1.0 - this.config.initialRateMultiplier) * progress;
114
+ return Math.min(1.0, multiplier);
115
+ }
116
+ /**
117
+ * Checks if a send should be gated. Returns {allowed, reason, retryAfterMs?}
118
+ */
119
+ beforeSend() {
120
+ if (!this.config.enabled || !this.throttledSince) {
121
+ return { allowed: true };
122
+ }
123
+ const now = Date.now();
124
+ const multiplier = this.getCurrentMultiplier();
125
+ // If fully ramped up, allow all sends
126
+ if (multiplier >= 1.0) {
127
+ this.throttledSince = null;
128
+ return { allowed: true };
129
+ }
130
+ // Reset window if needed
131
+ if (now - this.currentWindowStart >= this.WINDOW_DURATION_MS) {
132
+ this.sendsInCurrentWindow = 0;
133
+ this.currentWindowStart = now;
134
+ }
135
+ // Calculate budget for current window
136
+ const baselineRate = this.config.baselineRatePerMinute ? this.config.baselineRatePerMinute() : 8;
137
+ const allowedInWindow = Math.floor(baselineRate * multiplier);
138
+ // Check if we're over budget
139
+ if (this.sendsInCurrentWindow >= allowedInWindow) {
140
+ const windowRemaining = this.WINDOW_DURATION_MS - (now - this.currentWindowStart);
141
+ return {
142
+ allowed: false,
143
+ reason: `Post-reconnect throttle: ${Math.floor(multiplier * 100)}% rate (${this.sendsInCurrentWindow}/${allowedInWindow} sends in window)`,
144
+ retryAfterMs: windowRemaining,
145
+ };
146
+ }
147
+ // Allow send and increment counter
148
+ this.sendsInCurrentWindow++;
149
+ this.throttledSendCount++;
150
+ return { allowed: true };
151
+ }
152
+ /**
153
+ * Get current stats
154
+ */
155
+ getStats() {
156
+ const multiplier = this.getCurrentMultiplier();
157
+ const isThrottled = this.throttledSince !== null && multiplier < 1.0;
158
+ const remainingMs = isThrottled && this.throttledSince
159
+ ? Math.max(0, this.config.rampDurationMs - (Date.now() - this.throttledSince))
160
+ : 0;
161
+ return {
162
+ isThrottled,
163
+ currentMultiplier: multiplier,
164
+ throttledSinceMs: this.throttledSince,
165
+ remainingMs,
166
+ throttledSendCount: this.throttledSendCount,
167
+ lifetimeReconnects: this.lifetimeReconnects,
168
+ };
169
+ }
170
+ /**
171
+ * Destroy and clean up timers
172
+ */
173
+ destroy() {
174
+ if (this.rampTimer) {
175
+ clearTimeout(this.rampTimer);
176
+ this.rampTimer = null;
177
+ }
178
+ this.throttledSince = null;
179
+ }
180
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * RetryReasonTracker — Track message retry reasons and detect retry spirals
3
+ *
4
+ * Inspired by whatsapp-rust's protocol/retry.rs module which defines 13 typed
5
+ * RetryReason codes with MAX_RETRY=5 and optimized key-include behavior.
6
+ *
7
+ * In the middleware layer, we can't control key inclusion (that's transport-level),
8
+ * but we CAN observe retry patterns from messages.update events and classify them.
9
+ * High retry rates per reason = ban signal precursor.
10
+ *
11
+ * Usage:
12
+ * const tracker = new RetryReasonTracker({ enabled: true, maxRetries: 5 });
13
+ *
14
+ * // In messages.update handler:
15
+ * tracker.onMessageUpdate(update);
16
+ *
17
+ * // Check for spirals:
18
+ * if (tracker.isSpiraling(msgId)) {
19
+ * console.warn('Message stuck in retry spiral, dropping');
20
+ * }
21
+ *
22
+ * // On successful send:
23
+ * tracker.clear(msgId);
24
+ *
25
+ * // Get stats:
26
+ * const stats = tracker.getStats();
27
+ */
28
+ export type RetryReason = 'no_session' | 'invalid_key' | 'bad_mac' | 'decryption_failure' | 'server_error_463' | 'server_error_429' | 'timeout' | 'no_route' | 'node_malformed' | 'unknown';
29
+ export interface RetryTrackerConfig {
30
+ enabled?: boolean;
31
+ maxRetries?: number;
32
+ spiralThreshold?: number;
33
+ onSpiral?: (msgId: string, reason: RetryReason) => void;
34
+ }
35
+ export interface RetryStats {
36
+ totalRetries: number;
37
+ byReason: Record<RetryReason, number>;
38
+ spiralsDetected: number;
39
+ activeRetries: number;
40
+ }
41
+ export declare class RetryReasonTracker {
42
+ private config;
43
+ private retries;
44
+ private totalRetries;
45
+ private reasonCounts;
46
+ private spiralsDetected;
47
+ constructor(config?: RetryTrackerConfig);
48
+ /**
49
+ * Call when a messages.update event arrives with a status/error.
50
+ * Classifies and records the retry.
51
+ */
52
+ onMessageUpdate(update: {
53
+ key: {
54
+ id?: string;
55
+ };
56
+ status?: number;
57
+ error?: any;
58
+ }): void;
59
+ /**
60
+ * Classify an arbitrary error object into a RetryReason
61
+ */
62
+ classify(err: any): RetryReason;
63
+ /**
64
+ * Record a retry for a message
65
+ */
66
+ private recordRetry;
67
+ /**
68
+ * Should we warn the user this message is spiraling?
69
+ */
70
+ isSpiraling(msgId: string): boolean;
71
+ /**
72
+ * Reset counters for a specific message (call on successful delivery)
73
+ */
74
+ clear(msgId: string): void;
75
+ /**
76
+ * Get current stats
77
+ */
78
+ getStats(): RetryStats;
79
+ /**
80
+ * Clean up old retry records (>5 minutes old)
81
+ */
82
+ private cleanup;
83
+ /**
84
+ * Destroy and clean up
85
+ */
86
+ destroy(): void;
87
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * RetryReasonTracker — Track message retry reasons and detect retry spirals
3
+ *
4
+ * Inspired by whatsapp-rust's protocol/retry.rs module which defines 13 typed
5
+ * RetryReason codes with MAX_RETRY=5 and optimized key-include behavior.
6
+ *
7
+ * In the middleware layer, we can't control key inclusion (that's transport-level),
8
+ * but we CAN observe retry patterns from messages.update events and classify them.
9
+ * High retry rates per reason = ban signal precursor.
10
+ *
11
+ * Usage:
12
+ * const tracker = new RetryReasonTracker({ enabled: true, maxRetries: 5 });
13
+ *
14
+ * // In messages.update handler:
15
+ * tracker.onMessageUpdate(update);
16
+ *
17
+ * // Check for spirals:
18
+ * if (tracker.isSpiraling(msgId)) {
19
+ * console.warn('Message stuck in retry spiral, dropping');
20
+ * }
21
+ *
22
+ * // On successful send:
23
+ * tracker.clear(msgId);
24
+ *
25
+ * // Get stats:
26
+ * const stats = tracker.getStats();
27
+ */
28
+ const DEFAULT_CONFIG = {
29
+ enabled: false,
30
+ maxRetries: 5,
31
+ spiralThreshold: 3,
32
+ onSpiral: () => { },
33
+ };
34
+ export class RetryReasonTracker {
35
+ config;
36
+ retries = new Map();
37
+ totalRetries = 0;
38
+ reasonCounts = {
39
+ no_session: 0,
40
+ invalid_key: 0,
41
+ bad_mac: 0,
42
+ decryption_failure: 0,
43
+ server_error_463: 0,
44
+ server_error_429: 0,
45
+ timeout: 0,
46
+ no_route: 0,
47
+ node_malformed: 0,
48
+ unknown: 0,
49
+ };
50
+ spiralsDetected = 0;
51
+ constructor(config) {
52
+ this.config = { ...DEFAULT_CONFIG, ...config };
53
+ }
54
+ /**
55
+ * Call when a messages.update event arrives with a status/error.
56
+ * Classifies and records the retry.
57
+ */
58
+ onMessageUpdate(update) {
59
+ if (!this.config.enabled)
60
+ return;
61
+ const msgId = update.key?.id;
62
+ if (!msgId)
63
+ return;
64
+ // Only track error statuses
65
+ if (update.status !== 0 && !update.error)
66
+ return;
67
+ const reason = this.classify(update.error || update);
68
+ this.recordRetry(msgId, reason);
69
+ }
70
+ /**
71
+ * Classify an arbitrary error object into a RetryReason
72
+ */
73
+ classify(err) {
74
+ if (!err)
75
+ return 'unknown';
76
+ // Check for HTTP status codes
77
+ const statusCode = err.output?.statusCode || err.statusCode || err.status;
78
+ if (statusCode === 463)
79
+ return 'server_error_463';
80
+ if (statusCode === 429)
81
+ return 'server_error_429';
82
+ // Extract error message/text for pattern matching
83
+ const errorMsg = (err.message || err.text || String(err)).toLowerCase();
84
+ // Pattern matching for known errors
85
+ if (errorMsg.includes('bad mac'))
86
+ return 'bad_mac';
87
+ if (errorMsg.includes('no session') || errorMsg.includes('session not found'))
88
+ return 'no_session';
89
+ if (errorMsg.includes('invalid key') || errorMsg.includes('key error'))
90
+ return 'invalid_key';
91
+ if (errorMsg.includes('decryption') || errorMsg.includes('decrypt'))
92
+ return 'decryption_failure';
93
+ if (errorMsg.includes('timeout') || errorMsg.includes('timed out'))
94
+ return 'timeout';
95
+ if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
96
+ return 'no_route';
97
+ if (errorMsg.includes('malformed') || errorMsg.includes('invalid node'))
98
+ return 'node_malformed';
99
+ return 'unknown';
100
+ }
101
+ /**
102
+ * Record a retry for a message
103
+ */
104
+ recordRetry(msgId, reason) {
105
+ const now = Date.now();
106
+ let record = this.retries.get(msgId);
107
+ if (!record) {
108
+ record = {
109
+ msgId,
110
+ count: 0,
111
+ reasons: [],
112
+ firstRetry: now,
113
+ lastRetry: now,
114
+ };
115
+ this.retries.set(msgId, record);
116
+ }
117
+ record.count++;
118
+ record.reasons.push(reason);
119
+ record.lastRetry = now;
120
+ this.totalRetries++;
121
+ this.reasonCounts[reason]++;
122
+ // Check for spiral
123
+ if (record.count >= this.config.spiralThreshold) {
124
+ this.spiralsDetected++;
125
+ this.config.onSpiral(msgId, reason);
126
+ }
127
+ }
128
+ /**
129
+ * Should we warn the user this message is spiraling?
130
+ */
131
+ isSpiraling(msgId) {
132
+ const record = this.retries.get(msgId);
133
+ return record ? record.count >= this.config.spiralThreshold : false;
134
+ }
135
+ /**
136
+ * Reset counters for a specific message (call on successful delivery)
137
+ */
138
+ clear(msgId) {
139
+ this.retries.delete(msgId);
140
+ }
141
+ /**
142
+ * Get current stats
143
+ */
144
+ getStats() {
145
+ return {
146
+ totalRetries: this.totalRetries,
147
+ byReason: { ...this.reasonCounts },
148
+ spiralsDetected: this.spiralsDetected,
149
+ activeRetries: this.retries.size,
150
+ };
151
+ }
152
+ /**
153
+ * Clean up old retry records (>5 minutes old)
154
+ */
155
+ cleanup() {
156
+ const now = Date.now();
157
+ const maxAge = 5 * 60 * 1000; // 5 minutes
158
+ for (const [msgId, record] of this.retries.entries()) {
159
+ if (now - record.lastRetry > maxAge) {
160
+ this.retries.delete(msgId);
161
+ }
162
+ }
163
+ }
164
+ /**
165
+ * Destroy and clean up
166
+ */
167
+ destroy() {
168
+ this.retries.clear();
169
+ this.cleanup();
170
+ }
171
+ }
package/dist/wrapper.d.ts CHANGED
@@ -1,18 +1,31 @@
1
1
  /**
2
2
  * Socket Wrapper — Drop-in replacement that wraps sendMessage with anti-ban protection
3
3
  *
4
- * Usage:
4
+ * Works with both baileys and @oxidezap/baileyrs transports.
5
+ *
6
+ * Usage with baileys:
5
7
  * import makeWASocket from 'baileys';
6
8
  * import { wrapSocket } from 'baileys-antiban';
7
9
  *
8
10
  * const sock = makeWASocket({ ... });
9
11
  * const safeSock = wrapSocket(sock);
10
12
  *
13
+ * Usage with baileyrs:
14
+ * import { makeWASocket } from '@oxidezap/baileyrs';
15
+ * import { wrapSocket } from 'baileys-antiban';
16
+ *
17
+ * const sock = makeWASocket({ ... });
18
+ * const safeSock = wrapSocket(sock);
19
+ *
11
20
  * // Use safeSock.sendMessage() — automatically rate-limited and monitored
12
21
  * await safeSock.sendMessage(jid, { text: 'Hello!' });
13
22
  *
14
23
  * // Check health anytime
15
24
  * console.log(safeSock.antiban.getStats());
25
+ *
26
+ * Note: reachoutTimeLock timelock module silently noops on baileyrs until upstream
27
+ * emits reachoutTimeLock events — confirmed NOT present in baileyrs v0.0.8.
28
+ * Timelock guard will operate in detection-only mode (relies on 463 errors only).
16
29
  */
17
30
  import { AntiBan, type AntiBanConfig } from './antiban.js';
18
31
  import type { WarmUpState } from './warmup.js';
package/dist/wrapper.js CHANGED
@@ -1,18 +1,31 @@
1
1
  /**
2
2
  * Socket Wrapper — Drop-in replacement that wraps sendMessage with anti-ban protection
3
3
  *
4
- * Usage:
4
+ * Works with both baileys and @oxidezap/baileyrs transports.
5
+ *
6
+ * Usage with baileys:
5
7
  * import makeWASocket from 'baileys';
6
8
  * import { wrapSocket } from 'baileys-antiban';
7
9
  *
8
10
  * const sock = makeWASocket({ ... });
9
11
  * const safeSock = wrapSocket(sock);
10
12
  *
13
+ * Usage with baileyrs:
14
+ * import { makeWASocket } from '@oxidezap/baileyrs';
15
+ * import { wrapSocket } from 'baileys-antiban';
16
+ *
17
+ * const sock = makeWASocket({ ... });
18
+ * const safeSock = wrapSocket(sock);
19
+ *
11
20
  * // Use safeSock.sendMessage() — automatically rate-limited and monitored
12
21
  * await safeSock.sendMessage(jid, { text: 'Hello!' });
13
22
  *
14
23
  * // Check health anytime
15
24
  * console.log(safeSock.antiban.getStats());
25
+ *
26
+ * Note: reachoutTimeLock timelock module silently noops on baileyrs until upstream
27
+ * emits reachoutTimeLock events — confirmed NOT present in baileyrs v0.0.8.
28
+ * Timelock guard will operate in detection-only mode (relies on 463 errors only).
16
29
  */
17
30
  import { AntiBan } from './antiban.js';
18
31
  /**
@@ -50,16 +63,19 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
50
63
  });
51
64
  }
52
65
  }
53
- // Catch 463 errors from message updates
66
+ // Catch 463 errors from message updates + track retries
54
67
  if (events['messages.update']) {
55
68
  const updates = events['messages.update'];
56
69
  for (const update of updates) {
70
+ // 463 error detection
57
71
  if (update?.update?.messageStubParameters) {
58
72
  const params = update.update.messageStubParameters;
59
73
  if (params.includes(463) || params.includes('463')) {
60
74
  antiban.timelock.record463Error();
61
75
  }
62
76
  }
77
+ // Retry tracking
78
+ antiban.retryTracker.onMessageUpdate(update);
63
79
  }
64
80
  }
65
81
  // Register known chats from incoming messages + handle reply suggestions
@@ -120,15 +136,18 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
120
136
  });
121
137
  }
122
138
  });
123
- // Catch 463 errors from message updates
139
+ // Catch 463 errors from message updates + track retries
124
140
  sock.ev.on('messages.update', (updates) => {
125
141
  for (const update of updates) {
142
+ // 463 error detection
126
143
  if (update?.update?.messageStubParameters) {
127
144
  const params = update.update.messageStubParameters;
128
145
  if (params.includes(463) || params.includes('463')) {
129
146
  antiban.timelock.record463Error();
130
147
  }
131
148
  }
149
+ // Retry tracking
150
+ antiban.retryTracker.onMessageUpdate(update);
132
151
  }
133
152
  });
134
153
  // Register known chats from incoming messages + handle reply suggestions
@@ -185,6 +204,10 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
185
204
  const result = await originalSendMessage(jid, content, options);
186
205
  antiban.afterSend(jid, text);
187
206
  antiban.timelock.registerKnownChat(jid);
207
+ // Clear retry tracking on successful send
208
+ if (result?.key?.id) {
209
+ antiban.retryTracker.clear(result.key.id);
210
+ }
188
211
  return result;
189
212
  }
190
213
  catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "1.3.1",
4
- "description": "Anti-ban middleware for Baileys — human-like messaging patterns to protect your WhatsApp number",
3
+ "version": "1.5.0",
4
+ "description": "Transport-agnostic anti-ban middleware for Baileys and baileyrs — human-like messaging patterns to protect your WhatsApp number",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "keywords": [
31
31
  "baileys",
32
+ "baileyrs",
32
33
  "whatsapp",
33
34
  "anti-ban",
34
35
  "rate-limit",
@@ -36,7 +37,8 @@
36
37
  "whatsapp-bot",
37
38
  "bot-protection",
38
39
  "whatsapp-api",
39
- "nodejs"
40
+ "nodejs",
41
+ "transport-agnostic"
40
42
  ],
41
43
  "author": "Kobus Wentzel <kobie@pop.co.za>",
42
44
  "license": "MIT",
@@ -49,13 +51,22 @@
49
51
  },
50
52
  "homepage": "https://github.com/kobie3717/baileys-antiban#readme",
51
53
  "peerDependencies": {
54
+ "@oxidezap/baileyrs": ">=0.0.8",
52
55
  "baileys": ">=6.0.0"
53
56
  },
57
+ "peerDependenciesMeta": {
58
+ "baileys": {
59
+ "optional": true
60
+ },
61
+ "@oxidezap/baileyrs": {
62
+ "optional": true
63
+ }
64
+ },
54
65
  "devDependencies": {
55
66
  "@types/jest": "^29.5.14",
56
67
  "@types/node": "^20.0.0",
57
68
  "jest": "^29.7.0",
58
- "ts-jest": "^29.4.6",
69
+ "ts-jest": "^29.4.9",
59
70
  "tsx": "^4.21.0",
60
71
  "typescript": "^5.0.0"
61
72
  }