baileys-antiban 3.8.2 → 3.8.4

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.4] - 2026-05-09
9
+
10
+ ### Added
11
+ - **`DeafSessionDetector`** — detects WebSocket sessions that stay open but stop delivering `messages.upsert` / `messages.update` events (Baileys issue #2491). Root cause: `messageMutex` holding ACKs under Redis latency spikes triggers WhatsApp server-side flow control, silently stopping message delivery while keepAlive pings still succeed. The detector runs a 30-second interval, fires `onDeafSession` callback with silence duration info, and optionally calls `sock.end(new Error('deaf-session'))` for auto-reconnect. Configurable `timeoutMs` (default 5 min), `minUptimeMs` warmup guard (default 2 min), `autoReconnect` flag.
12
+ - **`wrapSocket` `deafSession` option** — pass `deafSession: DeafSessionConfig` to `wrapOptions` to enable automatic deaf-session detection on the wrapped socket. Activity signals are wired to all `messages.upsert` and `messages.update` events; cleanup is tied to `connection.update` close events.
13
+ - New exports: `DeafSessionDetector`, `DeafSessionConfig`, `DeafSessionInfo`.
14
+
15
+ ## [3.8.3] - 2026-05-09
16
+
17
+ ### Fixed
18
+ - **Compiled dist now ships with rc10 fixes.** v3.8.2 published stale dist to npm — the sendLock serialization, `fetchMessageHistory` defensive guard, and `retryTracker` rc10 path were in source but not in the published package. v3.8.3 corrects this.
19
+ - **`sendMessage` concurrent send serialization.** Wraps each send in a `sendLock` promise chain so `beforeSend`→`afterSend` accounting is serialized. Without this, all concurrent callers read the same committed rate-limiter state before any `afterSend` records, allowing burst sends to bypass per-minute limits.
20
+ - **`messageRecovery`: defensive `fetchMessageHistory` guard.** Baileys v7 may change this signature. Now catches any error, logs a one-time warning, and skips recovery for that reconnect rather than crashing the handler.
21
+ - **`retryTracker`: rc10 `update.update` path.** Baileys rc10 wraps error info in `update.update` rather than `update.error` in some cases. Classifier now checks all three forms.
22
+
8
23
  ## [3.8.1] - 2026-04-28
9
24
 
10
25
  ### Fixed
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type Retr
19
19
  export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
20
20
  export { LidResolver, type LidResolverConfig, type LidResolverStats, type LidMapping } from './lidResolver.js';
21
21
  export { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
22
- export { SessionHealthMonitor, type SessionHealthStats, type SessionHealthConfig, wrapWithSessionStability, type SessionStabilityConfig, classifyDisconnect, type DisconnectClassification, type DisconnectCategory, } from './sessionStability.js';
22
+ export { SessionHealthMonitor, type SessionHealthStats, type SessionHealthConfig, wrapWithSessionStability, type SessionStabilityConfig, classifyDisconnect, type DisconnectClassification, type DisconnectCategory, DeafSessionDetector, type DeafSessionConfig, type DeafSessionInfo, } from './sessionStability.js';
23
23
  export { LidFirstResolver, createLidFirstResolver, type LidPhoneMapping, } from './lidFirstResolver.js';
24
24
  export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
25
25
  export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { PostReconnectThrottle } from './reconnectThrottle.js';
24
24
  export { LidResolver } from './lidResolver.js';
25
25
  export { JidCanonicalizer } from './jidCanonicalizer.js';
26
26
  // v2.0 new modules
27
- export { SessionHealthMonitor, wrapWithSessionStability, classifyDisconnect, } from './sessionStability.js';
27
+ export { SessionHealthMonitor, wrapWithSessionStability, classifyDisconnect, DeafSessionDetector, } from './sessionStability.js';
28
28
  // v2.1 new modules
29
29
  export { LidFirstResolver, createLidFirstResolver, } from './lidFirstResolver.js';
30
30
  export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
@@ -76,6 +76,77 @@ export declare class SessionHealthMonitor {
76
76
  */
77
77
  reset(): void;
78
78
  }
79
+ export interface DeafSessionConfig {
80
+ /**
81
+ * How long the session must be silent (no messages.upsert or messages.update)
82
+ * while the WS connection is open before it is declared "deaf".
83
+ * Default: 5 minutes.
84
+ */
85
+ timeoutMs?: number;
86
+ /**
87
+ * Minimum uptime before the detector starts checking.
88
+ * Avoids false positives immediately after a fresh connect.
89
+ * Default: 2 minutes.
90
+ */
91
+ minUptimeMs?: number;
92
+ /**
93
+ * Called when a deaf session is detected, before any auto-reconnect.
94
+ * Use this to log, alert, or run custom recovery logic.
95
+ */
96
+ onDeafSession?: (info: DeafSessionInfo) => void;
97
+ /**
98
+ * If true, call sock.end(new Error('deaf-session')) automatically.
99
+ * Set false if you want to handle reconnection yourself in onDeafSession.
100
+ * Default: true.
101
+ */
102
+ autoReconnect?: boolean;
103
+ }
104
+ export interface DeafSessionInfo {
105
+ /** Timestamp of last observed message activity, or null if none since connect. */
106
+ lastMessageAt: Date | null;
107
+ /** How long the session has been silent in ms. */
108
+ silenceDurationMs: number;
109
+ /** How long the WS has been open in ms. */
110
+ connectedSinceMs: number;
111
+ }
112
+ /**
113
+ * Detects "deaf sessions" — WebSocket connections that stay open but stop
114
+ * delivering messages.upsert / messages.update events.
115
+ *
116
+ * Root cause (Baileys issue #2491): messageMutex holding ACKs hostage under
117
+ * Redis latency spikes causes WhatsApp's server-side flow control to stop
118
+ * delivering messages to that client, while keepAlive pings still succeed.
119
+ *
120
+ * Usage: call onConnect() / onDisconnect() from connection.update events,
121
+ * onMessageActivity() from messages.upsert and messages.update events.
122
+ * Pass a sock reference via attach() so auto-reconnect can call sock.end().
123
+ */
124
+ export declare class DeafSessionDetector {
125
+ private readonly timeoutMs;
126
+ private readonly minUptimeMs;
127
+ private readonly autoReconnect;
128
+ private readonly onDeafSessionCb?;
129
+ private lastMessageAt;
130
+ private connectedAt;
131
+ private timer;
132
+ private sockRef;
133
+ constructor(config?: DeafSessionConfig);
134
+ /** Attach a socket so auto-reconnect can call sock.end() */
135
+ attach(sock: {
136
+ end: (err?: Error) => void;
137
+ }): void;
138
+ /** Call when connection.update → connection === 'open' */
139
+ onConnect(): void;
140
+ /** Call when connection.update → connection === 'close' */
141
+ onDisconnect(): void;
142
+ /** Call on every messages.upsert and messages.update event */
143
+ onMessageActivity(): void;
144
+ /** Release the interval — call when discarding the socket */
145
+ destroy(): void;
146
+ private startTimer;
147
+ private stopTimer;
148
+ private check;
149
+ }
79
150
  export interface SessionStabilityConfig {
80
151
  /** Enable canonical JID normalization before sendMessage (default: true) */
81
152
  canonicalJidNormalization?: boolean;
@@ -207,6 +207,95 @@ export class SessionHealthMonitor {
207
207
  this.badMacTimestamps = [];
208
208
  }
209
209
  }
210
+ /**
211
+ * Detects "deaf sessions" — WebSocket connections that stay open but stop
212
+ * delivering messages.upsert / messages.update events.
213
+ *
214
+ * Root cause (Baileys issue #2491): messageMutex holding ACKs hostage under
215
+ * Redis latency spikes causes WhatsApp's server-side flow control to stop
216
+ * delivering messages to that client, while keepAlive pings still succeed.
217
+ *
218
+ * Usage: call onConnect() / onDisconnect() from connection.update events,
219
+ * onMessageActivity() from messages.upsert and messages.update events.
220
+ * Pass a sock reference via attach() so auto-reconnect can call sock.end().
221
+ */
222
+ export class DeafSessionDetector {
223
+ timeoutMs;
224
+ minUptimeMs;
225
+ autoReconnect;
226
+ onDeafSessionCb;
227
+ lastMessageAt = null;
228
+ connectedAt = null;
229
+ timer = null;
230
+ sockRef = null;
231
+ constructor(config = {}) {
232
+ this.timeoutMs = config.timeoutMs ?? 5 * 60_000;
233
+ this.minUptimeMs = config.minUptimeMs ?? 2 * 60_000;
234
+ this.autoReconnect = config.autoReconnect ?? true;
235
+ this.onDeafSessionCb = config.onDeafSession;
236
+ }
237
+ /** Attach a socket so auto-reconnect can call sock.end() */
238
+ attach(sock) {
239
+ this.sockRef = sock;
240
+ }
241
+ /** Call when connection.update → connection === 'open' */
242
+ onConnect() {
243
+ this.connectedAt = Date.now();
244
+ this.lastMessageAt = Date.now(); // reset — fresh connect is not silence
245
+ this.startTimer();
246
+ }
247
+ /** Call when connection.update → connection === 'close' */
248
+ onDisconnect() {
249
+ this.connectedAt = null;
250
+ this.stopTimer();
251
+ }
252
+ /** Call on every messages.upsert and messages.update event */
253
+ onMessageActivity() {
254
+ this.lastMessageAt = Date.now();
255
+ }
256
+ /** Release the interval — call when discarding the socket */
257
+ destroy() {
258
+ this.stopTimer();
259
+ this.sockRef = null;
260
+ }
261
+ startTimer() {
262
+ this.stopTimer();
263
+ this.timer = setInterval(() => this.check(), 30_000);
264
+ }
265
+ stopTimer() {
266
+ if (this.timer !== null) {
267
+ clearInterval(this.timer);
268
+ this.timer = null;
269
+ }
270
+ }
271
+ check() {
272
+ if (this.connectedAt === null)
273
+ return;
274
+ const now = Date.now();
275
+ const connectedSinceMs = now - this.connectedAt;
276
+ if (connectedSinceMs < this.minUptimeMs)
277
+ return;
278
+ const silenceDurationMs = now - (this.lastMessageAt ?? this.connectedAt);
279
+ if (silenceDurationMs < this.timeoutMs)
280
+ return;
281
+ const info = {
282
+ lastMessageAt: this.lastMessageAt !== null ? new Date(this.lastMessageAt) : null,
283
+ silenceDurationMs,
284
+ connectedSinceMs,
285
+ };
286
+ this.onDeafSessionCb?.(info);
287
+ if (this.autoReconnect && this.sockRef) {
288
+ try {
289
+ this.sockRef.end(new Error('deaf-session'));
290
+ }
291
+ catch {
292
+ // socket may already be closing
293
+ }
294
+ }
295
+ // Stop checking after triggering — let onConnect reset on next reconnect
296
+ this.stopTimer();
297
+ }
298
+ }
210
299
  /**
211
300
  * Wrap a Baileys socket with session stability features.
212
301
  * Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
package/dist/wrapper.d.ts CHANGED
@@ -28,6 +28,7 @@
28
28
  * Timelock guard will operate in detection-only mode (relies on 463 errors only).
29
29
  */
30
30
  import { AntiBan, type AntiBanConfig } from './antiban.js';
31
+ import { type DeafSessionConfig } from './sessionStability.js';
31
32
  import type { WarmUpState } from './warmup.js';
32
33
  export type WASocket = {
33
34
  sendMessage: (jid: string, content: any, options?: any) => Promise<any>;
@@ -44,6 +45,12 @@ export type WASocket = {
44
45
  export interface WrapSocketOptions {
45
46
  /** Auto-respond to incoming messages when reply ratio suggests it (default: false) */
46
47
  autoRespondToIncoming?: boolean;
48
+ /**
49
+ * Deaf session detection — monitors for WS connections that stop delivering
50
+ * messages while keepAlive pings still succeed (Baileys issue #2491).
51
+ * Pass a config object to enable; omit to disable.
52
+ */
53
+ deafSession?: DeafSessionConfig;
47
54
  }
48
55
  export type WrappedSocket<T extends WASocket = WASocket> = T & {
49
56
  antiban: AntiBan;
package/dist/wrapper.js CHANGED
@@ -28,6 +28,7 @@
28
28
  * Timelock guard will operate in detection-only mode (relies on 463 errors only).
29
29
  */
30
30
  import { AntiBan } from './antiban.js';
31
+ import { DeafSessionDetector } from './sessionStability.js';
31
32
  /**
32
33
  * Wrap a Baileys socket with anti-ban protection.
33
34
  * The returned socket has the same API but sendMessage() is protected.
@@ -38,6 +39,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
38
39
  autoRespondToIncoming: false,
39
40
  ...wrapOptions,
40
41
  };
42
+ // Deaf session detector — optional, enabled via wrapOptions.deafSession
43
+ const deafDetector = options.deafSession
44
+ ? new DeafSessionDetector(options.deafSession)
45
+ : null;
46
+ if (deafDetector)
47
+ deafDetector.attach(sock);
41
48
  // Hook into Baileys events for health monitoring
42
49
  // Prefer ev.process() (Baileys ≥ late 2022) for batched event handling
43
50
  // Fall back to ev.on() for older versions
@@ -50,9 +57,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
50
57
  const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
51
58
  antiban.onDisconnect(reason);
52
59
  antiban.destroy(); // Clean up all timers
60
+ deafDetector?.onDisconnect();
61
+ deafDetector?.destroy();
53
62
  }
54
63
  if (update.connection === 'open') {
55
64
  antiban.onReconnect();
65
+ deafDetector?.onConnect();
56
66
  }
57
67
  // Reachout timelock detection
58
68
  if (update.reachoutTimeLock) {
@@ -66,6 +76,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
66
76
  // Catch 463 errors from message updates + track retries + learn LID mappings
67
77
  if (events['messages.update']) {
68
78
  const updates = events['messages.update'];
79
+ deafDetector?.onMessageActivity();
69
80
  for (const update of updates) {
70
81
  // 463 error detection
71
82
  if (update?.update?.messageStubParameters) {
@@ -85,6 +96,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
85
96
  // Register known chats from incoming messages + handle reply suggestions + learn LID mappings
86
97
  if (events['messages.upsert']) {
87
98
  const { messages } = events['messages.upsert'];
99
+ deafDetector?.onMessageActivity();
88
100
  // Learn LID mappings FIRST (before any other processing)
89
101
  antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert']);
90
102
  for (const msg of messages || []) {
@@ -129,9 +141,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
129
141
  const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
130
142
  antiban.onDisconnect(reason);
131
143
  antiban.destroy(); // Clean up all timers
144
+ deafDetector?.onDisconnect();
145
+ deafDetector?.destroy();
132
146
  }
133
147
  if (update.connection === 'open') {
134
148
  antiban.onReconnect();
149
+ deafDetector?.onConnect();
135
150
  }
136
151
  // Reachout timelock detection
137
152
  if (update.reachoutTimeLock) {
@@ -144,6 +159,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
144
159
  });
145
160
  // Catch 463 errors from message updates + track retries + learn LID mappings
146
161
  sock.ev.on('messages.update', (updates) => {
162
+ deafDetector?.onMessageActivity();
147
163
  for (const update of updates) {
148
164
  // 463 error detection
149
165
  if (update?.update?.messageStubParameters) {
@@ -161,6 +177,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
161
177
  // Register known chats from incoming messages + handle reply suggestions + learn LID mappings
162
178
  sock.ev.on('messages.upsert', (upsert) => {
163
179
  const { messages } = upsert;
180
+ deafDetector?.onMessageActivity();
164
181
  // Learn LID mappings FIRST (before any other processing)
165
182
  antiban.jidCanonicalizer?.onIncomingEvent(upsert);
166
183
  for (const msg of messages || []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "3.8.2",
3
+ "version": "3.8.4",
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",