baileys-antiban 3.3.0 → 3.4.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,56 @@ 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.4.0] — 2026-04-26
9
+
10
+ ### Added
11
+ - **WPM-based typing duration model** — Realistic typing indicator patterns based on human typing speed
12
+ - `PresenceChoreographer.computeTypingPlan(messageLength)` — Generates realistic typing plan with Gaussian WPM variance
13
+ - `PresenceChoreographer.executeTypingPlan(sock, jid, plan)` — Executes multi-step typing/pause cycle
14
+ - Gaussian sampling (Box-Muller) for WPM variance (default: 45 WPM ± 15 stdDev, clamped 10-120)
15
+ - Think-pause injection: 8% probability per 10 chars, 0.8-3.5s pauses (humans pause mid-thought)
16
+ - Intermittent `paused` state (40% probability) before send for realism
17
+ - Configurable min/max typing duration caps (default: 0.6s - 90s)
18
+ - AbortSignal support for mid-plan cancellation
19
+ - New stats: `typingPlansComputed`, `typingPlansExecuted`, `totalTypingTimeMs`
20
+ - Zero new dependencies — pure TypeScript with Box-Muller transform
21
+
22
+ ### Why v3.4
23
+ WhatsApp's ML models flag accounts that fire `composing` then immediately send, or never fire typing indicators at all. Real humans typing a 200-character message take 30-60 seconds with multiple typing/paused cycles. This is the missing signal layer that completes PresenceChoreographer's anti-detection coverage. The WPM model is the final piece of the presence choreography puzzle — realistic read receipts (v1.3), distraction pauses (v1.3), circadian rhythm (v1.3), and now typing duration.
24
+
25
+ ### Usage
26
+ ```ts
27
+ import { PresenceChoreographer } from 'baileys-antiban';
28
+
29
+ const choreo = new PresenceChoreographer({
30
+ enabled: true,
31
+ enableTypingModel: true,
32
+ typingWPM: 45, // Average human typing speed
33
+ typingWPMStdDev: 15, // Variance (slow/fast days)
34
+ thinkPauseProbability: 0.08,
35
+ thinkPauseMinMs: 800,
36
+ thinkPauseMaxMs: 3500,
37
+ });
38
+
39
+ // Before sending a message
40
+ const messageText = "Hello, how are you doing today?";
41
+ const plan = choreo.computeTypingPlan(messageText.length);
42
+
43
+ // Execute typing plan
44
+ await choreo.executeTypingPlan(sock, jid, plan);
45
+
46
+ // Send actual message
47
+ await sock.sendMessage(jid, { text: messageText });
48
+ ```
49
+
50
+ ### Technical Details
51
+ - Plan structure: `Array<{ state: 'composing' | 'paused', durationMs: number }>`
52
+ - WPM → chars/sec conversion: `(WPM × 5) / 60` (industry standard: 5 chars/word)
53
+ - Think pauses are extras, not subtracted from base typing time (20% budget slack)
54
+ - Composing chunks coalesced when no pause injected between them
55
+ - AbortSignal cleanup: sets presence to `paused` before throwing
56
+ - All existing PresenceChoreographer features remain unchanged and backward compatible
57
+
8
58
  ## [3.3.0] — 2026-04-26
9
59
 
10
60
  ### Added
package/README.md CHANGED
@@ -326,6 +326,42 @@ const antiban = new AntiBan({
326
326
  // No manual intervention needed
327
327
  ```
328
328
 
329
+ #### WPM Typing Model (v3.4+)
330
+
331
+ Real humans typing a 200-character message take 30-60 seconds with multiple `composing`/`paused` cycles. Bots that fire `composing` then immediately send (or never fire it) are detectable.
332
+
333
+ ```typescript
334
+ import { PresenceChoreographer } from 'baileys-antiban';
335
+
336
+ const choreo = new PresenceChoreographer({
337
+ enabled: true,
338
+ enableTypingModel: true,
339
+ typingWPM: 45, // Average human typing speed
340
+ typingWPMStdDev: 15, // Variance (slow/fast days)
341
+ thinkPauseProbability: 0.08, // 8% chance of mid-typing pause per 10 chars
342
+ thinkPauseMinMs: 800,
343
+ thinkPauseMaxMs: 3500,
344
+ });
345
+
346
+ // Before sending
347
+ const messageText = "Hello, how are you doing today?";
348
+ const plan = choreo.computeTypingPlan(messageText.length);
349
+
350
+ // Execute typing plan (sends composing/paused updates)
351
+ await choreo.executeTypingPlan(sock, jid, plan);
352
+
353
+ // Send message
354
+ await sock.sendMessage(jid, { text: messageText });
355
+ ```
356
+
357
+ **How it works:**
358
+ 1. Samples WPM from Gaussian distribution (default: 45 WPM ± 15 stdDev)
359
+ 2. Converts to realistic typing duration: `(messageLength / charsPerSec) * 1000`
360
+ 3. Injects "think pauses" (0.8-3.5s) mid-typing at 8% probability per 10 chars
361
+ 4. Returns plan: `[{ state: 'composing', durationMs: 4200 }, { state: 'paused', durationMs: 950 }, ...]`
362
+ 5. Executes plan: fires `sendPresenceUpdate('composing'/'paused')` + sleeps for each step
363
+ 6. Supports AbortSignal for mid-plan cancellation
364
+
329
365
  **Why these features?** 2025-2026 ban research showed WhatsApp's ML models heavily weight reply-ratio (<10% = high risk), contact-graph distance (strangers = high risk), and temporal patterns (robotic timing = high risk). These modules address the three largest gaps in existing anti-ban libraries.
330
366
 
331
367
  ## baileys-antiban vs Whapi.Cloud vs DIY rate limiting
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { HealthMonitor, type HealthStatus, type HealthMonitorConfig, type BanRis
14
14
  export { TimelockGuard, type TimelockGuardConfig, type TimelockState } from './timelockGuard.js';
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
- export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
17
+ export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats, type TypingPlanStep } from './presenceChoreographer.js';
18
18
  export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type RetryReason } from './retryTracker.js';
19
19
  export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
20
20
  export { LidResolver, type LidResolverConfig, type LidResolverStats, type LidMapping } from './lidResolver.js';
@@ -39,6 +39,24 @@ export interface PresenceChoreographerConfig {
39
39
  offlineGapMinMs?: number;
40
40
  /** Max offline gap duration in ms (default: 900000 = 15min) */
41
41
  offlineGapMaxMs?: number;
42
+ /** Enable WPM-based typing duration model (default: true when enabled) */
43
+ enableTypingModel?: boolean;
44
+ /** Mean typing speed in words per minute (default: 45) */
45
+ typingWPM?: number;
46
+ /** Std dev around the mean WPM (default: 15) — humans vary a lot */
47
+ typingWPMStdDev?: number;
48
+ /** Probability of "thinking pause" mid-typing per 10 chars (default: 0.08) */
49
+ thinkPauseProbability?: number;
50
+ /** Min think pause ms (default: 800) */
51
+ thinkPauseMinMs?: number;
52
+ /** Max think pause ms (default: 3500) */
53
+ thinkPauseMaxMs?: number;
54
+ /** Probability bot ALSO fires 'paused' state mid-cycle (default: 0.4) */
55
+ intermittentPausedProbability?: number;
56
+ /** Cap on total typing duration regardless of message length (default: 90_000 = 90s) */
57
+ typingMaxMs?: number;
58
+ /** Min typing duration even for short messages (default: 600 = 0.6s) */
59
+ typingMinMs?: number;
42
60
  }
43
61
  export interface PresenceChoreographerStats {
44
62
  currentActivityFactor: number;
@@ -47,6 +65,13 @@ export interface PresenceChoreographerStats {
47
65
  readReceiptsDelayed: number;
48
66
  readReceiptsSkipped: number;
49
67
  currentHourLocal: number;
68
+ typingPlansComputed: number;
69
+ typingPlansExecuted: number;
70
+ totalTypingTimeMs: number;
71
+ }
72
+ export interface TypingPlanStep {
73
+ state: 'composing' | 'paused';
74
+ durationMs: number;
50
75
  }
51
76
  export declare class PresenceChoreographer {
52
77
  private config;
@@ -83,6 +108,31 @@ export declare class PresenceChoreographer {
83
108
  mark: boolean;
84
109
  delayMs: number;
85
110
  };
111
+ /**
112
+ * Compute realistic typing duration for a message of given length.
113
+ * Includes Gaussian WPM variance + think-pause injection.
114
+ * Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
115
+ *
116
+ * plan = [
117
+ * { state: 'composing', durationMs: 4200 },
118
+ * { state: 'paused', durationMs: 950 }, // think pause
119
+ * { state: 'composing', durationMs: 6800 },
120
+ * { state: 'paused', durationMs: 600 }, // brief stop before send
121
+ * ]
122
+ */
123
+ computeTypingPlan(messageLength: number): TypingPlanStep[];
124
+ /**
125
+ * Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
126
+ * Awaits each step's duration. Updates stats.
127
+ *
128
+ * await choreo.executeTypingPlan(sock, jid, plan);
129
+ * await sock.sendMessage(jid, content);
130
+ */
131
+ executeTypingPlan(sock: {
132
+ sendPresenceUpdate: (state: string, jid: string) => Promise<void> | void;
133
+ }, jid: string, plan: TypingPlanStep[], options?: {
134
+ signal?: AbortSignal;
135
+ }): Promise<void>;
86
136
  /**
87
137
  * Get statistics.
88
138
  */
@@ -93,4 +143,11 @@ export declare class PresenceChoreographer {
93
143
  reset(): void;
94
144
  private getLocalHour;
95
145
  private randomBetween;
146
+ private clamp;
147
+ /**
148
+ * Generate Gaussian sample using Box-Muller transform.
149
+ * Returns a sample from N(mean, stdDev).
150
+ */
151
+ private gaussianSample;
152
+ private sleep;
96
153
  }
@@ -26,6 +26,15 @@ const DEFAULT_CONFIG = {
26
26
  offlineGapProbability: 0.03,
27
27
  offlineGapMinMs: 300000,
28
28
  offlineGapMaxMs: 900000,
29
+ enableTypingModel: true,
30
+ typingWPM: 45,
31
+ typingWPMStdDev: 15,
32
+ thinkPauseProbability: 0.08,
33
+ thinkPauseMinMs: 800,
34
+ thinkPauseMaxMs: 3500,
35
+ intermittentPausedProbability: 0.4,
36
+ typingMaxMs: 90000,
37
+ typingMinMs: 600,
29
38
  };
30
39
  /**
31
40
  * Activity curves (0.1 to 1.0 multipliers by hour)
@@ -70,6 +79,9 @@ export class PresenceChoreographer {
70
79
  offlineGapsInjected: 0,
71
80
  readReceiptsDelayed: 0,
72
81
  readReceiptsSkipped: 0,
82
+ typingPlansComputed: 0,
83
+ typingPlansExecuted: 0,
84
+ totalTypingTimeMs: 0,
73
85
  };
74
86
  constructor(config = {}) {
75
87
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -136,6 +148,106 @@ export class PresenceChoreographer {
136
148
  this.stats.readReceiptsDelayed++;
137
149
  return { mark: true, delayMs };
138
150
  }
151
+ /**
152
+ * Compute realistic typing duration for a message of given length.
153
+ * Includes Gaussian WPM variance + think-pause injection.
154
+ * Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
155
+ *
156
+ * plan = [
157
+ * { state: 'composing', durationMs: 4200 },
158
+ * { state: 'paused', durationMs: 950 }, // think pause
159
+ * { state: 'composing', durationMs: 6800 },
160
+ * { state: 'paused', durationMs: 600 }, // brief stop before send
161
+ * ]
162
+ */
163
+ computeTypingPlan(messageLength) {
164
+ if (!this.config.enabled || !this.config.enableTypingModel) {
165
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }];
166
+ }
167
+ this.stats.typingPlansComputed++;
168
+ // Handle empty message
169
+ if (messageLength === 0) {
170
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }];
171
+ }
172
+ // 1. Sample WPM from Gaussian distribution
173
+ const wpmSample = this.clamp(this.gaussianSample(this.config.typingWPM, this.config.typingWPMStdDev), 10, 120);
174
+ // 2. Convert to chars/sec (WPM standard: 5 chars/word)
175
+ const cps = (wpmSample * 5) / 60;
176
+ // 3. Base typing time
177
+ const baseMs = (messageLength / cps) * 1000;
178
+ // 4. Clamp to min/max
179
+ const targetMs = this.clamp(baseMs, this.config.typingMinMs, this.config.typingMaxMs);
180
+ // 5. Build plan with think pauses
181
+ const plan = [];
182
+ let remainingBudget = targetMs;
183
+ let position = 0;
184
+ // Walk through message in chunks of 10 chars
185
+ const chunkSize = 10;
186
+ const numChunks = Math.max(1, Math.ceil(messageLength / chunkSize));
187
+ for (let i = 0; i < numChunks && remainingBudget > 0; i++) {
188
+ const charsInChunk = Math.min(chunkSize, messageLength - position);
189
+ // Distribute remaining budget proportionally across remaining chunks
190
+ const remainingChunks = numChunks - i;
191
+ const chunkBudget = remainingBudget / remainingChunks;
192
+ const chunkTypingMs = Math.floor(Math.min(chunkBudget, remainingBudget));
193
+ if (chunkTypingMs <= 0)
194
+ break;
195
+ // Should we inject a think pause after this chunk?
196
+ if (i > 0 && i < numChunks - 1 && Math.random() < this.config.thinkPauseProbability) {
197
+ // Add composing step
198
+ plan.push({ state: 'composing', durationMs: chunkTypingMs });
199
+ remainingBudget -= chunkTypingMs;
200
+ // Add think pause (don't subtract from typing budget)
201
+ const pauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs);
202
+ plan.push({ state: 'paused', durationMs: pauseMs });
203
+ }
204
+ else {
205
+ // Accumulate typing time
206
+ if (plan.length === 0 || plan[plan.length - 1].state === 'paused') {
207
+ plan.push({ state: 'composing', durationMs: chunkTypingMs });
208
+ }
209
+ else {
210
+ plan[plan.length - 1].durationMs += chunkTypingMs;
211
+ }
212
+ remainingBudget -= chunkTypingMs;
213
+ }
214
+ position += charsInChunk;
215
+ }
216
+ // 6. Optional final pause before send
217
+ if (Math.random() < this.config.intermittentPausedProbability) {
218
+ const finalPauseMs = this.randomBetween(200, 800);
219
+ plan.push({ state: 'paused', durationMs: finalPauseMs });
220
+ }
221
+ // Ensure we have at least one composing step
222
+ if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
223
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }];
224
+ }
225
+ return plan;
226
+ }
227
+ /**
228
+ * Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
229
+ * Awaits each step's duration. Updates stats.
230
+ *
231
+ * await choreo.executeTypingPlan(sock, jid, plan);
232
+ * await sock.sendMessage(jid, content);
233
+ */
234
+ async executeTypingPlan(sock, jid, plan, options) {
235
+ this.stats.typingPlansExecuted++;
236
+ for (const step of plan) {
237
+ // Check abort signal
238
+ if (options?.signal?.aborted) {
239
+ // Restore presence to paused before throwing
240
+ await Promise.resolve(sock.sendPresenceUpdate('paused', jid));
241
+ throw new Error('Typing plan aborted');
242
+ }
243
+ // Update presence
244
+ await Promise.resolve(sock.sendPresenceUpdate(step.state, jid));
245
+ // Sleep for duration
246
+ await this.sleep(step.durationMs);
247
+ // Track total typing time
248
+ this.stats.totalTypingTimeMs += step.durationMs;
249
+ }
250
+ }
139
251
  /**
140
252
  * Get statistics.
141
253
  */
@@ -147,6 +259,9 @@ export class PresenceChoreographer {
147
259
  readReceiptsDelayed: this.stats.readReceiptsDelayed,
148
260
  readReceiptsSkipped: this.stats.readReceiptsSkipped,
149
261
  currentHourLocal: this.getLocalHour(),
262
+ typingPlansComputed: this.stats.typingPlansComputed,
263
+ typingPlansExecuted: this.stats.typingPlansExecuted,
264
+ totalTypingTimeMs: this.stats.totalTypingTimeMs,
150
265
  };
151
266
  }
152
267
  /**
@@ -158,6 +273,9 @@ export class PresenceChoreographer {
158
273
  offlineGapsInjected: 0,
159
274
  readReceiptsDelayed: 0,
160
275
  readReceiptsSkipped: 0,
276
+ typingPlansComputed: 0,
277
+ typingPlansExecuted: 0,
278
+ totalTypingTimeMs: 0,
161
279
  };
162
280
  }
163
281
  // Private helpers
@@ -184,4 +302,21 @@ export class PresenceChoreographer {
184
302
  randomBetween(min, max) {
185
303
  return Math.floor(Math.random() * (max - min + 1)) + min;
186
304
  }
305
+ clamp(value, min, max) {
306
+ return Math.max(min, Math.min(max, value));
307
+ }
308
+ /**
309
+ * Generate Gaussian sample using Box-Muller transform.
310
+ * Returns a sample from N(mean, stdDev).
311
+ */
312
+ gaussianSample(mean, stdDev) {
313
+ // Box-Muller transform
314
+ const u1 = Math.random();
315
+ const u2 = Math.random();
316
+ const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
317
+ return mean + z0 * stdDev;
318
+ }
319
+ sleep(ms) {
320
+ return new Promise(resolve => setTimeout(resolve, ms));
321
+ }
187
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
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",