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 +19 -0
- package/README.md +28 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/messageRecovery.js +14 -6
- package/dist/retryTracker.d.ts +1 -0
- package/dist/retryTracker.js +3 -2
- package/dist/stealthConnect.d.ts +117 -23
- package/dist/stealthConnect.js +108 -26
- package/dist/wrapper.js +36 -23
- package/package.json +2 -1
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.
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
614
|
-
|
|
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';
|
package/dist/messageRecovery.js
CHANGED
|
@@ -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
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
before: undefined
|
|
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
|
package/dist/retryTracker.d.ts
CHANGED
package/dist/retryTracker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/dist/stealthConnect.d.ts
CHANGED
|
@@ -1,39 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stealth Connect —
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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: '
|
|
11
|
-
* const sock = makeWASocket({ ...config,
|
|
12
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
* @returns Partial socket config to merge into makeWASocket options
|
|
56
|
+
* Precedence: `browser` > `os` > random pick from `STEALTH_BROWSER_POOL`.
|
|
20
57
|
*/
|
|
21
|
-
export
|
|
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
|
-
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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>;
|
package/dist/stealthConnect.js
CHANGED
|
@@ -1,48 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stealth Connect —
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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: '
|
|
11
|
-
* const sock = makeWASocket({ ...config,
|
|
12
|
-
*
|
|
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
|
|
16
|
-
* Sets markOnlineOnConnect=false and provides sensible browser defaults.
|
|
48
|
+
* Returns a partial Baileys socket config tuned for stealth connect.
|
|
17
49
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
25
|
-
...(opts?.os && { defaultQueryTimeoutMs: undefined }), // placeholder — actual os field lives in auth
|
|
78
|
+
browser,
|
|
26
79
|
};
|
|
27
80
|
}
|
|
28
81
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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.
|
|
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",
|