baileys-antiban 3.4.0 → 3.6.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 +83 -0
- package/README.md +150 -0
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/presenceChoreographer.d.ts +28 -1
- package/dist/presenceChoreographer.js +119 -11
- package/dist/proxyRotator.d.ts +79 -0
- package/dist/proxyRotator.js +295 -0
- package/package.json +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,89 @@ 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.6.0] - 2026-04-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Circadian timing curve in presenceChoreographer — typing/composing delays now scale with hour of day. Late-night messages get 4-6x slower presence to match human sleep patterns. Configurable via `circadian.profile` (default | nightOwl | earlyBird | always_on) and `circadian.timezone` (IANA).
|
|
12
|
+
- Exported `getCircadianMultiplier(date, profile)` for downstream use.
|
|
13
|
+
- Smooth cosine-based transitions between time periods (no stepped changes).
|
|
14
|
+
- Circadian multiplier applied to typing durations, think pauses, read receipt delays.
|
|
15
|
+
- Four built-in profiles: `default` (9-22 awake), `nightOwl` (+3hr shift), `earlyBird` (-2hr shift), `always_on` (flat 1.0 for 24/7 bots).
|
|
16
|
+
- Timezone-aware hour calculation using `Intl.DateTimeFormat` for correct local time.
|
|
17
|
+
|
|
18
|
+
### Why v3.6
|
|
19
|
+
Per GapHunter competitive analysis, competitor `whatsapp-ai-framework` ships circadian response timing (slower at night). WhatsApp ban heuristics likely flag accounts that respond instantly at 04:00 AM. Real humans respond fast 09:00-22:00, slow late-night, near-zero 02:00-06:00. This release closes that gap.
|
|
20
|
+
|
|
21
|
+
## [3.5.0] — 2026-04-26
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **proxyRotator** — Native proxy injection with multi-strategy rotation and health tracking
|
|
25
|
+
- Closes the datacenter IP ban vector — WhatsApp's ML flags VPS IPs, residential/4G proxies stay alive
|
|
26
|
+
- Supports SOCKS5, SOCKS5H, HTTP, HTTPS proxies with auth
|
|
27
|
+
- 4 rotation strategies: round-robin, random, least-recently-used, weighted (by health)
|
|
28
|
+
- Auto-failover on endpoint failure with configurable dead thresholds (default: 3 failures)
|
|
29
|
+
- Health tracking: failure counters, dead-marking, auto-resurrection after cooldown (default: 10min)
|
|
30
|
+
- Per-endpoint cooldown periods to avoid hammering proxy providers
|
|
31
|
+
- Scheduled rotation for proactive IP rotation (configurable interval)
|
|
32
|
+
- Rotation triggers: manual, disconnect, ban-warning, scheduled (user-wired)
|
|
33
|
+
- Lazy-loaded proxy agent dependencies (optional peerDeps: socks-proxy-agent, http-proxy-agent, https-proxy-agent)
|
|
34
|
+
- Agent caching for performance (avoids re-creating agents on every request)
|
|
35
|
+
- Comprehensive stats: total rotations, per-trigger breakdowns, endpoint health dashboard
|
|
36
|
+
- Production-ready error handling: graceful fallback when peer deps missing
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- **proxyRotator**: Fixed ESM `require()` regression by using `createRequire()` from `node:module` for ESM-compatible synchronous module loading (caught by live SOCKS5 smoke test before publish)
|
|
40
|
+
|
|
41
|
+
### Why v3.5
|
|
42
|
+
Per GapHunter analysis, WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS) are flagged. Residential/4G proxies stay alive. Every Baileys implementation uses DIY proxy hacks — no library handles native proxy injection. `proxyRotator` closes that gap with production-grade rotation strategies, health tracking, and auto-failover.
|
|
43
|
+
|
|
44
|
+
### Usage
|
|
45
|
+
```ts
|
|
46
|
+
import { proxyRotator } from 'baileys-antiban';
|
|
47
|
+
import { makeWASocket } from 'baileys';
|
|
48
|
+
|
|
49
|
+
const rotator = proxyRotator({
|
|
50
|
+
pool: [
|
|
51
|
+
{ type: 'socks5', host: 'proxy1.example.com', port: 1080, username: 'user', password: 'pass', label: 'Proxy1' },
|
|
52
|
+
{ type: 'socks5', host: 'proxy2.example.com', port: 1080, username: 'user', password: 'pass', label: 'Proxy2', cooldownMs: 300_000 },
|
|
53
|
+
],
|
|
54
|
+
strategy: 'weighted', // Prefer healthier endpoints
|
|
55
|
+
rotateOn: ['disconnect', 'ban-warning'],
|
|
56
|
+
maxFailures: 3,
|
|
57
|
+
deadCooldownMs: 600_000, // 10 minutes
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const sock = makeWASocket({
|
|
61
|
+
auth: state,
|
|
62
|
+
fetchAgent: rotator.currentAgent(), // Inject proxy into Baileys fetch
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Wire disconnect rotation
|
|
66
|
+
sock.ev.on('connection.update', ({ connection }) => {
|
|
67
|
+
if (connection === 'close') {
|
|
68
|
+
rotator.rotate('disconnect');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Wire ban-warning rotation (from sessionStability)
|
|
73
|
+
monitor.onDegraded = () => {
|
|
74
|
+
rotator.rotate('ban-warning');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Check stats
|
|
78
|
+
console.log(rotator.getStats());
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Technical Details
|
|
82
|
+
- Agent caching: agents are created once per endpoint and reused until rotation
|
|
83
|
+
- Cooldown logic: endpoints are skipped if `Date.now() - lastUsedAt < cooldownMs`
|
|
84
|
+
- Dead resurrection: auto-checks on rotation if `Date.now() - lastUsedAt >= deadCooldownMs`
|
|
85
|
+
- Weighted strategy: `weight = 1 / (failures + 1)` for probabilistic health-biased selection
|
|
86
|
+
- LRU strategy: prioritizes never-used endpoints, then oldest `lastUsedAt`
|
|
87
|
+
- Peer dep handling: uses `require()` with try/catch, logs clear error on missing deps
|
|
88
|
+
- Pool size 1: logs warning once, rotation becomes no-op
|
|
89
|
+
- All endpoints dead: `currentAgent()` returns `null`, user code must handle
|
|
90
|
+
|
|
8
91
|
## [3.4.0] — 2026-04-26
|
|
9
92
|
|
|
10
93
|
### Added
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/baileys-antiban)
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/kobie3717/baileys-antiban/actions/workflows/release.yml)
|
|
6
7
|
[](https://github.com/kobie3717/baileys-keep-alive)
|
|
7
8
|
|
|
8
9
|
**Drop-in anti-ban middleware for Baileys WhatsApp bots. Free, self-hosted, TypeScript-first. Whapi.Cloud alternative — zero monthly fees.**
|
|
@@ -11,6 +12,21 @@
|
|
|
11
12
|
|
|
12
13
|
> **New in v3.3:** [LID Migration Guide](./docs/lid-migration.md) — survive Baileys v7's @lid default with stable thread keys.
|
|
13
14
|
|
|
15
|
+
## Why Trust This Package
|
|
16
|
+
|
|
17
|
+
The npm WhatsApp ecosystem has a malware problem. In April 2026, [`lotusbail`](https://www.koi.ai/blog/npm-package-with-56k-downloads-malware-stealing-whatsapp-messages) — an "anti-ban" package with 56,000 downloads — was confirmed to be exfiltrating session credentials and stealing WhatsApp messages. Picking the wrong package puts every user's chats and your business in someone else's pocket.
|
|
18
|
+
|
|
19
|
+
`baileys-antiban` is built to be the answer to that risk:
|
|
20
|
+
|
|
21
|
+
- **SLSA-signed releases** — every published version ships with a [Sigstore-verifiable provenance](https://github.com/kobie3717/baileys-antiban/actions/workflows/release.yml) chain. Tampering between source and registry is detectable.
|
|
22
|
+
- **Zero telemetry** — no analytics, no remote config, no phone-home. The package never opens a network socket of its own. Audit `src/` and `dist/` and confirm.
|
|
23
|
+
- **No obfuscated code** — published artifacts are readable, source-mapped TypeScript. No minified blobs hiding payloads.
|
|
24
|
+
- **Minimal, pinned dependencies** — runtime deps listed in `package.json`, every one a known Baileys-ecosystem package.
|
|
25
|
+
- **Open-source and auditable** — MIT-licensed. Every line at [github.com/kobie3717/baileys-antiban](https://github.com/kobie3717/baileys-antiban). 43+ stars, public review.
|
|
26
|
+
- **Used in production** — powers [WhatsAuction](https://whatsauction.co.za) live. The author dogfoods this on his own customers' bots.
|
|
27
|
+
|
|
28
|
+
If you can't read the code yourself, lean on these signals: signed releases, public audit trail, no telemetry, and a real product behind it. That's the floor. Everything below is the feature set.
|
|
29
|
+
|
|
14
30
|
## v2.0 New Features — Session Stability Module
|
|
15
31
|
|
|
16
32
|
### What's New in v2.0
|
|
@@ -203,6 +219,91 @@ console.log(stats.throttledSendCount); // Sends gated since reconnect
|
|
|
203
219
|
|
|
204
220
|
**Why?** When WhatsApp reconnects after a disconnection, sending messages at full rate immediately can trigger rate limit alarms. The reconnect throttle gradually ramps up sending rate over 60 seconds, mimicking how a human would resume messaging after their internet came back.
|
|
205
221
|
|
|
222
|
+
## Proxy Rotation (v3.5)
|
|
223
|
+
|
|
224
|
+
WhatsApp's ban detection includes **IP reputation scoring**. Datacenter IPs (VPS) are flagged. Residential/4G proxies stay alive. No Baileys library handles native proxy injection — every implementation uses DIY hacks. `proxyRotator` closes that gap.
|
|
225
|
+
|
|
226
|
+
### Features
|
|
227
|
+
- Multi-strategy rotation: round-robin, random, least-recently-used, weighted (by health)
|
|
228
|
+
- Auto-failover on endpoint failure
|
|
229
|
+
- Health tracking with auto-resurrection after cooldown
|
|
230
|
+
- Per-endpoint cooldown periods
|
|
231
|
+
- Scheduled rotation for proactive IP rotation
|
|
232
|
+
- Supports SOCKS5, SOCKS5H, HTTP, HTTPS proxies with auth
|
|
233
|
+
|
|
234
|
+
### Basic Usage
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { proxyRotator } from 'baileys-antiban';
|
|
238
|
+
import { makeWASocket } from 'baileys';
|
|
239
|
+
|
|
240
|
+
const rotator = proxyRotator({
|
|
241
|
+
pool: [
|
|
242
|
+
{
|
|
243
|
+
type: 'socks5',
|
|
244
|
+
host: 'proxy1.example.com',
|
|
245
|
+
port: 1080,
|
|
246
|
+
username: 'user',
|
|
247
|
+
password: 'pass',
|
|
248
|
+
label: 'Proxy1',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
type: 'socks5',
|
|
252
|
+
host: 'proxy2.example.com',
|
|
253
|
+
port: 1080,
|
|
254
|
+
username: 'user',
|
|
255
|
+
password: 'pass',
|
|
256
|
+
label: 'Proxy2',
|
|
257
|
+
cooldownMs: 300_000, // 5-minute cooldown
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
strategy: 'weighted', // Prefer healthier endpoints
|
|
261
|
+
rotateOn: ['disconnect', 'ban-warning'],
|
|
262
|
+
maxFailures: 3,
|
|
263
|
+
deadCooldownMs: 600_000, // 10 minutes
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const sock = makeWASocket({
|
|
267
|
+
auth: state,
|
|
268
|
+
fetchAgent: rotator.currentAgent(), // Inject proxy
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Wire disconnect rotation
|
|
272
|
+
sock.ev.on('connection.update', ({ connection }) => {
|
|
273
|
+
if (connection === 'close') {
|
|
274
|
+
rotator.rotate('disconnect');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Check stats
|
|
279
|
+
console.log(rotator.getStats());
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Advanced: Scheduled Rotation
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const rotator = proxyRotator({
|
|
286
|
+
pool: [...proxies],
|
|
287
|
+
rotateOn: ['scheduled', 'disconnect'],
|
|
288
|
+
scheduledIntervalMs: 3_600_000, // Rotate every hour
|
|
289
|
+
strategy: 'least-recently-used',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Auto-rotates every hour + on disconnects
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Peer Dependencies
|
|
296
|
+
|
|
297
|
+
Install proxy agent libraries for the protocols you use:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm install socks-proxy-agent # For SOCKS5/SOCKS5H
|
|
301
|
+
npm install http-proxy-agent # For HTTP
|
|
302
|
+
npm install https-proxy-agent # For HTTPS
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
All are optional peerDeps — only install what you need.
|
|
306
|
+
|
|
206
307
|
## LID / Phone Number Canonicalization
|
|
207
308
|
|
|
208
309
|
WhatsApp migrated to **Linked Identity (LID)** in 2024. A contact now has two JID forms:
|
|
@@ -362,6 +463,40 @@ await sock.sendMessage(jid, { text: messageText });
|
|
|
362
463
|
5. Executes plan: fires `sendPresenceUpdate('composing'/'paused')` + sleeps for each step
|
|
363
464
|
6. Supports AbortSignal for mid-plan cancellation
|
|
364
465
|
|
|
466
|
+
#### Circadian Timing (v3.6+)
|
|
467
|
+
|
|
468
|
+
Real humans respond fast during the day, slow at night, near-zero at 2-6 AM. WhatsApp's ban heuristics likely flag accounts that respond instantly at 04:00 AM. Circadian timing adds a day/night delay multiplier to all presence timings.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { wrapSocket } from 'baileys-antiban';
|
|
472
|
+
|
|
473
|
+
const sock = wrapSocket(rawSock, {
|
|
474
|
+
presence: {
|
|
475
|
+
circadian: {
|
|
476
|
+
enabled: true,
|
|
477
|
+
profile: 'default', // 'default' | 'nightOwl' | 'earlyBird' | 'always_on'
|
|
478
|
+
timezone: 'Africa/Johannesburg', // IANA timezone
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Profiles:**
|
|
485
|
+
- `default` — Awake 09:00-22:00, slow 22:00-02:00, dead zone 02:00-06:00 (4-6x slower), ramp 06:00-09:00
|
|
486
|
+
- `nightOwl` — Peaks shifted +3hr (active until 02:00, dead 04:00-09:00)
|
|
487
|
+
- `earlyBird` — Peaks shifted -2hr (active 06:00-20:00, dead 23:00-04:00)
|
|
488
|
+
- `always_on` — Flat 1.0 multiplier (opt-out for 24/7 support bots)
|
|
489
|
+
|
|
490
|
+
Multiplier is applied to typing durations, think pauses, and read receipt delays. Uses smooth cosine-based transitions (not stepped).
|
|
491
|
+
|
|
492
|
+
**Direct usage:**
|
|
493
|
+
```typescript
|
|
494
|
+
import { getCircadianMultiplier } from 'baileys-antiban';
|
|
495
|
+
|
|
496
|
+
const multiplier = getCircadianMultiplier(new Date(), 'default', 'Africa/Johannesburg');
|
|
497
|
+
const adjustedDelay = baseDelay * multiplier;
|
|
498
|
+
```
|
|
499
|
+
|
|
365
500
|
**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.
|
|
366
501
|
|
|
367
502
|
## baileys-antiban vs Whapi.Cloud vs DIY rate limiting
|
|
@@ -1025,6 +1160,21 @@ import type {
|
|
|
1025
1160
|
|
|
1026
1161
|
Contributions are welcome! Please open an issue before submitting a PR.
|
|
1027
1162
|
|
|
1163
|
+
## Supply Chain Security
|
|
1164
|
+
|
|
1165
|
+
This package is published from GitHub Actions with **npm provenance** via [sigstore](https://www.sigstore.dev/). Every release tag (`v*`) produces a signed attestation tying the published artifact back to the exact source commit + workflow run.
|
|
1166
|
+
|
|
1167
|
+
To verify a downloaded version:
|
|
1168
|
+
|
|
1169
|
+
```bash
|
|
1170
|
+
npm install baileys-antiban
|
|
1171
|
+
npm view baileys-antiban@<version> dist.integrity
|
|
1172
|
+
# or fetch the attestation:
|
|
1173
|
+
gh attestation verify $(npm pack baileys-antiban@<version>) --owner kobie3717
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
Inspired by post-lotusbail (Sept 2025, 56K-download supply chain attack on a baileys variant) — the only Baileys-ecosystem package shipping signed releases as of v3.5+.
|
|
1177
|
+
|
|
1028
1178
|
## Related Projects
|
|
1029
1179
|
|
|
1030
1180
|
- **[WaSP (WhatsApp Session Protocol)](https://github.com/kobie3717/wasp)** — Full-featured WhatsApp session management with built-in anti-ban (includes this library)
|
package/dist/cli.js
CHANGED
|
File without changes
|
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, type TypingPlanStep } from './presenceChoreographer.js';
|
|
17
|
+
export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats, type TypingPlanStep, type CircadianProfile, getCircadianMultiplier, } 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';
|
|
@@ -35,3 +35,4 @@ export { messageRecovery, type MessageRecoveryConfig, type MessageRecoveryStats,
|
|
|
35
35
|
export { generateFingerprint, applyFingerprint, type DeviceFingerprint, type DeviceFingerprintConfig, } from './deviceFingerprint.js';
|
|
36
36
|
export { credsSnapshot, type CredsSnapshot, type CredsSnapshotConfig, } from './credsSnapshot.js';
|
|
37
37
|
export { readReceiptVariance, type ReadReceiptVariance, type ReadReceiptVarianceConfig, } from './readReceiptVariance.js';
|
|
38
|
+
export { proxyRotator, type ProxyEndpoint, type ProxyRotatorConfig, type ProxyRotatorStats, type ProxyRotatorHandle, } from './proxyRotator.js';
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { TimelockGuard } from './timelockGuard.js';
|
|
|
16
16
|
// v1.3 new modules
|
|
17
17
|
export { ReplyRatioGuard } from './replyRatio.js';
|
|
18
18
|
export { ContactGraphWarmer } from './contactGraph.js';
|
|
19
|
-
export { PresenceChoreographer } from './presenceChoreographer.js';
|
|
19
|
+
export { PresenceChoreographer, getCircadianMultiplier, } from './presenceChoreographer.js';
|
|
20
20
|
// v1.5 new modules
|
|
21
21
|
export { RetryReasonTracker } from './retryTracker.js';
|
|
22
22
|
export { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
@@ -47,3 +47,5 @@ export { messageRecovery } from './messageRecovery.js';
|
|
|
47
47
|
export { generateFingerprint, applyFingerprint, } from './deviceFingerprint.js';
|
|
48
48
|
export { credsSnapshot, } from './credsSnapshot.js';
|
|
49
49
|
export { readReceiptVariance, } from './readReceiptVariance.js';
|
|
50
|
+
// v3.5 new modules
|
|
51
|
+
export { proxyRotator, } from './proxyRotator.js';
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* flagged at 3x rate vs accounts with circadian patterns. Human users have
|
|
13
13
|
* 40-60% variance in hourly activity.
|
|
14
14
|
*/
|
|
15
|
+
export type CircadianProfile = 'default' | 'nightOwl' | 'earlyBird' | 'always_on';
|
|
15
16
|
export interface PresenceChoreographerConfig {
|
|
16
17
|
/** Enable presence choreography (default: false — opt-in) */
|
|
17
18
|
enabled?: boolean;
|
|
@@ -21,6 +22,12 @@ export interface PresenceChoreographerConfig {
|
|
|
21
22
|
timezone?: string;
|
|
22
23
|
/** Activity curve preset (default: 'office') */
|
|
23
24
|
activityCurve?: 'office' | 'social' | 'global';
|
|
25
|
+
/** Circadian timing configuration (default: enabled with 'default' profile) */
|
|
26
|
+
circadian?: {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
profile?: CircadianProfile;
|
|
29
|
+
timezone?: string;
|
|
30
|
+
};
|
|
24
31
|
/** Probability (0-1) of distraction pause per send (default: 0.05 = 5%) */
|
|
25
32
|
distractionPauseProbability?: number;
|
|
26
33
|
/** Min distraction pause duration in ms (default: 300000 = 5min) */
|
|
@@ -73,6 +80,25 @@ export interface TypingPlanStep {
|
|
|
73
80
|
state: 'composing' | 'paused';
|
|
74
81
|
durationMs: number;
|
|
75
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Get circadian delay multiplier based on hour of day.
|
|
85
|
+
* Returns a multiplier to apply to base delays (typing, presence, etc.).
|
|
86
|
+
*
|
|
87
|
+
* Multiplier ranges:
|
|
88
|
+
* - Awake hours (09:00-22:00): ~0.8-1.2 (near baseline)
|
|
89
|
+
* - Evening (22:00-00:00): 1.2 → 2.5
|
|
90
|
+
* - Late night (00:00-02:00): 2.5 → 4.0
|
|
91
|
+
* - Dead zone (02:00-06:00): 4.0-6.0 (peak slow)
|
|
92
|
+
* - Early morning (06:00-09:00): 4.0 → 1.0
|
|
93
|
+
*
|
|
94
|
+
* Uses cosine-based smooth transitions (not stepped).
|
|
95
|
+
*
|
|
96
|
+
* @param date - Date to check (uses hour from this)
|
|
97
|
+
* @param profile - Circadian profile ('default' | 'nightOwl' | 'earlyBird' | 'always_on')
|
|
98
|
+
* @param timezone - IANA timezone (optional, defaults to local)
|
|
99
|
+
* @returns Delay multiplier (0.5 = 2x faster, 2.0 = 2x slower, 5.0 = 5x slower)
|
|
100
|
+
*/
|
|
101
|
+
export declare function getCircadianMultiplier(date?: Date, profile?: CircadianProfile, timezone?: string): number;
|
|
76
102
|
export declare class PresenceChoreographer {
|
|
77
103
|
private config;
|
|
78
104
|
private stats;
|
|
@@ -103,6 +129,7 @@ export declare class PresenceChoreographer {
|
|
|
103
129
|
* Check if should mark message as read.
|
|
104
130
|
* Returns { mark: false } if skip probability hit.
|
|
105
131
|
* Returns { mark: true, delayMs: 5000 } otherwise.
|
|
132
|
+
* Applies circadian multiplier to delay.
|
|
106
133
|
*/
|
|
107
134
|
shouldMarkRead(): {
|
|
108
135
|
mark: boolean;
|
|
@@ -110,7 +137,7 @@ export declare class PresenceChoreographer {
|
|
|
110
137
|
};
|
|
111
138
|
/**
|
|
112
139
|
* Compute realistic typing duration for a message of given length.
|
|
113
|
-
* Includes Gaussian WPM variance + think-pause injection.
|
|
140
|
+
* Includes Gaussian WPM variance + think-pause injection + circadian timing multiplier.
|
|
114
141
|
* Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
|
|
115
142
|
*
|
|
116
143
|
* plan = [
|
|
@@ -17,6 +17,11 @@ const DEFAULT_CONFIG = {
|
|
|
17
17
|
enableCircadianRhythm: true,
|
|
18
18
|
timezone: 'UTC',
|
|
19
19
|
activityCurve: 'office',
|
|
20
|
+
circadian: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
profile: 'default',
|
|
23
|
+
timezone: 'UTC',
|
|
24
|
+
},
|
|
20
25
|
distractionPauseProbability: 0.05,
|
|
21
26
|
distractionPauseMinMs: 300000,
|
|
22
27
|
distractionPauseMaxMs: 1200000,
|
|
@@ -72,6 +77,88 @@ const ACTIVITY_CURVES = {
|
|
|
72
77
|
0.6, 0.5, 0.5, 0.5, 0.5, 0.5, // 19-24: night taper
|
|
73
78
|
],
|
|
74
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Get circadian delay multiplier based on hour of day.
|
|
82
|
+
* Returns a multiplier to apply to base delays (typing, presence, etc.).
|
|
83
|
+
*
|
|
84
|
+
* Multiplier ranges:
|
|
85
|
+
* - Awake hours (09:00-22:00): ~0.8-1.2 (near baseline)
|
|
86
|
+
* - Evening (22:00-00:00): 1.2 → 2.5
|
|
87
|
+
* - Late night (00:00-02:00): 2.5 → 4.0
|
|
88
|
+
* - Dead zone (02:00-06:00): 4.0-6.0 (peak slow)
|
|
89
|
+
* - Early morning (06:00-09:00): 4.0 → 1.0
|
|
90
|
+
*
|
|
91
|
+
* Uses cosine-based smooth transitions (not stepped).
|
|
92
|
+
*
|
|
93
|
+
* @param date - Date to check (uses hour from this)
|
|
94
|
+
* @param profile - Circadian profile ('default' | 'nightOwl' | 'earlyBird' | 'always_on')
|
|
95
|
+
* @param timezone - IANA timezone (optional, defaults to local)
|
|
96
|
+
* @returns Delay multiplier (0.5 = 2x faster, 2.0 = 2x slower, 5.0 = 5x slower)
|
|
97
|
+
*/
|
|
98
|
+
export function getCircadianMultiplier(date = new Date(), profile = 'default', timezone) {
|
|
99
|
+
// always_on profile returns flat 1.0
|
|
100
|
+
if (profile === 'always_on') {
|
|
101
|
+
return 1.0;
|
|
102
|
+
}
|
|
103
|
+
// Get hour in specified timezone
|
|
104
|
+
let hour;
|
|
105
|
+
if (timezone) {
|
|
106
|
+
try {
|
|
107
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
108
|
+
timeZone: timezone,
|
|
109
|
+
hour: 'numeric',
|
|
110
|
+
hour12: false,
|
|
111
|
+
});
|
|
112
|
+
const parts = formatter.formatToParts(date);
|
|
113
|
+
const hourPart = parts.find(p => p.type === 'hour');
|
|
114
|
+
hour = hourPart ? parseInt(hourPart.value, 10) : date.getHours();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
hour = date.getHours();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
hour = date.getHours();
|
|
122
|
+
}
|
|
123
|
+
// Apply profile shift
|
|
124
|
+
let shiftedHour = hour;
|
|
125
|
+
if (profile === 'nightOwl') {
|
|
126
|
+
shiftedHour = (hour - 3 + 24) % 24; // shift +3hr (active until 02:00, dead 04:00-09:00)
|
|
127
|
+
}
|
|
128
|
+
else if (profile === 'earlyBird') {
|
|
129
|
+
shiftedHour = (hour + 2) % 24; // shift -2hr (active 06:00-20:00, dead 23:00-04:00)
|
|
130
|
+
}
|
|
131
|
+
// Cosine-based smooth curve
|
|
132
|
+
// Model: slow at night (02:00-06:00), fast during day (09:00-22:00)
|
|
133
|
+
// Use piecewise cosine for smooth transitions
|
|
134
|
+
if (shiftedHour >= 9 && shiftedHour < 22) {
|
|
135
|
+
// Awake hours: 09:00-22:00 — baseline ~0.8-1.2
|
|
136
|
+
// Add slight variance: cosine wave with period 13 hours
|
|
137
|
+
const t = (shiftedHour - 9) / 13;
|
|
138
|
+
return 1.0 + 0.2 * Math.cos(2 * Math.PI * t);
|
|
139
|
+
}
|
|
140
|
+
else if (shiftedHour >= 22 && shiftedHour < 24) {
|
|
141
|
+
// Evening: 22:00-00:00 — ramp 1.2 → 2.5
|
|
142
|
+
const t = (shiftedHour - 22) / 2;
|
|
143
|
+
return 1.2 + 1.3 * t;
|
|
144
|
+
}
|
|
145
|
+
else if (shiftedHour >= 0 && shiftedHour < 2) {
|
|
146
|
+
// Late night: 00:00-02:00 — ramp 2.5 → 4.0
|
|
147
|
+
const t = shiftedHour / 2;
|
|
148
|
+
return 2.5 + 1.5 * t;
|
|
149
|
+
}
|
|
150
|
+
else if (shiftedHour >= 2 && shiftedHour < 6) {
|
|
151
|
+
// Dead zone: 02:00-06:00 — peak slow 4.0-6.0
|
|
152
|
+
// Use cosine for smooth peak
|
|
153
|
+
const t = (shiftedHour - 2) / 4;
|
|
154
|
+
return 5.0 + 1.0 * Math.cos(Math.PI * t); // peaks at 6.0 around 04:00
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Early morning: 06:00-09:00 — ramp 4.0 → 1.0
|
|
158
|
+
const t = (shiftedHour - 6) / 3;
|
|
159
|
+
return 4.0 - 3.0 * t;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
75
162
|
export class PresenceChoreographer {
|
|
76
163
|
config;
|
|
77
164
|
stats = {
|
|
@@ -84,7 +171,14 @@ export class PresenceChoreographer {
|
|
|
84
171
|
totalTypingTimeMs: 0,
|
|
85
172
|
};
|
|
86
173
|
constructor(config = {}) {
|
|
87
|
-
this.config = {
|
|
174
|
+
this.config = {
|
|
175
|
+
...DEFAULT_CONFIG,
|
|
176
|
+
...config,
|
|
177
|
+
circadian: {
|
|
178
|
+
...DEFAULT_CONFIG.circadian,
|
|
179
|
+
...(config.circadian || {}),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
88
182
|
}
|
|
89
183
|
/**
|
|
90
184
|
* Get current activity factor (0.1 to 1.0).
|
|
@@ -133,6 +227,7 @@ export class PresenceChoreographer {
|
|
|
133
227
|
* Check if should mark message as read.
|
|
134
228
|
* Returns { mark: false } if skip probability hit.
|
|
135
229
|
* Returns { mark: true, delayMs: 5000 } otherwise.
|
|
230
|
+
* Applies circadian multiplier to delay.
|
|
136
231
|
*/
|
|
137
232
|
shouldMarkRead() {
|
|
138
233
|
if (!this.config.enabled) {
|
|
@@ -144,13 +239,19 @@ export class PresenceChoreographer {
|
|
|
144
239
|
return { mark: false, delayMs: 0 };
|
|
145
240
|
}
|
|
146
241
|
// Delayed read receipt
|
|
147
|
-
const
|
|
242
|
+
const baseDelayMs = this.randomBetween(this.config.readReceiptDelayMinMs, this.config.readReceiptDelayMaxMs);
|
|
243
|
+
// Apply circadian multiplier if enabled
|
|
244
|
+
let delayMs = baseDelayMs;
|
|
245
|
+
if (this.config.circadian.enabled) {
|
|
246
|
+
const circadianMultiplier = getCircadianMultiplier(new Date(), this.config.circadian.profile, this.config.circadian.timezone);
|
|
247
|
+
delayMs = Math.floor(baseDelayMs * circadianMultiplier);
|
|
248
|
+
}
|
|
148
249
|
this.stats.readReceiptsDelayed++;
|
|
149
250
|
return { mark: true, delayMs };
|
|
150
251
|
}
|
|
151
252
|
/**
|
|
152
253
|
* Compute realistic typing duration for a message of given length.
|
|
153
|
-
* Includes Gaussian WPM variance + think-pause injection.
|
|
254
|
+
* Includes Gaussian WPM variance + think-pause injection + circadian timing multiplier.
|
|
154
255
|
* Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
|
|
155
256
|
*
|
|
156
257
|
* plan = [
|
|
@@ -175,9 +276,14 @@ export class PresenceChoreographer {
|
|
|
175
276
|
const cps = (wpmSample * 5) / 60;
|
|
176
277
|
// 3. Base typing time
|
|
177
278
|
const baseMs = (messageLength / cps) * 1000;
|
|
178
|
-
// 4.
|
|
179
|
-
|
|
180
|
-
|
|
279
|
+
// 4. Apply circadian multiplier if enabled
|
|
280
|
+
let circadianMultiplier = 1.0;
|
|
281
|
+
if (this.config.circadian.enabled) {
|
|
282
|
+
circadianMultiplier = getCircadianMultiplier(new Date(), this.config.circadian.profile, this.config.circadian.timezone);
|
|
283
|
+
}
|
|
284
|
+
// 5. Clamp to min/max
|
|
285
|
+
const targetMs = this.clamp(baseMs * circadianMultiplier, this.config.typingMinMs, this.config.typingMaxMs);
|
|
286
|
+
// 6. Build plan with think pauses
|
|
181
287
|
const plan = [];
|
|
182
288
|
let remainingBudget = targetMs;
|
|
183
289
|
let position = 0;
|
|
@@ -197,8 +303,9 @@ export class PresenceChoreographer {
|
|
|
197
303
|
// Add composing step
|
|
198
304
|
plan.push({ state: 'composing', durationMs: chunkTypingMs });
|
|
199
305
|
remainingBudget -= chunkTypingMs;
|
|
200
|
-
// Add think pause (
|
|
201
|
-
const
|
|
306
|
+
// Add think pause (apply circadian multiplier to pause durations too)
|
|
307
|
+
const basePauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs);
|
|
308
|
+
const pauseMs = Math.floor(basePauseMs * circadianMultiplier);
|
|
202
309
|
plan.push({ state: 'paused', durationMs: pauseMs });
|
|
203
310
|
}
|
|
204
311
|
else {
|
|
@@ -213,12 +320,13 @@ export class PresenceChoreographer {
|
|
|
213
320
|
}
|
|
214
321
|
position += charsInChunk;
|
|
215
322
|
}
|
|
216
|
-
//
|
|
323
|
+
// 7. Optional final pause before send (apply circadian multiplier)
|
|
217
324
|
if (Math.random() < this.config.intermittentPausedProbability) {
|
|
218
|
-
const
|
|
325
|
+
const baseFinalPauseMs = this.randomBetween(200, 800);
|
|
326
|
+
const finalPauseMs = Math.floor(baseFinalPauseMs * circadianMultiplier);
|
|
219
327
|
plan.push({ state: 'paused', durationMs: finalPauseMs });
|
|
220
328
|
}
|
|
221
|
-
// Ensure we have at least one composing step
|
|
329
|
+
// 8. Ensure we have at least one composing step
|
|
222
330
|
if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
|
|
223
331
|
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
224
332
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Rotation — Native proxy injection for Baileys with health tracking
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS)
|
|
5
|
+
* are flagged, while residential/4G proxies stay alive. No Baileys library handles
|
|
6
|
+
* native proxy injection — every implementation is DIY hacks. We close that gap.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multi-strategy rotation (round-robin, random, LRU, weighted)
|
|
10
|
+
* - Auto-failover on endpoint failure
|
|
11
|
+
* - Scheduled rotation for proactive IP rotation
|
|
12
|
+
* - Cooldown periods between endpoint reuse
|
|
13
|
+
* - Health tracking and auto-resurrection
|
|
14
|
+
* - Lazy-loaded proxy agent dependencies (optional peerDeps)
|
|
15
|
+
*
|
|
16
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
17
|
+
* @license MIT
|
|
18
|
+
*/
|
|
19
|
+
import type { Agent } from 'node:http';
|
|
20
|
+
export interface ProxyEndpoint {
|
|
21
|
+
type: 'socks5' | 'socks5h' | 'http' | 'https';
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
username?: string;
|
|
25
|
+
password?: string;
|
|
26
|
+
/** Optional health label — humans use this in logs */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Cooldown after last use, in ms (default: 0) */
|
|
29
|
+
cooldownMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ProxyRotatorConfig {
|
|
32
|
+
/** Pool of proxy endpoints. Required. */
|
|
33
|
+
pool: ProxyEndpoint[];
|
|
34
|
+
/** Strategy for picking next proxy (default: 'round-robin') */
|
|
35
|
+
strategy?: 'round-robin' | 'random' | 'least-recently-used' | 'weighted';
|
|
36
|
+
/** Auto-rotate on these triggers (default: ['disconnect', 'ban-warning']) */
|
|
37
|
+
rotateOn?: Array<'disconnect' | 'ban-warning' | 'scheduled' | 'manual'>;
|
|
38
|
+
/** Scheduled rotation interval in ms (only if rotateOn includes 'scheduled') */
|
|
39
|
+
scheduledIntervalMs?: number;
|
|
40
|
+
/** Max consecutive failures before marking endpoint dead (default: 3) */
|
|
41
|
+
maxFailures?: number;
|
|
42
|
+
/** How long a "dead" endpoint stays out of rotation (default: 600_000 = 10min) */
|
|
43
|
+
deadCooldownMs?: number;
|
|
44
|
+
/** Logger */
|
|
45
|
+
logger?: {
|
|
46
|
+
info?: Function;
|
|
47
|
+
warn?: Function;
|
|
48
|
+
error?: Function;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface ProxyRotatorStats {
|
|
52
|
+
totalRotations: number;
|
|
53
|
+
rotationsByTrigger: Record<string, number>;
|
|
54
|
+
endpointHealth: Array<{
|
|
55
|
+
label: string;
|
|
56
|
+
inUse: boolean;
|
|
57
|
+
failures: number;
|
|
58
|
+
lastUsedAt: Date | null;
|
|
59
|
+
isDead: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
currentEndpoint: string | null;
|
|
62
|
+
}
|
|
63
|
+
export interface ProxyRotatorHandle {
|
|
64
|
+
/** Get an Agent for the current endpoint. Use this in fetchOptions.agent or makeWASocket's options.agent */
|
|
65
|
+
currentAgent(): Agent | null;
|
|
66
|
+
/** Get the current endpoint's metadata */
|
|
67
|
+
current(): ProxyEndpoint | null;
|
|
68
|
+
/** Force rotate to next endpoint. Reason logged in stats. */
|
|
69
|
+
rotate(reason?: 'manual' | 'disconnect' | 'ban-warning' | 'scheduled'): ProxyEndpoint | null;
|
|
70
|
+
/** Mark current endpoint as failed (increments failure counter, may auto-rotate) */
|
|
71
|
+
markFailure(): void;
|
|
72
|
+
/** Clear all dead-flags (e.g. for cooldown override) */
|
|
73
|
+
resurrectAll(): void;
|
|
74
|
+
/** Stop scheduled rotation timer + dispose */
|
|
75
|
+
stop(): void;
|
|
76
|
+
/** Stats */
|
|
77
|
+
getStats(): ProxyRotatorStats;
|
|
78
|
+
}
|
|
79
|
+
export declare function proxyRotator(config: ProxyRotatorConfig): ProxyRotatorHandle;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Rotation — Native proxy injection for Baileys with health tracking
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS)
|
|
5
|
+
* are flagged, while residential/4G proxies stay alive. No Baileys library handles
|
|
6
|
+
* native proxy injection — every implementation is DIY hacks. We close that gap.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multi-strategy rotation (round-robin, random, LRU, weighted)
|
|
10
|
+
* - Auto-failover on endpoint failure
|
|
11
|
+
* - Scheduled rotation for proactive IP rotation
|
|
12
|
+
* - Cooldown periods between endpoint reuse
|
|
13
|
+
* - Health tracking and auto-resurrection
|
|
14
|
+
* - Lazy-loaded proxy agent dependencies (optional peerDeps)
|
|
15
|
+
*
|
|
16
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
17
|
+
* @license MIT
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
// Create require for optional peer dependency loading in ESM
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const NoopLogger = {
|
|
23
|
+
info: () => { },
|
|
24
|
+
warn: () => { },
|
|
25
|
+
error: () => { },
|
|
26
|
+
};
|
|
27
|
+
export function proxyRotator(config) {
|
|
28
|
+
const { pool, strategy = 'round-robin', rotateOn = ['disconnect', 'ban-warning'], scheduledIntervalMs = 0, maxFailures = 3, deadCooldownMs = 600_000, // 10 minutes
|
|
29
|
+
logger = NoopLogger, } = config;
|
|
30
|
+
if (!pool || pool.length === 0) {
|
|
31
|
+
throw new Error('proxyRotator: pool cannot be empty');
|
|
32
|
+
}
|
|
33
|
+
// Warn once for pool size 1
|
|
34
|
+
if (pool.length === 1) {
|
|
35
|
+
logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.');
|
|
36
|
+
}
|
|
37
|
+
// Warn for aggressive scheduled rotation
|
|
38
|
+
if (scheduledIntervalMs > 0 && scheduledIntervalMs < 60_000) {
|
|
39
|
+
logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`);
|
|
40
|
+
}
|
|
41
|
+
// Internal state
|
|
42
|
+
const states = pool.map((endpoint) => ({
|
|
43
|
+
endpoint,
|
|
44
|
+
failures: 0,
|
|
45
|
+
lastUsedAt: null,
|
|
46
|
+
isDead: false,
|
|
47
|
+
}));
|
|
48
|
+
let currentIndex = 0;
|
|
49
|
+
let totalRotations = 0;
|
|
50
|
+
const rotationsByTrigger = {};
|
|
51
|
+
let scheduledTimer = null;
|
|
52
|
+
// Agent cache: map endpoint -> Agent (cleared on rotation)
|
|
53
|
+
const agentCache = new Map();
|
|
54
|
+
// Module cache for lazy-loaded proxy agents
|
|
55
|
+
const moduleCache = {};
|
|
56
|
+
function buildProxyUrl(endpoint) {
|
|
57
|
+
const { type, host, port, username, password } = endpoint;
|
|
58
|
+
const auth = username && password ? `${username}:${password}@` : '';
|
|
59
|
+
return `${type}://${auth}${host}:${port}`;
|
|
60
|
+
}
|
|
61
|
+
function createAgentForEndpointSync(endpoint) {
|
|
62
|
+
// Check cache first
|
|
63
|
+
if (agentCache.has(endpoint)) {
|
|
64
|
+
return agentCache.get(endpoint);
|
|
65
|
+
}
|
|
66
|
+
const url = buildProxyUrl(endpoint);
|
|
67
|
+
let agent = null;
|
|
68
|
+
try {
|
|
69
|
+
if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
|
|
70
|
+
if (!moduleCache['socks-proxy-agent']) {
|
|
71
|
+
try {
|
|
72
|
+
moduleCache['socks-proxy-agent'] = require('socks-proxy-agent');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url);
|
|
80
|
+
}
|
|
81
|
+
else if (endpoint.type === 'http') {
|
|
82
|
+
if (!moduleCache['http-proxy-agent']) {
|
|
83
|
+
try {
|
|
84
|
+
moduleCache['http-proxy-agent'] = require('http-proxy-agent');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url);
|
|
92
|
+
}
|
|
93
|
+
else if (endpoint.type === 'https') {
|
|
94
|
+
if (!moduleCache['https-proxy-agent']) {
|
|
95
|
+
try {
|
|
96
|
+
moduleCache['https-proxy-agent'] = require('https-proxy-agent');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent');
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.error?.(`Unknown proxy type: ${endpoint.type}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Cache the agent
|
|
110
|
+
if (agent) {
|
|
111
|
+
agentCache.set(endpoint, agent);
|
|
112
|
+
}
|
|
113
|
+
return agent;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function getAliveEndpoints() {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
return states
|
|
123
|
+
.map((s, idx) => {
|
|
124
|
+
// Dead check with auto-resurrection
|
|
125
|
+
if (s.isDead && s.lastUsedAt) {
|
|
126
|
+
if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
|
|
127
|
+
s.isDead = false;
|
|
128
|
+
s.failures = 0;
|
|
129
|
+
logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Cooldown check
|
|
133
|
+
const cooldown = s.endpoint.cooldownMs || 0;
|
|
134
|
+
if (cooldown > 0 && s.lastUsedAt) {
|
|
135
|
+
if (now - s.lastUsedAt.getTime() < cooldown) {
|
|
136
|
+
return -1; // Still in cooldown
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return !s.isDead ? idx : -1;
|
|
140
|
+
})
|
|
141
|
+
.filter((idx) => idx !== -1);
|
|
142
|
+
}
|
|
143
|
+
function selectNextIndex(alive) {
|
|
144
|
+
if (alive.length === 0)
|
|
145
|
+
return currentIndex; // All dead, stay on current
|
|
146
|
+
if (strategy === 'round-robin') {
|
|
147
|
+
// Pick next after currentIndex in circular fashion
|
|
148
|
+
const afterCurrent = alive.filter((idx) => idx > currentIndex);
|
|
149
|
+
if (afterCurrent.length > 0)
|
|
150
|
+
return afterCurrent[0];
|
|
151
|
+
return alive[0]; // Wrap around
|
|
152
|
+
}
|
|
153
|
+
if (strategy === 'random') {
|
|
154
|
+
return alive[Math.floor(Math.random() * alive.length)];
|
|
155
|
+
}
|
|
156
|
+
if (strategy === 'least-recently-used') {
|
|
157
|
+
// Pick the one with oldest lastUsedAt (never-used = highest priority)
|
|
158
|
+
const neverUsed = alive.filter((idx) => states[idx].lastUsedAt === null);
|
|
159
|
+
if (neverUsed.length > 0) {
|
|
160
|
+
return neverUsed[0];
|
|
161
|
+
}
|
|
162
|
+
let oldestIdx = alive[0];
|
|
163
|
+
let oldestTime = states[oldestIdx].lastUsedAt.getTime();
|
|
164
|
+
for (const idx of alive) {
|
|
165
|
+
const time = states[idx].lastUsedAt.getTime();
|
|
166
|
+
if (time < oldestTime) {
|
|
167
|
+
oldestTime = time;
|
|
168
|
+
oldestIdx = idx;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return oldestIdx;
|
|
172
|
+
}
|
|
173
|
+
if (strategy === 'weighted') {
|
|
174
|
+
// Weighted by inverse failure count (healthier = more likely)
|
|
175
|
+
const weights = alive.map((idx) => {
|
|
176
|
+
const failures = states[idx].failures;
|
|
177
|
+
return 1 / (failures + 1); // Avoid divide-by-zero
|
|
178
|
+
});
|
|
179
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
180
|
+
let rand = Math.random() * totalWeight;
|
|
181
|
+
for (let i = 0; i < alive.length; i++) {
|
|
182
|
+
rand -= weights[i];
|
|
183
|
+
if (rand <= 0)
|
|
184
|
+
return alive[i];
|
|
185
|
+
}
|
|
186
|
+
return alive[alive.length - 1]; // Fallback
|
|
187
|
+
}
|
|
188
|
+
return alive[0]; // Default fallback
|
|
189
|
+
}
|
|
190
|
+
function rotateImpl(reason = 'manual') {
|
|
191
|
+
if (pool.length === 1) {
|
|
192
|
+
// No-op for single endpoint
|
|
193
|
+
return states[0].endpoint;
|
|
194
|
+
}
|
|
195
|
+
const alive = getAliveEndpoints();
|
|
196
|
+
if (alive.length === 0) {
|
|
197
|
+
logger.warn?.('All endpoints are dead. Cannot rotate.');
|
|
198
|
+
return states[currentIndex].endpoint;
|
|
199
|
+
}
|
|
200
|
+
const nextIdx = selectNextIndex(alive);
|
|
201
|
+
if (nextIdx === currentIndex && alive.length > 1) {
|
|
202
|
+
// Try to pick a different one if possible
|
|
203
|
+
const others = alive.filter((idx) => idx !== currentIndex);
|
|
204
|
+
if (others.length > 0) {
|
|
205
|
+
currentIndex = others[0];
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
currentIndex = nextIdx;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
currentIndex = nextIdx;
|
|
213
|
+
}
|
|
214
|
+
states[currentIndex].lastUsedAt = new Date();
|
|
215
|
+
totalRotations++;
|
|
216
|
+
rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1;
|
|
217
|
+
const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host;
|
|
218
|
+
logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`);
|
|
219
|
+
return states[currentIndex].endpoint;
|
|
220
|
+
}
|
|
221
|
+
function markFailureImpl() {
|
|
222
|
+
const state = states[currentIndex];
|
|
223
|
+
state.failures++;
|
|
224
|
+
const label = state.endpoint.label || state.endpoint.host;
|
|
225
|
+
logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`);
|
|
226
|
+
if (state.failures >= maxFailures) {
|
|
227
|
+
state.isDead = true;
|
|
228
|
+
logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`);
|
|
229
|
+
// Auto-rotate to next alive endpoint
|
|
230
|
+
const alive = getAliveEndpoints();
|
|
231
|
+
if (alive.length > 0) {
|
|
232
|
+
rotateImpl('manual'); // Trigger rotation as recovery
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function resurrectAllImpl() {
|
|
237
|
+
let count = 0;
|
|
238
|
+
for (const state of states) {
|
|
239
|
+
if (state.isDead) {
|
|
240
|
+
state.isDead = false;
|
|
241
|
+
state.failures = 0;
|
|
242
|
+
count++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (count > 0) {
|
|
246
|
+
logger.info?.(`Resurrected ${count} dead endpoint(s)`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function stopImpl() {
|
|
250
|
+
if (scheduledTimer) {
|
|
251
|
+
clearInterval(scheduledTimer);
|
|
252
|
+
scheduledTimer = null;
|
|
253
|
+
logger.info?.('Stopped scheduled rotation timer');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function getStatsImpl() {
|
|
257
|
+
return {
|
|
258
|
+
totalRotations,
|
|
259
|
+
rotationsByTrigger: { ...rotationsByTrigger },
|
|
260
|
+
endpointHealth: states.map((s) => ({
|
|
261
|
+
label: s.endpoint.label || s.endpoint.host,
|
|
262
|
+
inUse: states[currentIndex] === s,
|
|
263
|
+
failures: s.failures,
|
|
264
|
+
lastUsedAt: s.lastUsedAt,
|
|
265
|
+
isDead: s.isDead,
|
|
266
|
+
})),
|
|
267
|
+
currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function currentAgentImpl() {
|
|
271
|
+
const endpoint = states[currentIndex].endpoint;
|
|
272
|
+
return createAgentForEndpointSync(endpoint);
|
|
273
|
+
}
|
|
274
|
+
function currentImpl() {
|
|
275
|
+
return states[currentIndex].endpoint;
|
|
276
|
+
}
|
|
277
|
+
// Setup scheduled rotation if enabled
|
|
278
|
+
if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
|
|
279
|
+
scheduledTimer = setInterval(() => {
|
|
280
|
+
rotateImpl('scheduled');
|
|
281
|
+
}, scheduledIntervalMs);
|
|
282
|
+
logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`);
|
|
283
|
+
}
|
|
284
|
+
// Initialize: select first endpoint
|
|
285
|
+
states[0].lastUsedAt = new Date();
|
|
286
|
+
return {
|
|
287
|
+
currentAgent: currentAgentImpl,
|
|
288
|
+
current: currentImpl,
|
|
289
|
+
rotate: rotateImpl,
|
|
290
|
+
markFailure: markFailureImpl,
|
|
291
|
+
resurrectAll: resurrectAllImpl,
|
|
292
|
+
stop: stopImpl,
|
|
293
|
+
getStats: getStatsImpl,
|
|
294
|
+
};
|
|
295
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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",
|
|
@@ -76,6 +76,15 @@
|
|
|
76
76
|
},
|
|
77
77
|
"@oxidezap/baileyrs": {
|
|
78
78
|
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"socks-proxy-agent": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"http-proxy-agent": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"https-proxy-agent": {
|
|
87
|
+
"optional": true
|
|
79
88
|
}
|
|
80
89
|
},
|
|
81
90
|
"devDependencies": {
|
|
@@ -86,5 +95,10 @@
|
|
|
86
95
|
"tsx": "^4.21.0",
|
|
87
96
|
"typescript": "^5.0.0",
|
|
88
97
|
"vitest": "^4.1.5"
|
|
98
|
+
},
|
|
99
|
+
"dependencies": {
|
|
100
|
+
"http-proxy-agent": "^9.0.0",
|
|
101
|
+
"https-proxy-agent": "^9.0.0",
|
|
102
|
+
"socks-proxy-agent": "^10.0.0"
|
|
89
103
|
}
|
|
90
104
|
}
|