baileys-antiban 3.8.0 → 3.8.2

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,25 @@ 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.1] - 2026-04-28
9
+
10
+ ### Fixed
11
+ - **`getStealthSocketConfig({ os })` now propagates `os` properly.** v3.8.0 silently dropped the value (placeholder no-op spread). The `os` opt rewrites the first slot of the resulting browser tuple as documented.
12
+ - **Browser fingerprint randomized.** v3.8.0 hardcoded `['Ubuntu', 'Chrome', '20.0.04']` for every consumer — identical fingerprint across the install base is trivially cluster-able by WhatsApp. Now picks at random from `STEALTH_BROWSER_POOL`, a frozen array of realistic Mac/Windows/Linux Chrome/Safari/Firefox/Edge tuples with real-world version strings. Pool is exported so callers can extend or override.
13
+ - **`rampPresenceAfterConnect()` accepts `AbortSignal`.** v3.8.0 had no way to cancel the pending timer; if the socket disconnected during the ramp window the post-delay `sendPresenceUpdate` would run against a dead socket. New `signal` option causes the returned promise to reject with `AbortError` when aborted, and clears the timer.
14
+ - **Structural types instead of `any`.** `sock` parameter now typed as `PresenceCapableSocket` (matches `presenceChoreographer.ts`). `getStealthSocketConfig()` returns a typed `StealthSocketConfig`. Consumers get autocomplete on `markOnlineOnConnect` / `browser`.
15
+ - JSDoc claims corrected — removed phantom `os` default value.
16
+
17
+ ### Added
18
+ - `STEALTH_BROWSER_POOL` (named export) — frozen pool of realistic browser tuples used by `getStealthSocketConfig()`.
19
+ - `AbortError` (named export) — thrown when `rampPresenceAfterConnect` is aborted via signal. Mirrors DOM `AbortError` semantics.
20
+ - New typed exports: `BrowserTuple`, `StealthSocketConfig`, `GetStealthSocketConfigOptions`, `RampPresenceOptions`, `PresenceCapableSocket`.
21
+ - `random` opt on both helpers — inject custom RNG (useful for deterministic tests).
22
+ - `browser` opt on `getStealthSocketConfig()` — supply an explicit tuple. Takes precedence over `os` and the pool.
23
+
24
+ ### Migration
25
+ - v3.8.0 → v3.8.1 is non-breaking. The shape of the returned config is unchanged at runtime; the default browser tuple is now random rather than fixed. If you depended on the exact `['Ubuntu', 'Chrome', '20.0.04']` value, pass it explicitly via `browser: ['Ubuntu', 'Chrome', '20.0.04']`.
26
+
8
27
  ## [3.8.0] - 2026-04-28
9
28
 
10
29
  ### Added
package/README.md CHANGED
@@ -599,19 +599,42 @@ npx baileys-antiban warmup --simulate 7 --preset moderate
599
599
  npx baileys-antiban reset --state ./antiban-state.json
600
600
  ```
601
601
 
602
- ### Stealth Connect (v3.8.0)
602
+ ### Stealth Connect (v3.8.1)
603
603
 
604
- Bots that instantly snap online and start blasting messages look suspicious. Stealth connect delays presence ramp to look more human.
604
+ Bots that instantly snap online and start blasting messages look suspicious. Stealth connect joins WhatsApp without broadcasting `available`, then delays the presence ramp so the socket looks more like a human session.
605
605
 
606
606
  ```typescript
607
607
  import { makeWASocket } from '@whiskeysockets/baileys';
608
- import { getStealthSocketConfig, rampPresenceAfterConnect } from 'baileys-antiban';
608
+ import {
609
+ getStealthSocketConfig,
610
+ rampPresenceAfterConnect,
611
+ AbortError,
612
+ } from 'baileys-antiban';
609
613
 
614
+ // Random fingerprint from STEALTH_BROWSER_POOL + markOnlineOnConnect: false.
615
+ // Pass `os` to rebrand the OS slot, or `browser: [...]` to supply an explicit tuple.
610
616
  const config = getStealthSocketConfig({ os: 'My Custom App' });
611
617
  const sock = makeWASocket({ ...config, auth: state });
612
618
 
613
- // Wait 30-90s, then go available (or fire-and-forget)
614
- await rampPresenceAfterConnect(sock, { minDelayMs: 45000, maxDelayMs: 120000 });
619
+ // Cancel the pending ramp if the socket dies before the timer fires.
620
+ const ac = new AbortController();
621
+ sock.ev.on('connection.update', (u) => {
622
+ if (u.connection === 'close') ac.abort();
623
+ });
624
+
625
+ try {
626
+ await rampPresenceAfterConnect(sock, {
627
+ minDelayMs: 45000,
628
+ maxDelayMs: 120000,
629
+ signal: ac.signal,
630
+ });
631
+ } catch (err) {
632
+ if (err instanceof AbortError) {
633
+ // socket disconnected before ramp fired — swallow
634
+ } else {
635
+ throw err;
636
+ }
637
+ }
615
638
  ```
616
639
 
617
640
  ## Quick Start (Legacy)
package/dist/index.d.ts CHANGED
@@ -37,4 +37,4 @@ export { credsSnapshot, type CredsSnapshot, type CredsSnapshotConfig, } from './
37
37
  export { readReceiptVariance, type ReadReceiptVariance, type ReadReceiptVarianceConfig, } from './readReceiptVariance.js';
38
38
  export { proxyRotator, type ProxyEndpoint, type ProxyRotatorConfig, type ProxyRotatorStats, type ProxyRotatorHandle, } from './proxyRotator.js';
39
39
  export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, type SessionFingerprint, type SessionFingerprintConfig, } from './sessionFingerprint.js';
40
- export { getStealthSocketConfig, rampPresenceAfterConnect, } from './stealthConnect.js';
40
+ export { getStealthSocketConfig, rampPresenceAfterConnect, STEALTH_BROWSER_POOL, AbortError, type BrowserTuple, type StealthSocketConfig, type GetStealthSocketConfigOptions, type RampPresenceOptions, type PresenceCapableSocket, } from './stealthConnect.js';
package/dist/index.js CHANGED
@@ -52,4 +52,4 @@ export { proxyRotator, } from './proxyRotator.js';
52
52
  // v3.6 new modules (Obscura-inspired)
53
53
  export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, } from './sessionFingerprint.js';
54
54
  // v3.8 new modules
55
- export { getStealthSocketConfig, rampPresenceAfterConnect, } from './stealthConnect.js';
55
+ export { getStealthSocketConfig, rampPresenceAfterConnect, STEALTH_BROWSER_POOL, AbortError, } from './stealthConnect.js';
@@ -152,12 +152,20 @@ export function messageRecovery(sock, config) {
152
152
  }
153
153
  for (const [jid, lastSeenEntry] of chatsToRecover) {
154
154
  try {
155
- // Fetch messages newer than lastSeen timestamp
156
- // fetchMessageHistory typically: (jid, count, cursor) => Promise<messages[]>
157
- // We'll fetch up to 50 messages and filter by timestamp
158
- const messages = await sock.fetchMessageHistory(jid, 50, {
159
- before: undefined, // Get latest
160
- });
155
+ // Fetch messages newer than lastSeen timestamp.
156
+ // Baileys v7 may have changed this signature — fall back gracefully on any error.
157
+ let messages;
158
+ try {
159
+ const result = await sock.fetchMessageHistory(jid, 50, { before: undefined });
160
+ messages = Array.isArray(result) ? result : [];
161
+ }
162
+ catch {
163
+ if (!loggedFetchWarning) {
164
+ logger.warn?.(`[messageRecovery] sock.fetchMessageHistory failed — signature may have changed in this Baileys version. Recovery skipped for this reconnect.`);
165
+ loggedFetchWarning = true;
166
+ }
167
+ continue;
168
+ }
161
169
  if (!messages || !Array.isArray(messages))
162
170
  continue;
163
171
  // Filter to messages newer than lastSeen
@@ -54,6 +54,7 @@ export declare class RetryReasonTracker {
54
54
  id?: string;
55
55
  };
56
56
  status?: number;
57
+ update?: any;
57
58
  error?: any;
58
59
  }): void;
59
60
  /**
@@ -61,10 +61,11 @@ export class RetryReasonTracker {
61
61
  const msgId = update.key?.id;
62
62
  if (!msgId)
63
63
  return;
64
- // Only track error statuses
64
+ // Only track error statuses (status 0 = error in Baileys WAMessageStatus)
65
65
  if (update.status !== 0 && !update.error)
66
66
  return;
67
- const reason = this.classify(update.error || update);
67
+ // rc10 may surface error info in update.update (wrapped message); check all forms
68
+ const reason = this.classify(update.error || update.update || update);
68
69
  this.recordRetry(msgId, reason);
69
70
  }
70
71
  /**
@@ -1,39 +1,133 @@
1
1
  /**
2
- * Stealth Connect — Gradual presence ramp to reduce ban signals
2
+ * Stealth Connect — Reduce ban signal on socket connect + presence ramp
3
3
  *
4
4
  * Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
5
- * instantly snap online and start blasting messages look suspicious.
6
- * This helper connects without advertising "online" presence, then
7
- * gradually ramps to "available" after a randomized delay.
5
+ * snap online immediately and start blasting messages look suspicious to
6
+ * WhatsApp's anti-spam classifier. This module ships two helpers:
7
+ *
8
+ * - `getStealthSocketConfig()` returns a partial Baileys socket config
9
+ * that disables `markOnlineOnConnect` and provides a non-default
10
+ * browser fingerprint (random pick from a small pool unless overridden).
11
+ *
12
+ * - `rampPresenceAfterConnect()` waits a randomized delay, then issues
13
+ * `sendPresenceUpdate('available')`. Supports `AbortSignal` so the
14
+ * caller can cancel if the socket dies before the timer fires.
8
15
  *
9
16
  * Usage:
10
- * const config = getStealthSocketConfig({ os: 'My Custom App' });
11
- * const sock = makeWASocket({ ...config, ...otherOptions });
12
- * await rampPresenceAfterConnect(sock, { minDelayMs: 45000, maxDelayMs: 120000 });
17
+ * const config = getStealthSocketConfig({ os: 'MyApp' });
18
+ * const sock = makeWASocket({ ...config, auth: state });
19
+ *
20
+ * const ac = new AbortController();
21
+ * sock.ev.on('connection.update', u => {
22
+ * if (u.connection === 'close') ac.abort();
23
+ * });
24
+ * await rampPresenceAfterConnect(sock, { signal: ac.signal });
25
+ */
26
+ /**
27
+ * Browser tuple expected by Baileys: [appName, browserName, browserVersion].
28
+ */
29
+ export type BrowserTuple = [string, string, string];
30
+ /**
31
+ * Pool of realistic browser fingerprints.
32
+ *
33
+ * Values match formats actually emitted by WhatsApp Web clients in the wild.
34
+ * `getStealthSocketConfig()` picks one at random when caller does not supply
35
+ * an explicit `browser` or `os` option, so multiple consumers of the library
36
+ * do not all advertise an identical fingerprint (which would be trivially
37
+ * cluster-able by WhatsApp).
38
+ *
39
+ * Exported so callers can extend or override the pool if desired.
13
40
  */
41
+ export declare const STEALTH_BROWSER_POOL: readonly BrowserTuple[];
14
42
  /**
15
- * Returns socket configuration for stealth connect.
16
- * Sets markOnlineOnConnect=false and provides sensible browser defaults.
43
+ * Minimum structural shape a stealth socket config consumer needs.
44
+ * Avoids a hard dependency on Baileys' internal type names while still
45
+ * giving consumers TypeScript autocomplete on the keys we actually set.
46
+ */
47
+ export interface StealthSocketConfig {
48
+ /** Whether Baileys broadcasts `available` presence on initial connect. */
49
+ markOnlineOnConnect: boolean;
50
+ /** Browser tuple sent during the WhatsApp Web pairing handshake. */
51
+ browser: BrowserTuple;
52
+ }
53
+ /**
54
+ * Options for `getStealthSocketConfig()`.
17
55
  *
18
- * @param opts.os - Optional custom OS name for device fingerprint (default: 'Baileys')
19
- * @returns Partial socket config to merge into makeWASocket options
56
+ * Precedence: `browser` > `os` > random pick from `STEALTH_BROWSER_POOL`.
20
57
  */
21
- export declare function getStealthSocketConfig(opts?: {
58
+ export interface GetStealthSocketConfigOptions {
59
+ /**
60
+ * Override the OS / app name slot of the browser tuple. If supplied
61
+ * without `browser`, the OS replaces the first element of a randomly
62
+ * picked tuple — `[os, browser, version]`.
63
+ */
22
64
  os?: string;
23
- }): any;
65
+ /**
66
+ * Provide an explicit browser tuple. When set, takes precedence over
67
+ * `os` and the pool. Use this if you have a fingerprint you trust.
68
+ */
69
+ browser?: BrowserTuple;
70
+ /**
71
+ * Custom RNG. Defaults to `Math.random`. Useful for tests.
72
+ */
73
+ random?: () => number;
74
+ }
24
75
  /**
25
- * Ramps presence from unavailable to available after a randomized delay.
26
- * Call this after socket connects. Returns a promise that resolves once
27
- * presence is set. Can be awaited or fire-and-forget.
76
+ * Returns a partial Baileys socket config tuned for stealth connect.
77
+ *
78
+ * - `markOnlineOnConnect` set to `false` so the socket joins without
79
+ * broadcasting `available` (matches GOWA's `presence-on-connect=unavailable`).
80
+ * - `browser` is a randomized realistic tuple from `STEALTH_BROWSER_POOL`
81
+ * unless overridden via `opts.browser` or `opts.os`.
28
82
  *
29
- * @param sock - Baileys socket instance (must have sendPresenceUpdate method)
30
- * @param opts.minDelayMs - Minimum delay in ms (default: 30000 = 30s)
31
- * @param opts.maxDelayMs - Maximum delay in ms (default: 90000 = 90s)
32
- * @param opts.targetState - Presence state to set after delay (default: 'available')
33
- * @returns Promise that resolves when presence is updated
83
+ * Merge the result into your `makeWASocket` options:
84
+ *
85
+ * const sock = makeWASocket({ ...getStealthSocketConfig(), auth: state });
86
+ */
87
+ export declare function getStealthSocketConfig(opts?: GetStealthSocketConfigOptions): StealthSocketConfig;
88
+ /**
89
+ * Minimal structural type for the socket consumed by `rampPresenceAfterConnect`.
90
+ * Matches the shape used elsewhere in this library (e.g. `presenceChoreographer`).
34
91
  */
35
- export declare function rampPresenceAfterConnect(sock: any, opts?: {
92
+ export interface PresenceCapableSocket {
93
+ sendPresenceUpdate: (state: string, jid?: string) => Promise<void> | void;
94
+ }
95
+ /**
96
+ * Options for `rampPresenceAfterConnect()`.
97
+ */
98
+ export interface RampPresenceOptions {
99
+ /** Minimum delay before issuing the presence update, ms. Default: 30000. */
36
100
  minDelayMs?: number;
101
+ /** Maximum delay before issuing the presence update, ms. Default: 90000. */
37
102
  maxDelayMs?: number;
103
+ /** Presence state to set after the delay. Default: `'available'`. */
38
104
  targetState?: 'available' | 'unavailable' | 'composing' | 'recording' | 'paused';
39
- }): Promise<void>;
105
+ /**
106
+ * If set, cancels the pending timer and prevents the presence update.
107
+ * Use this when the socket disconnects before the ramp fires.
108
+ * Aborting causes the returned promise to reject with `AbortError`.
109
+ */
110
+ signal?: AbortSignal;
111
+ /** Custom RNG. Defaults to `Math.random`. Useful for tests. */
112
+ random?: () => number;
113
+ }
114
+ /**
115
+ * Custom error thrown when `rampPresenceAfterConnect` is aborted via signal.
116
+ * Mirrors DOM `AbortError` semantics so consumers can `instanceof` check.
117
+ */
118
+ export declare class AbortError extends Error {
119
+ name: string;
120
+ constructor(message?: string);
121
+ }
122
+ /**
123
+ * Waits a randomized delay then calls `sock.sendPresenceUpdate(targetState)`.
124
+ *
125
+ * Supports `AbortSignal` — abort during the delay window cancels the timer
126
+ * and rejects the returned promise with `AbortError`. Aborting after the
127
+ * presence update has already been sent is a no-op.
128
+ *
129
+ * Caller is responsible for invoking abort when the socket disconnects;
130
+ * otherwise the post-delay `sendPresenceUpdate` may run against a dead
131
+ * socket.
132
+ */
133
+ export declare function rampPresenceAfterConnect(sock: PresenceCapableSocket, opts?: RampPresenceOptions): Promise<void>;
@@ -1,48 +1,130 @@
1
1
  /**
2
- * Stealth Connect — Gradual presence ramp to reduce ban signals
2
+ * Stealth Connect — Reduce ban signal on socket connect + presence ramp
3
3
  *
4
4
  * Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
5
- * instantly snap online and start blasting messages look suspicious.
6
- * This helper connects without advertising "online" presence, then
7
- * gradually ramps to "available" after a randomized delay.
5
+ * snap online immediately and start blasting messages look suspicious to
6
+ * WhatsApp's anti-spam classifier. This module ships two helpers:
7
+ *
8
+ * - `getStealthSocketConfig()` returns a partial Baileys socket config
9
+ * that disables `markOnlineOnConnect` and provides a non-default
10
+ * browser fingerprint (random pick from a small pool unless overridden).
11
+ *
12
+ * - `rampPresenceAfterConnect()` waits a randomized delay, then issues
13
+ * `sendPresenceUpdate('available')`. Supports `AbortSignal` so the
14
+ * caller can cancel if the socket dies before the timer fires.
8
15
  *
9
16
  * Usage:
10
- * const config = getStealthSocketConfig({ os: 'My Custom App' });
11
- * const sock = makeWASocket({ ...config, ...otherOptions });
12
- * await rampPresenceAfterConnect(sock, { minDelayMs: 45000, maxDelayMs: 120000 });
17
+ * const config = getStealthSocketConfig({ os: 'MyApp' });
18
+ * const sock = makeWASocket({ ...config, auth: state });
19
+ *
20
+ * const ac = new AbortController();
21
+ * sock.ev.on('connection.update', u => {
22
+ * if (u.connection === 'close') ac.abort();
23
+ * });
24
+ * await rampPresenceAfterConnect(sock, { signal: ac.signal });
25
+ */
26
+ /**
27
+ * Pool of realistic browser fingerprints.
28
+ *
29
+ * Values match formats actually emitted by WhatsApp Web clients in the wild.
30
+ * `getStealthSocketConfig()` picks one at random when caller does not supply
31
+ * an explicit `browser` or `os` option, so multiple consumers of the library
32
+ * do not all advertise an identical fingerprint (which would be trivially
33
+ * cluster-able by WhatsApp).
34
+ *
35
+ * Exported so callers can extend or override the pool if desired.
13
36
  */
37
+ export const STEALTH_BROWSER_POOL = Object.freeze([
38
+ ['Mac OS', 'Chrome', '120.0.6099.109'],
39
+ ['Mac OS', 'Safari', '17.2.1'],
40
+ ['Windows', 'Chrome', '121.0.6167.85'],
41
+ ['Windows', 'Firefox', '122.0'],
42
+ ['Windows', 'Edge', '120.0.2210.144'],
43
+ ['Linux', 'Chrome', '120.0.6099.109'],
44
+ ['Linux', 'Firefox', '122.0'],
45
+ ['Ubuntu', 'Chrome', '121.0.6167.85'],
46
+ ]);
14
47
  /**
15
- * Returns socket configuration for stealth connect.
16
- * Sets markOnlineOnConnect=false and provides sensible browser defaults.
48
+ * Returns a partial Baileys socket config tuned for stealth connect.
17
49
  *
18
- * @param opts.os - Optional custom OS name for device fingerprint (default: 'Baileys')
19
- * @returns Partial socket config to merge into makeWASocket options
50
+ * - `markOnlineOnConnect` set to `false` so the socket joins without
51
+ * broadcasting `available` (matches GOWA's `presence-on-connect=unavailable`).
52
+ * - `browser` is a randomized realistic tuple from `STEALTH_BROWSER_POOL`
53
+ * unless overridden via `opts.browser` or `opts.os`.
54
+ *
55
+ * Merge the result into your `makeWASocket` options:
56
+ *
57
+ * const sock = makeWASocket({ ...getStealthSocketConfig(), auth: state });
20
58
  */
21
59
  export function getStealthSocketConfig(opts) {
60
+ const random = opts?.random ?? Math.random;
61
+ let browser;
62
+ if (opts?.browser) {
63
+ browser = opts.browser;
64
+ }
65
+ else {
66
+ const pick = STEALTH_BROWSER_POOL[Math.floor(random() * STEALTH_BROWSER_POOL.length)];
67
+ // pick is guaranteed defined — pool is non-empty and frozen.
68
+ const tuple = pick;
69
+ if (opts?.os) {
70
+ browser = [opts.os, tuple[1], tuple[2]];
71
+ }
72
+ else {
73
+ browser = [tuple[0], tuple[1], tuple[2]];
74
+ }
75
+ }
22
76
  return {
23
77
  markOnlineOnConnect: false,
24
- browser: ['Ubuntu', 'Chrome', '20.0.04'],
25
- ...(opts?.os && { defaultQueryTimeoutMs: undefined }), // placeholder — actual os field lives in auth
78
+ browser,
26
79
  };
27
80
  }
28
81
  /**
29
- * Ramps presence from unavailable to available after a randomized delay.
30
- * Call this after socket connects. Returns a promise that resolves once
31
- * presence is set. Can be awaited or fire-and-forget.
32
- *
33
- * @param sock - Baileys socket instance (must have sendPresenceUpdate method)
34
- * @param opts.minDelayMs - Minimum delay in ms (default: 30000 = 30s)
35
- * @param opts.maxDelayMs - Maximum delay in ms (default: 90000 = 90s)
36
- * @param opts.targetState - Presence state to set after delay (default: 'available')
37
- * @returns Promise that resolves when presence is updated
82
+ * Custom error thrown when `rampPresenceAfterConnect` is aborted via signal.
83
+ * Mirrors DOM `AbortError` semantics so consumers can `instanceof` check.
84
+ */
85
+ export class AbortError extends Error {
86
+ name = 'AbortError';
87
+ constructor(message = 'rampPresenceAfterConnect aborted') {
88
+ super(message);
89
+ }
90
+ }
91
+ /**
92
+ * Waits a randomized delay then calls `sock.sendPresenceUpdate(targetState)`.
93
+ *
94
+ * Supports `AbortSignal` — abort during the delay window cancels the timer
95
+ * and rejects the returned promise with `AbortError`. Aborting after the
96
+ * presence update has already been sent is a no-op.
97
+ *
98
+ * Caller is responsible for invoking abort when the socket disconnects;
99
+ * otherwise the post-delay `sendPresenceUpdate` may run against a dead
100
+ * socket.
38
101
  */
39
102
  export async function rampPresenceAfterConnect(sock, opts) {
40
103
  const minDelayMs = opts?.minDelayMs ?? 30000;
41
104
  const maxDelayMs = opts?.maxDelayMs ?? 90000;
42
105
  const targetState = opts?.targetState ?? 'available';
43
- // Random delay in [minDelayMs, maxDelayMs]
44
- const delayMs = Math.floor(Math.random() * (maxDelayMs - minDelayMs + 1)) + minDelayMs;
45
- await new Promise((resolve) => setTimeout(resolve, delayMs));
46
- // Send presence update (undefined = broadcast to all conversations)
106
+ const random = opts?.random ?? Math.random;
107
+ const signal = opts?.signal;
108
+ if (signal?.aborted) {
109
+ throw new AbortError();
110
+ }
111
+ const range = Math.max(0, maxDelayMs - minDelayMs);
112
+ const delayMs = Math.floor(random() * (range + 1)) + minDelayMs;
113
+ await new Promise((resolve, reject) => {
114
+ const timer = setTimeout(() => {
115
+ if (signal) {
116
+ signal.removeEventListener('abort', onAbort);
117
+ }
118
+ resolve();
119
+ }, delayMs);
120
+ const onAbort = () => {
121
+ clearTimeout(timer);
122
+ reject(new AbortError());
123
+ };
124
+ if (signal) {
125
+ signal.addEventListener('abort', onAbort, { once: true });
126
+ }
127
+ });
128
+ // jid undefined => broadcast presence to all conversations
47
129
  await sock.sendPresenceUpdate(targetState, undefined);
48
130
  }
package/dist/wrapper.js CHANGED
@@ -69,7 +69,9 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
69
69
  for (const update of updates) {
70
70
  // 463 error detection
71
71
  if (update?.update?.messageStubParameters) {
72
- const params = update.update.messageStubParameters;
72
+ const params = Array.isArray(update.update.messageStubParameters)
73
+ ? update.update.messageStubParameters
74
+ : [];
73
75
  if (params.includes(463) || params.includes('463')) {
74
76
  antiban.timelock.record463Error();
75
77
  }
@@ -197,6 +199,10 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
197
199
  }
198
200
  // Create proxy that intercepts sendMessage
199
201
  const originalSendMessage = sock.sendMessage.bind(sock);
202
+ // Serializes beforeSend→afterSend so rate limiter accounting is accurate
203
+ // under concurrent sends. Without this, all concurrent callers read the same
204
+ // committed state before any afterSend records, bypassing per-minute limits.
205
+ let sendLock = Promise.resolve();
200
206
  const wrappedSendMessage = async (jid, content, options) => {
201
207
  /**
202
208
  * LID/PN Canonicalization — Normalize JID to canonical form
@@ -211,29 +217,36 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
211
217
  const canonicalJid = antiban.jidCanonicalizer?.canonicalizeTarget(jid) || jid;
212
218
  // Extract text content for rate limiter analysis
213
219
  const text = content?.text || content?.caption || content?.image?.caption || '';
214
- const decision = await antiban.beforeSend(canonicalJid, text);
215
- if (!decision.allowed) {
216
- throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`);
217
- }
218
- // Apply delay
219
- if (decision.delayMs > 0) {
220
- await new Promise(resolve => setTimeout(resolve, decision.delayMs));
221
- }
222
- // Send message (using canonical JID)
223
- try {
224
- const result = await originalSendMessage(canonicalJid, content, options);
225
- antiban.afterSend(canonicalJid, text);
226
- antiban.timelock.registerKnownChat(canonicalJid);
227
- // Clear retry tracking on successful send
228
- if (result?.key?.id) {
229
- antiban.retryTracker.clear(result.key.id);
220
+ // Chain this send onto the previous — each waits for the prior send's
221
+ // afterSend to commit before running its own beforeSend check.
222
+ const sendResult = sendLock.then(async () => {
223
+ const decision = await antiban.beforeSend(canonicalJid, text);
224
+ if (!decision.allowed) {
225
+ throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`);
230
226
  }
231
- return result;
232
- }
233
- catch (error) {
234
- antiban.afterSendFailed(error instanceof Error ? error.message : String(error));
235
- throw error;
236
- }
227
+ // Apply delay
228
+ if (decision.delayMs > 0) {
229
+ await new Promise(resolve => setTimeout(resolve, decision.delayMs));
230
+ }
231
+ // Send message (using canonical JID)
232
+ try {
233
+ const result = await originalSendMessage(canonicalJid, content, options);
234
+ antiban.afterSend(canonicalJid, text);
235
+ antiban.timelock.registerKnownChat(canonicalJid);
236
+ // Clear retry tracking on successful send
237
+ if (result?.key?.id) {
238
+ antiban.retryTracker.clear(result.key.id);
239
+ }
240
+ return result;
241
+ }
242
+ catch (error) {
243
+ antiban.afterSendFailed(error instanceof Error ? error.message : String(error));
244
+ throw error;
245
+ }
246
+ });
247
+ // Advance the lock regardless of success/failure so the chain never stalls
248
+ sendLock = sendResult.then(() => { }, () => { });
249
+ return sendResult;
237
250
  };
238
251
  // Return enhanced socket
239
252
  const wrapped = Object.create(sock);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
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",
@@ -90,6 +90,7 @@
90
90
  "devDependencies": {
91
91
  "@types/jest": "^29.5.14",
92
92
  "@types/node": "^20.0.0",
93
+ "baileys": "github:WhiskeySockets/Baileys#dfad98f815feb771cc561f32707a00c6e085b1f1",
93
94
  "jest": "^29.7.0",
94
95
  "ts-jest": "^29.4.9",
95
96
  "tsx": "^4.21.0",