baileys-antiban 3.7.0 → 3.8.1

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,33 @@ 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
+
27
+ ## [3.8.0] - 2026-04-28
28
+
29
+ ### Added
30
+ - **Stealth Connect** — Gradual presence ramp to reduce ban signals. Inspired by GOWA's `--presence-on-connect=unavailable` flag. Bots that instantly snap online and start blasting messages look suspicious. New helpers:
31
+ - `getStealthSocketConfig({ os?: string })` — returns socket config with `markOnlineOnConnect: false` and sensible browser defaults.
32
+ - `rampPresenceAfterConnect(sock, { minDelayMs?, maxDelayMs?, targetState? })` — waits 30-90s (configurable), then transitions presence to `available` (or custom state). Call after socket connects. Returns a promise.
33
+ - Use together: connect silently, ramp presence gradually when ready to act.
34
+
8
35
  ## [3.7.0] - 2026-04-27
9
36
 
10
37
  ### Added
package/README.md CHANGED
@@ -599,6 +599,44 @@ 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.1)
603
+
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
+
606
+ ```typescript
607
+ import { makeWASocket } from '@whiskeysockets/baileys';
608
+ import {
609
+ getStealthSocketConfig,
610
+ rampPresenceAfterConnect,
611
+ AbortError,
612
+ } from 'baileys-antiban';
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.
616
+ const config = getStealthSocketConfig({ os: 'My Custom App' });
617
+ const sock = makeWASocket({ ...config, auth: state });
618
+
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
+ }
638
+ ```
639
+
602
640
  ## Quick Start (Legacy)
603
641
 
604
642
  ### Option 1: Wrap Your Socket (Easiest)
package/dist/index.d.ts CHANGED
@@ -36,3 +36,5 @@ export { generateFingerprint, applyFingerprint, type DeviceFingerprint, type Dev
36
36
  export { credsSnapshot, type CredsSnapshot, type CredsSnapshotConfig, } from './credsSnapshot.js';
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
+ export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, type SessionFingerprint, type SessionFingerprintConfig, } from './sessionFingerprint.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
@@ -49,3 +49,7 @@ export { credsSnapshot, } from './credsSnapshot.js';
49
49
  export { readReceiptVariance, } from './readReceiptVariance.js';
50
50
  // v3.5 new modules
51
51
  export { proxyRotator, } from './proxyRotator.js';
52
+ // v3.6 new modules (Obscura-inspired)
53
+ export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, } from './sessionFingerprint.js';
54
+ // v3.8 new modules
55
+ export { getStealthSocketConfig, rampPresenceAfterConnect, STEALTH_BROWSER_POOL, AbortError, } from './stealthConnect.js';
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Session Fingerprint Randomization (Obscura-inspired)
3
+ *
4
+ * Per-session fingerprint randomization to prevent device tracking.
5
+ * Scavenged patterns from Obscura headless browser's stealth mode.
6
+ *
7
+ * Key principles from Obscura:
8
+ * 1. Per-session randomization (not per-request)
9
+ * 2. Consistent within session (same session = same fingerprint)
10
+ * 3. Feature-flag pattern for optional anti-detection
11
+ * 4. Emulation of real device profiles (not synthetic values)
12
+ *
13
+ * Browser fingerprint → WhatsApp signal mapping:
14
+ * - TLS fingerprint → WA protocol version
15
+ * - Canvas noise → message timing jitter
16
+ * - Audio fingerprint → voice note metadata
17
+ * - GPU info → device model/brand
18
+ * - Battery → connection state variation
19
+ * - User agent → WA client version
20
+ *
21
+ * @author Kobus Wentzel <kobie@pop.co.za>
22
+ * @license MIT
23
+ */
24
+ import { type DeviceFingerprint } from './deviceFingerprint.js';
25
+ export interface SessionFingerprintConfig {
26
+ /** Master switch for enhanced fingerprinting */
27
+ enabled?: boolean;
28
+ /** Device profile randomization (from deviceFingerprint.ts) */
29
+ deviceProfile?: {
30
+ randomizeAppVersion?: boolean;
31
+ randomizeOsVersion?: boolean;
32
+ randomizeDeviceModel?: boolean;
33
+ appVersionPool?: number[][];
34
+ osVersionPool?: string[];
35
+ deviceModelPool?: string[];
36
+ };
37
+ /** Network timing variance (anti-pattern detection) */
38
+ networkTiming?: {
39
+ /** Add jitter to message send timing (ms) */
40
+ sendJitterMs?: [number, number];
41
+ /** Add jitter to typing indicators (ms) */
42
+ typingJitterMs?: [number, number];
43
+ /** Vary connection retry backoff */
44
+ retryJitterMs?: [number, number];
45
+ };
46
+ /** Voice note metadata randomization */
47
+ voiceNote?: {
48
+ /** Vary waveform pattern slightly */
49
+ randomizeWaveform?: boolean;
50
+ /** Vary duration by small amount (ms) */
51
+ durationJitterMs?: number;
52
+ /** Randomize sample rate from pool */
53
+ sampleRatePool?: number[];
54
+ };
55
+ /** Connection state variance */
56
+ connectionState?: {
57
+ /** Vary idle timeout */
58
+ idleTimeoutJitterMs?: [number, number];
59
+ /** Vary keepalive interval */
60
+ keepaliveJitterMs?: [number, number];
61
+ /** Randomize battery state reported */
62
+ randomizeBattery?: boolean;
63
+ /** Battery level pool (0-100) */
64
+ batteryLevelPool?: number[];
65
+ };
66
+ /** Protocol version variance */
67
+ protocolVersion?: {
68
+ /** Randomize protocol sub-version */
69
+ randomizeSubVersion?: boolean;
70
+ /** Protocol version pool (e.g., different patch versions) */
71
+ versionPool?: string[];
72
+ };
73
+ /** Seed for deterministic randomization (testing/debugging) */
74
+ seed?: string;
75
+ }
76
+ export interface SessionFingerprint {
77
+ /** Core device profile */
78
+ device: DeviceFingerprint;
79
+ /** Network timing variances (stable per session) */
80
+ networkTiming: {
81
+ sendJitterMs: number;
82
+ typingJitterMs: number;
83
+ retryJitterMs: number;
84
+ };
85
+ /** Voice note profile */
86
+ voiceNote: {
87
+ waveformSeed: number;
88
+ durationJitterMs: number;
89
+ sampleRate: number;
90
+ };
91
+ /** Connection state profile */
92
+ connectionState: {
93
+ idleTimeoutMs: number;
94
+ keepaliveMs: number;
95
+ batteryLevel: number;
96
+ batteryCharging: boolean;
97
+ };
98
+ /** Protocol version */
99
+ protocolVersion: string;
100
+ /** Session identifier (stable for this fingerprint) */
101
+ sessionId: string;
102
+ /** Timestamp when fingerprint was generated */
103
+ createdAt: number;
104
+ }
105
+ /**
106
+ * Generate a comprehensive session fingerprint.
107
+ * Call once per session (socket initialization).
108
+ *
109
+ * Obscura pattern: consistent per session, randomized across sessions.
110
+ */
111
+ export declare function generateSessionFingerprint(config?: SessionFingerprintConfig, sessionId?: string): SessionFingerprint;
112
+ /**
113
+ * Apply session fingerprint to Baileys socket config.
114
+ *
115
+ * Usage:
116
+ * const fingerprint = generateSessionFingerprint({ enabled: true });
117
+ * const sock = makeWASocket(applySessionFingerprint(config, fingerprint));
118
+ */
119
+ export declare function applySessionFingerprint(socketConfig: any, fingerprint: SessionFingerprint): any;
120
+ /**
121
+ * Get timing jitter for message send (helper for presenceChoreographer/rateLimiter)
122
+ *
123
+ * Usage in beforeSend():
124
+ * const jitter = getMessageSendJitter(fingerprint);
125
+ * await sleep(baseDelay + jitter);
126
+ */
127
+ export declare function getMessageSendJitter(fingerprint: SessionFingerprint): number;
128
+ /**
129
+ * Get typing indicator jitter (helper for presenceChoreographer)
130
+ */
131
+ export declare function getTypingJitter(fingerprint: SessionFingerprint): number;
132
+ /**
133
+ * Get retry backoff jitter (helper for reconnectThrottle)
134
+ */
135
+ export declare function getRetryJitter(fingerprint: SessionFingerprint): number;
136
+ /**
137
+ * Get voice note metadata (helper for voice message encoding)
138
+ *
139
+ * Returns suggested sample rate and duration adjustment based on session fingerprint.
140
+ */
141
+ export declare function getVoiceNoteMetadata(fingerprint: SessionFingerprint): {
142
+ sampleRate: number;
143
+ durationJitterMs: number;
144
+ waveformSeed: number;
145
+ };
146
+ /**
147
+ * Get battery state (helper for presence/connection state signals)
148
+ */
149
+ export declare function getBatteryState(fingerprint: SessionFingerprint): {
150
+ level: number;
151
+ charging: boolean;
152
+ };
153
+ /**
154
+ * Create a session fingerprint preset (Obscura-inspired feature flag pattern)
155
+ */
156
+ export declare function createStealthFingerprint(sessionId?: string): SessionFingerprint;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Session Fingerprint Randomization (Obscura-inspired)
3
+ *
4
+ * Per-session fingerprint randomization to prevent device tracking.
5
+ * Scavenged patterns from Obscura headless browser's stealth mode.
6
+ *
7
+ * Key principles from Obscura:
8
+ * 1. Per-session randomization (not per-request)
9
+ * 2. Consistent within session (same session = same fingerprint)
10
+ * 3. Feature-flag pattern for optional anti-detection
11
+ * 4. Emulation of real device profiles (not synthetic values)
12
+ *
13
+ * Browser fingerprint → WhatsApp signal mapping:
14
+ * - TLS fingerprint → WA protocol version
15
+ * - Canvas noise → message timing jitter
16
+ * - Audio fingerprint → voice note metadata
17
+ * - GPU info → device model/brand
18
+ * - Battery → connection state variation
19
+ * - User agent → WA client version
20
+ *
21
+ * @author Kobus Wentzel <kobie@pop.co.za>
22
+ * @license MIT
23
+ */
24
+ import { generateFingerprint } from './deviceFingerprint.js';
25
+ /**
26
+ * Simple deterministic PRNG using mulberry32
27
+ * Same as deviceFingerprint.ts for consistency
28
+ */
29
+ class SeededRandom {
30
+ state;
31
+ constructor(seed) {
32
+ let hash = 0;
33
+ for (let i = 0; i < seed.length; i++) {
34
+ hash = (hash << 5) - hash + seed.charCodeAt(i);
35
+ hash = hash & hash;
36
+ }
37
+ this.state = Math.abs(hash) || 1;
38
+ }
39
+ next() {
40
+ let t = (this.state += 0x6d2b79f5);
41
+ t = Math.imul(t ^ (t >>> 15), t | 1);
42
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
43
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
44
+ }
45
+ range(min, max) {
46
+ return Math.floor(this.next() * (max - min + 1)) + min;
47
+ }
48
+ rangeFloat(min, max) {
49
+ return this.next() * (max - min) + min;
50
+ }
51
+ pick(array) {
52
+ return array[Math.floor(this.next() * array.length)];
53
+ }
54
+ boolean(probability = 0.5) {
55
+ return this.next() < probability;
56
+ }
57
+ }
58
+ // Default configuration values (conservative, realistic)
59
+ const DEFAULT_SEND_JITTER_MS = [50, 300];
60
+ const DEFAULT_TYPING_JITTER_MS = [30, 150];
61
+ const DEFAULT_RETRY_JITTER_MS = [100, 500];
62
+ const DEFAULT_DURATION_JITTER_MS = 200;
63
+ const DEFAULT_SAMPLE_RATE_POOL = [8000, 16000, 44100, 48000];
64
+ const DEFAULT_IDLE_TIMEOUT_JITTER_MS = [25000, 35000];
65
+ const DEFAULT_KEEPALIVE_JITTER_MS = [15000, 25000];
66
+ const DEFAULT_BATTERY_LEVEL_POOL = [20, 35, 50, 65, 80, 95, 100];
67
+ const DEFAULT_PROTOCOL_VERSION_POOL = ['2.24.5', '2.24.4', '2.24.3'];
68
+ /**
69
+ * Generate a comprehensive session fingerprint.
70
+ * Call once per session (socket initialization).
71
+ *
72
+ * Obscura pattern: consistent per session, randomized across sessions.
73
+ */
74
+ export function generateSessionFingerprint(config = {}, sessionId) {
75
+ const { enabled = true, deviceProfile = {}, networkTiming = {}, voiceNote = {}, connectionState = {}, protocolVersion = {}, seed, } = config;
76
+ const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`;
77
+ const rng = new SeededRandom(seed || finalSessionId);
78
+ // Generate base device fingerprint (delegates to deviceFingerprint.ts)
79
+ const device = generateFingerprint({
80
+ enabled,
81
+ randomizeAppVersion: deviceProfile.randomizeAppVersion ?? true,
82
+ randomizeOsVersion: deviceProfile.randomizeOsVersion ?? true,
83
+ randomizeDeviceModel: deviceProfile.randomizeDeviceModel ?? true,
84
+ seed: seed || finalSessionId,
85
+ appVersionPool: deviceProfile.appVersionPool,
86
+ osVersionPool: deviceProfile.osVersionPool,
87
+ deviceModelPool: deviceProfile.deviceModelPool,
88
+ }, finalSessionId);
89
+ // Network timing variances (Obscura: prevent timing pattern detection)
90
+ const sendJitterRange = networkTiming.sendJitterMs || DEFAULT_SEND_JITTER_MS;
91
+ const typingJitterRange = networkTiming.typingJitterMs || DEFAULT_TYPING_JITTER_MS;
92
+ const retryJitterRange = networkTiming.retryJitterMs || DEFAULT_RETRY_JITTER_MS;
93
+ const networkTimingProfile = enabled
94
+ ? {
95
+ sendJitterMs: rng.range(sendJitterRange[0], sendJitterRange[1]),
96
+ typingJitterMs: rng.range(typingJitterRange[0], typingJitterRange[1]),
97
+ retryJitterMs: rng.range(retryJitterRange[0], retryJitterRange[1]),
98
+ }
99
+ : {
100
+ sendJitterMs: 0,
101
+ typingJitterMs: 0,
102
+ retryJitterMs: 0,
103
+ };
104
+ // Voice note profile (Obscura: audio fingerprint variance)
105
+ const sampleRatePool = voiceNote.sampleRatePool || DEFAULT_SAMPLE_RATE_POOL;
106
+ const voiceNoteProfile = {
107
+ waveformSeed: enabled ? rng.range(0, 2147483647) : 0,
108
+ durationJitterMs: enabled && voiceNote.randomizeWaveform !== false
109
+ ? rng.range(0, voiceNote.durationJitterMs || DEFAULT_DURATION_JITTER_MS)
110
+ : 0,
111
+ sampleRate: enabled ? rng.pick(sampleRatePool) : sampleRatePool[0],
112
+ };
113
+ // Connection state profile (Obscura: battery/network state variance)
114
+ const idleTimeoutRange = connectionState.idleTimeoutJitterMs || DEFAULT_IDLE_TIMEOUT_JITTER_MS;
115
+ const keepaliveRange = connectionState.keepaliveJitterMs || DEFAULT_KEEPALIVE_JITTER_MS;
116
+ const batteryLevelPool = connectionState.batteryLevelPool || DEFAULT_BATTERY_LEVEL_POOL;
117
+ const connectionStateProfile = {
118
+ idleTimeoutMs: enabled
119
+ ? rng.range(idleTimeoutRange[0], idleTimeoutRange[1])
120
+ : 30000,
121
+ keepaliveMs: enabled
122
+ ? rng.range(keepaliveRange[0], keepaliveRange[1])
123
+ : 20000,
124
+ batteryLevel: enabled && connectionState.randomizeBattery !== false
125
+ ? rng.pick(batteryLevelPool)
126
+ : 100,
127
+ batteryCharging: enabled ? rng.boolean(0.3) : false, // 30% charging probability
128
+ };
129
+ // Protocol version (Obscura: TLS fingerprint variance → WA protocol variance)
130
+ const versionPool = protocolVersion.versionPool || DEFAULT_PROTOCOL_VERSION_POOL;
131
+ const protocolVersionStr = enabled && protocolVersion.randomizeSubVersion !== false
132
+ ? rng.pick(versionPool)
133
+ : versionPool[0];
134
+ return {
135
+ device,
136
+ networkTiming: networkTimingProfile,
137
+ voiceNote: voiceNoteProfile,
138
+ connectionState: connectionStateProfile,
139
+ protocolVersion: protocolVersionStr,
140
+ sessionId: finalSessionId,
141
+ createdAt: Date.now(),
142
+ };
143
+ }
144
+ /**
145
+ * Apply session fingerprint to Baileys socket config.
146
+ *
147
+ * Usage:
148
+ * const fingerprint = generateSessionFingerprint({ enabled: true });
149
+ * const sock = makeWASocket(applySessionFingerprint(config, fingerprint));
150
+ */
151
+ export function applySessionFingerprint(socketConfig, fingerprint) {
152
+ const config = { ...socketConfig };
153
+ // Apply device profile (appVersion, browser tuple)
154
+ config.version = fingerprint.device.appVersion;
155
+ config.browser = [
156
+ fingerprint.device.deviceModel,
157
+ fingerprint.device.osVersion,
158
+ `WhatsApp/${fingerprint.device.appVersion.join('.')}`,
159
+ ];
160
+ // Apply connection timeouts if fields exist
161
+ if ('connectTimeoutMs' in config || config.connectTimeoutMs !== undefined) {
162
+ // Add idle timeout jitter to connection config
163
+ config.connectTimeoutMs = fingerprint.connectionState.idleTimeoutMs;
164
+ }
165
+ // Apply keepalive if supported
166
+ if ('keepAliveIntervalMs' in config || config.keepAliveIntervalMs !== undefined) {
167
+ config.keepAliveIntervalMs = fingerprint.connectionState.keepaliveMs;
168
+ }
169
+ // Store fingerprint for runtime access (helpers can read this)
170
+ config.__sessionFingerprint = fingerprint;
171
+ return config;
172
+ }
173
+ /**
174
+ * Get timing jitter for message send (helper for presenceChoreographer/rateLimiter)
175
+ *
176
+ * Usage in beforeSend():
177
+ * const jitter = getMessageSendJitter(fingerprint);
178
+ * await sleep(baseDelay + jitter);
179
+ */
180
+ export function getMessageSendJitter(fingerprint) {
181
+ // Return a random value within ±50% of the session's base jitter
182
+ // This adds per-message variance while staying within session profile
183
+ const base = fingerprint.networkTiming.sendJitterMs;
184
+ return Math.floor(base * 0.5 + Math.random() * base * 0.5);
185
+ }
186
+ /**
187
+ * Get typing indicator jitter (helper for presenceChoreographer)
188
+ */
189
+ export function getTypingJitter(fingerprint) {
190
+ const base = fingerprint.networkTiming.typingJitterMs;
191
+ return Math.floor(base * 0.5 + Math.random() * base * 0.5);
192
+ }
193
+ /**
194
+ * Get retry backoff jitter (helper for reconnectThrottle)
195
+ */
196
+ export function getRetryJitter(fingerprint) {
197
+ const base = fingerprint.networkTiming.retryJitterMs;
198
+ return Math.floor(base * 0.5 + Math.random() * base * 0.5);
199
+ }
200
+ /**
201
+ * Get voice note metadata (helper for voice message encoding)
202
+ *
203
+ * Returns suggested sample rate and duration adjustment based on session fingerprint.
204
+ */
205
+ export function getVoiceNoteMetadata(fingerprint) {
206
+ return {
207
+ sampleRate: fingerprint.voiceNote.sampleRate,
208
+ durationJitterMs: fingerprint.voiceNote.durationJitterMs,
209
+ waveformSeed: fingerprint.voiceNote.waveformSeed,
210
+ };
211
+ }
212
+ /**
213
+ * Get battery state (helper for presence/connection state signals)
214
+ */
215
+ export function getBatteryState(fingerprint) {
216
+ return {
217
+ level: fingerprint.connectionState.batteryLevel,
218
+ charging: fingerprint.connectionState.batteryCharging,
219
+ };
220
+ }
221
+ /**
222
+ * Create a session fingerprint preset (Obscura-inspired feature flag pattern)
223
+ */
224
+ export function createStealthFingerprint(sessionId) {
225
+ return generateSessionFingerprint({
226
+ enabled: true,
227
+ deviceProfile: {
228
+ randomizeAppVersion: true,
229
+ randomizeOsVersion: true,
230
+ randomizeDeviceModel: true,
231
+ },
232
+ networkTiming: {
233
+ sendJitterMs: [100, 500],
234
+ typingJitterMs: [50, 200],
235
+ retryJitterMs: [200, 800],
236
+ },
237
+ voiceNote: {
238
+ randomizeWaveform: true,
239
+ durationJitterMs: 300,
240
+ },
241
+ connectionState: {
242
+ randomizeBattery: true,
243
+ },
244
+ protocolVersion: {
245
+ randomizeSubVersion: true,
246
+ },
247
+ }, sessionId);
248
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Stealth Connect — Reduce ban signal on socket connect + presence ramp
3
+ *
4
+ * Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
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.
15
+ *
16
+ * Usage:
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.
40
+ */
41
+ export declare const STEALTH_BROWSER_POOL: readonly BrowserTuple[];
42
+ /**
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()`.
55
+ *
56
+ * Precedence: `browser` > `os` > random pick from `STEALTH_BROWSER_POOL`.
57
+ */
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
+ */
64
+ os?: string;
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
+ }
75
+ /**
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`.
82
+ *
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`).
91
+ */
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. */
100
+ minDelayMs?: number;
101
+ /** Maximum delay before issuing the presence update, ms. Default: 90000. */
102
+ maxDelayMs?: number;
103
+ /** Presence state to set after the delay. Default: `'available'`. */
104
+ targetState?: 'available' | 'unavailable' | 'composing' | 'recording' | 'paused';
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>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Stealth Connect — Reduce ban signal on socket connect + presence ramp
3
+ *
4
+ * Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
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.
15
+ *
16
+ * Usage:
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.
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
+ ]);
47
+ /**
48
+ * Returns a partial Baileys socket config tuned for stealth connect.
49
+ *
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 });
58
+ */
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
+ }
76
+ return {
77
+ markOnlineOnConnect: false,
78
+ browser,
79
+ };
80
+ }
81
+ /**
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.
101
+ */
102
+ export async function rampPresenceAfterConnect(sock, opts) {
103
+ const minDelayMs = opts?.minDelayMs ?? 30000;
104
+ const maxDelayMs = opts?.maxDelayMs ?? 90000;
105
+ const targetState = opts?.targetState ?? 'available';
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
129
+ await sock.sendPresenceUpdate(targetState, undefined);
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baileys-antiban",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
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",