baileys-antiban 1.6.0 → 2.1.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 +72 -0
- package/README.md +138 -1
- package/dist/antiban.d.ts +17 -0
- package/dist/antiban.js +29 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/lidFirstResolver.d.ts +71 -0
- package/dist/lidFirstResolver.js +174 -0
- package/dist/retryReason.d.ts +49 -0
- package/dist/retryReason.js +91 -0
- package/dist/sessionStability.d.ts +93 -0
- package/dist/sessionStability.js +241 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,78 @@ 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
|
+
## [2.1.0] - 2026-04-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Extended disconnect code coverage** — Added 405, 409, 412 to `classifyDisconnect()`
|
|
12
|
+
- **405** (Method Not Allowed) → `fatal`, no reconnect
|
|
13
|
+
- **409** (Conflict / Connection Replaced) → `fatal`, no reconnect (merged with 428 behavior)
|
|
14
|
+
- **412** (Precondition Failed) → `recoverable`, 30s backoff (auth state mismatch, retry after delay)
|
|
15
|
+
- **LidFirstResolver** — Standalone drop-in utility for LID↔phone mapping
|
|
16
|
+
- Loads mappings from Baileys auth state directory (`lid-mapping-*_reverse.json`)
|
|
17
|
+
- `resolveToLID(phoneOrJid)` — phone → LID lookup
|
|
18
|
+
- `resolveToPhone(lid)` — LID → phone lookup
|
|
19
|
+
- `loadFromAuthDir(dir)` — bulk load from auth state
|
|
20
|
+
- `learnFromEvent(event)` — learn from Baileys events (future-proof)
|
|
21
|
+
- `getMapping(jid)` — full mapping with metadata
|
|
22
|
+
- Factory function `createLidFirstResolver()` for singleton pattern
|
|
23
|
+
- Works independently of full AntiBan system
|
|
24
|
+
- **MessageRetryReason enum** — Typed retry reason codes for message encryption failures
|
|
25
|
+
- 8 retry reason codes: UnknownError, GenericError, SignalErrorInvalidKeyId, SignalErrorInvalidMessage, SignalErrorNoSession, SignalErrorBadMac, MessageExpired, DecryptionError
|
|
26
|
+
- `MAC_ERROR_CODES` set for quick MAC error detection
|
|
27
|
+
- `parseRetryReason(code)` — parse from string/number to enum
|
|
28
|
+
- `isMacError(reason)` — check if reason is a MAC error
|
|
29
|
+
- `getRetryReasonDescription(reason)` — human-readable descriptions
|
|
30
|
+
- Based on whatsapp-rust and Baileys protocol research
|
|
31
|
+
- Named `MessageRetryReason` to avoid conflict with existing `RetryReason` type from `retryTracker.ts`
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `index.ts` now exports `LidFirstResolver`, `createLidFirstResolver`, `LidPhoneMapping`, `MessageRetryReason`, `MAC_ERROR_CODES`, `parseRetryReason`, `isMacError`, `getRetryReasonDescription`
|
|
35
|
+
|
|
36
|
+
### Tests
|
|
37
|
+
- 30 new tests for `LidFirstResolver` (auth dir loading, phone↔LID resolution, malformed input handling, factory function)
|
|
38
|
+
- 21 new tests for `RetryReason` (enum values, MAC error detection, parsing, descriptions, integration scenarios)
|
|
39
|
+
- 3 new tests for disconnect codes 405, 409, 412 in `sessionStability.test.ts`
|
|
40
|
+
- Total new test coverage: 54 tests
|
|
41
|
+
|
|
42
|
+
### Technical Details
|
|
43
|
+
- `LidFirstResolver` uses in-memory maps for O(1) lookup performance
|
|
44
|
+
- Handles device suffix normalization (`:N` in JIDs)
|
|
45
|
+
- Gracefully handles malformed auth dirs and JSON files (no crashes)
|
|
46
|
+
- `RetryReason` enum matches Signal protocol + WhatsApp extensions
|
|
47
|
+
- Backward compatible — all new features are opt-in, no breaking changes
|
|
48
|
+
|
|
49
|
+
## [2.0.0] - 2026-04-19
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Session Stability Module** — New middleware layer for Baileys socket stability (opt-in, backward compatible)
|
|
53
|
+
- `wrapWithSessionStability()` — Proxy wrapper for Baileys socket with stability features
|
|
54
|
+
- `SessionHealthMonitor` — Track decrypt success/fail ratio, emit degradation alerts when Bad MAC rate exceeds threshold
|
|
55
|
+
- `classifyDisconnect()` — Typed disconnect reason classification with recovery recommendations
|
|
56
|
+
- Canonical JID normalization before `sendMessage()` — Auto-resolves PN↔LID using `LidResolver` to reduce mutex race triggers
|
|
57
|
+
- Comprehensive disconnect code coverage: 401, 408, 428, 429, 440, 500, 503, 515, 1000, unknown
|
|
58
|
+
- Degradation detection: triggers `onDegraded` callback when Bad MAC count exceeds threshold in time window (default: 3 in 60s)
|
|
59
|
+
- Recovery detection: triggers `onRecovered` callback when Bad MAC rate drops below threshold
|
|
60
|
+
- 19 new tests with 100% coverage of disconnect classification and health monitoring
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- `AntiBan` class extended with optional `sessionStability` config (default: disabled)
|
|
64
|
+
- `AntiBanConfig` interface includes `sessionStability` options (enabled, canonicalJidNormalization, healthMonitoring, badMacThreshold, badMacWindowMs)
|
|
65
|
+
- `AntiBanStats` includes `sessionStability` stats when enabled
|
|
66
|
+
- `destroy()` now cleans up session stability monitor
|
|
67
|
+
- Exposed `sessionStability` getter for direct access to health monitor
|
|
68
|
+
|
|
69
|
+
### Technical Details
|
|
70
|
+
- Pure middleware layer — no Baileys internals modification required
|
|
71
|
+
- Works alongside existing v1.x LID resolver and canonicalizer modules
|
|
72
|
+
- Default configuration: disabled for backward compatibility, opt-in via `sessionStability: { enabled: true }`
|
|
73
|
+
- Health monitor uses sliding window for Bad MAC detection (default: 3 errors in 60 seconds)
|
|
74
|
+
- Socket wrapper uses ES6 Proxy for transparent method interception
|
|
75
|
+
- TypeScript strict mode compliant, no `any` types except socket wrapper generic
|
|
76
|
+
|
|
77
|
+
### Breaking Changes
|
|
78
|
+
None — all v2.0 features are opt-in and backward compatible with v1.x
|
|
79
|
+
|
|
8
80
|
## [1.6.0] - 2026-04-18
|
|
9
81
|
|
|
10
82
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,144 @@
|
|
|
6
6
|
|
|
7
7
|
**Transport-agnostic** anti-ban middleware — protect your WhatsApp number with human-like messaging patterns. Works with both [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## v2.0 New Features — Session Stability Module
|
|
10
|
+
|
|
11
|
+
### What's New in v2.0
|
|
12
|
+
|
|
13
|
+
Three powerful new features to improve session stability and reduce "Bad MAC" errors:
|
|
14
|
+
|
|
15
|
+
1. **Typed Disconnect Reason Classification** — Know exactly why you disconnected and how to recover
|
|
16
|
+
2. **Session Health Monitor** — Detect session degradation before it causes bans
|
|
17
|
+
3. **Socket Wrapper with JID Canonicalization** — Middleware-layer fix for LID/PN race conditions
|
|
18
|
+
|
|
19
|
+
All v2.0 features are **opt-in** and **100% backward compatible** with v1.x.
|
|
20
|
+
|
|
21
|
+
### 1. Typed Disconnect Reason Classification
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { classifyDisconnect } from 'baileys-antiban';
|
|
25
|
+
|
|
26
|
+
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
27
|
+
if (connection === 'close' && lastDisconnect?.error) {
|
|
28
|
+
const statusCode = lastDisconnect.error.output?.statusCode;
|
|
29
|
+
const classification = classifyDisconnect(statusCode);
|
|
30
|
+
|
|
31
|
+
console.log(`Disconnected: ${classification.message}`);
|
|
32
|
+
console.log(`Category: ${classification.category}`); // fatal | recoverable | rate-limited | unknown
|
|
33
|
+
console.log(`Should reconnect: ${classification.shouldReconnect}`);
|
|
34
|
+
|
|
35
|
+
if (classification.shouldReconnect && classification.backoffMs) {
|
|
36
|
+
console.log(`Recommended backoff: ${classification.backoffMs}ms`);
|
|
37
|
+
setTimeout(() => connectToWhatsApp(), classification.backoffMs);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Supported disconnect codes**: 401 (logged out), 408 (timeout), 428 (connection replaced), 429 (rate limited), 440 (logged out), 500 (internal error), 503 (unavailable), 515 (restart required), 1000 (graceful close), and unknown codes.
|
|
44
|
+
|
|
45
|
+
### 2. Session Health Monitor
|
|
46
|
+
|
|
47
|
+
Track decrypt success/failure ratio to detect session degradation **before** it causes a ban:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { SessionHealthMonitor } from 'baileys-antiban';
|
|
51
|
+
|
|
52
|
+
const healthMonitor = new SessionHealthMonitor({
|
|
53
|
+
badMacThreshold: 3, // Alert after 3 Bad MACs
|
|
54
|
+
badMacWindowMs: 60_000, // ...in 60 seconds
|
|
55
|
+
onDegraded: (stats) => {
|
|
56
|
+
console.error(`🔴 SESSION DEGRADED: ${stats.badMacCount} Bad MACs in last minute`);
|
|
57
|
+
console.error('Action required: Restart session or switch to LID-based canonical form');
|
|
58
|
+
},
|
|
59
|
+
onRecovered: (stats) => {
|
|
60
|
+
console.log('🟢 Session recovered — decrypt success rate improved');
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Wire to Baileys events
|
|
65
|
+
sock.ev.on('messages.update', (updates) => {
|
|
66
|
+
for (const { key, update } of updates) {
|
|
67
|
+
if (update.messageStubType === Types.WAMessageStubType.CIPHERTEXT) {
|
|
68
|
+
healthMonitor.recordDecryptFail(true); // Bad MAC detected
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Check status anytime
|
|
74
|
+
const stats = healthMonitor.getStats();
|
|
75
|
+
console.log(`Decrypt success: ${stats.decryptSuccess}`);
|
|
76
|
+
console.log(`Bad MAC count: ${stats.badMacCount}`);
|
|
77
|
+
console.log(`Is degraded: ${stats.isDegraded}`);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Socket Wrapper with JID Canonicalization
|
|
81
|
+
|
|
82
|
+
The easiest way to use v2.0: wrap your socket for automatic JID canonicalization and health monitoring:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { wrapWithSessionStability, LidResolver } from 'baileys-antiban';
|
|
86
|
+
|
|
87
|
+
const resolver = new LidResolver({ canonical: 'pn' });
|
|
88
|
+
const sock = makeWASocket({ ... });
|
|
89
|
+
|
|
90
|
+
const safeSock = wrapWithSessionStability(sock, {
|
|
91
|
+
canonicalJidNormalization: true, // Auto-canonicalize JIDs before sendMessage
|
|
92
|
+
healthMonitoring: true, // Auto-track decrypt health
|
|
93
|
+
lidResolver: resolver,
|
|
94
|
+
health: {
|
|
95
|
+
badMacThreshold: 3,
|
|
96
|
+
badMacWindowMs: 60_000,
|
|
97
|
+
onDegraded: (stats) => console.error('Session degraded!'),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Use safeSock exactly like normal sock
|
|
102
|
+
await safeSock.sendMessage('123456@lid', { text: 'hello' });
|
|
103
|
+
// ^ Automatically canonicalized to '27825651069@s.whatsapp.net' if mapping exists
|
|
104
|
+
|
|
105
|
+
// Access health stats
|
|
106
|
+
const healthStats = safeSock.sessionHealthStats;
|
|
107
|
+
console.log(`Bad MAC count: ${healthStats.badMacCount}`);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Integration with AntiBan Class
|
|
111
|
+
|
|
112
|
+
You can also enable session stability via the main `AntiBan` config:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { AntiBan } from 'baileys-antiban';
|
|
116
|
+
|
|
117
|
+
const antiban = new AntiBan({
|
|
118
|
+
sessionStability: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
canonicalJidNormalization: true, // Auto-canonicalize JIDs
|
|
121
|
+
healthMonitoring: true, // Track Bad MAC rate
|
|
122
|
+
badMacThreshold: 3,
|
|
123
|
+
badMacWindowMs: 60_000,
|
|
124
|
+
},
|
|
125
|
+
jidCanonicalizer: {
|
|
126
|
+
enabled: true,
|
|
127
|
+
canonical: 'pn',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Access health monitor directly
|
|
132
|
+
const healthMonitor = antiban.sessionStability;
|
|
133
|
+
if (healthMonitor) {
|
|
134
|
+
console.log(healthMonitor.getStats());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Stats include session stability
|
|
138
|
+
const stats = antiban.getStats();
|
|
139
|
+
console.log(stats.sessionStability); // Health stats when enabled
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Why v2.0?** Bad MAC errors are the #1 reported Baileys issue. Session stability features give you early warning and automated mitigation, reducing bans caused by session degradation.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## v1.5 Features
|
|
10
147
|
|
|
11
148
|
### RetryReasonTracker
|
|
12
149
|
Tracks message retry reasons and detects retry spirals (when the same message keeps failing). Inspired by whatsapp-rust's protocol/retry.rs module.
|
package/dist/antiban.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { RetryReasonTracker, type RetryTrackerConfig, type RetryStats } from './
|
|
|
24
24
|
import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
|
|
25
25
|
import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
|
|
26
26
|
import { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
|
|
27
|
+
import { SessionHealthMonitor, type SessionHealthStats } from './sessionStability.js';
|
|
27
28
|
export interface AntiBanConfig {
|
|
28
29
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
29
30
|
warmUp?: Partial<WarmUpConfig>;
|
|
@@ -36,6 +37,18 @@ export interface AntiBanConfig {
|
|
|
36
37
|
reconnectThrottle?: Partial<ReconnectThrottleConfig>;
|
|
37
38
|
lidResolver?: LidResolverConfig;
|
|
38
39
|
jidCanonicalizer?: JidCanonicalizerConfig;
|
|
40
|
+
/** Session stability features (v2.0) — default disabled for backward compatibility */
|
|
41
|
+
sessionStability?: {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
/** Enable canonical JID normalization before sendMessage (default: true if enabled) */
|
|
44
|
+
canonicalJidNormalization?: boolean;
|
|
45
|
+
/** Enable session health monitoring (default: true if enabled) */
|
|
46
|
+
healthMonitoring?: boolean;
|
|
47
|
+
/** Bad MAC threshold before declaring session degraded (default: 3) */
|
|
48
|
+
badMacThreshold?: number;
|
|
49
|
+
/** Time window for Bad MAC threshold in ms (default: 60000) */
|
|
50
|
+
badMacWindowMs?: number;
|
|
51
|
+
};
|
|
39
52
|
/** Log warnings and blocks to console (default: true) */
|
|
40
53
|
logging?: boolean;
|
|
41
54
|
}
|
|
@@ -60,6 +73,7 @@ export interface AntiBanStats {
|
|
|
60
73
|
reconnectThrottle?: ReconnectThrottleStats | null;
|
|
61
74
|
lidResolver?: LidResolverStats | null;
|
|
62
75
|
jidCanonicalizer?: JidCanonicalizerStats | null;
|
|
76
|
+
sessionStability?: SessionHealthStats | null;
|
|
63
77
|
}
|
|
64
78
|
export declare class AntiBan {
|
|
65
79
|
private rateLimiter;
|
|
@@ -73,6 +87,7 @@ export declare class AntiBan {
|
|
|
73
87
|
private reconnectThrottleModule;
|
|
74
88
|
private lidResolverModule;
|
|
75
89
|
private jidCanonicalizerModule;
|
|
90
|
+
private sessionStabilityMonitor;
|
|
76
91
|
private logging;
|
|
77
92
|
private stats;
|
|
78
93
|
constructor(config?: AntiBanConfig, warmUpState?: WarmUpState);
|
|
@@ -126,6 +141,8 @@ export declare class AntiBan {
|
|
|
126
141
|
get lidResolver(): LidResolver | null;
|
|
127
142
|
/** Get the JID canonicalizer for direct access */
|
|
128
143
|
get jidCanonicalizer(): JidCanonicalizer | null;
|
|
144
|
+
/** Get the session stability monitor for direct access */
|
|
145
|
+
get sessionStability(): SessionHealthMonitor | null;
|
|
129
146
|
/**
|
|
130
147
|
* Export warm-up state for persistence between restarts
|
|
131
148
|
*/
|
package/dist/antiban.js
CHANGED
|
@@ -24,6 +24,7 @@ import { RetryReasonTracker } from './retryTracker.js';
|
|
|
24
24
|
import { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
25
25
|
import { LidResolver } from './lidResolver.js';
|
|
26
26
|
import { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
27
|
+
import { SessionHealthMonitor } from './sessionStability.js';
|
|
27
28
|
export class AntiBan {
|
|
28
29
|
rateLimiter;
|
|
29
30
|
warmUp;
|
|
@@ -36,6 +37,7 @@ export class AntiBan {
|
|
|
36
37
|
reconnectThrottleModule;
|
|
37
38
|
lidResolverModule = null;
|
|
38
39
|
jidCanonicalizerModule = null;
|
|
40
|
+
sessionStabilityMonitor = null;
|
|
39
41
|
logging;
|
|
40
42
|
stats = {
|
|
41
43
|
messagesAllowed: 0,
|
|
@@ -114,6 +116,25 @@ export class AntiBan {
|
|
|
114
116
|
// Standalone resolver without canonicalizer
|
|
115
117
|
this.lidResolverModule = new LidResolver(config.lidResolver);
|
|
116
118
|
}
|
|
119
|
+
// Initialize session stability monitor if enabled
|
|
120
|
+
if (config.sessionStability?.enabled) {
|
|
121
|
+
const healthConfig = {
|
|
122
|
+
badMacThreshold: config.sessionStability.badMacThreshold,
|
|
123
|
+
badMacWindowMs: config.sessionStability.badMacWindowMs,
|
|
124
|
+
onDegraded: (stats) => {
|
|
125
|
+
if (this.logging) {
|
|
126
|
+
console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${config.sessionStability?.badMacWindowMs || 60000}ms`);
|
|
127
|
+
console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
onRecovered: () => {
|
|
131
|
+
if (this.logging) {
|
|
132
|
+
console.log(`[baileys-antiban] 🟢 SESSION RECOVERED — decrypt success rate improved`);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
|
|
137
|
+
}
|
|
117
138
|
}
|
|
118
139
|
/**
|
|
119
140
|
* Check if a message can be sent and get required delay.
|
|
@@ -320,6 +341,9 @@ export class AntiBan {
|
|
|
320
341
|
if (this.jidCanonicalizerModule) {
|
|
321
342
|
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
|
|
322
343
|
}
|
|
344
|
+
if (this.sessionStabilityMonitor) {
|
|
345
|
+
stats.sessionStability = this.sessionStabilityMonitor.getStats();
|
|
346
|
+
}
|
|
323
347
|
return stats;
|
|
324
348
|
}
|
|
325
349
|
/** Get the timelock guard for direct access */
|
|
@@ -354,6 +378,10 @@ export class AntiBan {
|
|
|
354
378
|
get jidCanonicalizer() {
|
|
355
379
|
return this.jidCanonicalizerModule;
|
|
356
380
|
}
|
|
381
|
+
/** Get the session stability monitor for direct access */
|
|
382
|
+
get sessionStability() {
|
|
383
|
+
return this.sessionStabilityMonitor;
|
|
384
|
+
}
|
|
357
385
|
/**
|
|
358
386
|
* Export warm-up state for persistence between restarts
|
|
359
387
|
*/
|
|
@@ -408,6 +436,7 @@ export class AntiBan {
|
|
|
408
436
|
this.reconnectThrottleModule.destroy();
|
|
409
437
|
this.jidCanonicalizerModule?.destroy();
|
|
410
438
|
this.lidResolverModule?.destroy();
|
|
439
|
+
this.sessionStabilityMonitor?.reset();
|
|
411
440
|
if (this.logging) {
|
|
412
441
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
413
442
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type Retr
|
|
|
19
19
|
export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
|
|
20
20
|
export { LidResolver, type LidResolverConfig, type LidResolverStats, type LidMapping } from './lidResolver.js';
|
|
21
21
|
export { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
|
|
22
|
+
export { SessionHealthMonitor, type SessionHealthStats, type SessionHealthConfig, wrapWithSessionStability, type SessionStabilityConfig, classifyDisconnect, type DisconnectClassification, type DisconnectCategory, } from './sessionStability.js';
|
|
23
|
+
export { LidFirstResolver, createLidFirstResolver, type LidPhoneMapping, } from './lidFirstResolver.js';
|
|
24
|
+
export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
|
|
22
25
|
export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
|
|
23
26
|
export { MessageQueue, type QueuedMessage, type MessageQueueConfig } from './messageQueue.js';
|
|
24
27
|
export { ContentVariator, type VariatorConfig } from './contentVariator.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,11 @@ export { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
|
23
23
|
// v1.6 new modules
|
|
24
24
|
export { LidResolver } from './lidResolver.js';
|
|
25
25
|
export { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
26
|
+
// v2.0 new modules
|
|
27
|
+
export { SessionHealthMonitor, wrapWithSessionStability, classifyDisconnect, } from './sessionStability.js';
|
|
28
|
+
// v2.1 new modules
|
|
29
|
+
export { LidFirstResolver, createLidFirstResolver, } from './lidFirstResolver.js';
|
|
30
|
+
export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
|
|
26
31
|
// Socket wrapper
|
|
27
32
|
export { wrapSocket } from './wrapper.js';
|
|
28
33
|
// Optional features
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LidFirstResolver — Standalone LID↔Phone mapper for Baileys auth state
|
|
3
|
+
*
|
|
4
|
+
* Lightweight drop-in utility that:
|
|
5
|
+
* - Loads LID↔phone mappings from Baileys' auth state directory
|
|
6
|
+
* - Resolves phone numbers to LID JIDs and vice versa
|
|
7
|
+
* - Learns new mappings from Baileys events
|
|
8
|
+
* - Works independently of the full AntiBan system
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { LidFirstResolver } from 'baileys-antiban';
|
|
13
|
+
* const resolver = new LidFirstResolver();
|
|
14
|
+
* resolver.loadFromAuthDir('./whatsapp-auth/my-session');
|
|
15
|
+
* const jid = resolver.resolveToLID('27825651069'); // → "210543692497008@lid" or null
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
19
|
+
* @license MIT
|
|
20
|
+
*/
|
|
21
|
+
export interface LidPhoneMapping {
|
|
22
|
+
lid: string;
|
|
23
|
+
phone: string;
|
|
24
|
+
learnedAt: number;
|
|
25
|
+
source: 'auth-dir' | 'event';
|
|
26
|
+
}
|
|
27
|
+
export declare class LidFirstResolver {
|
|
28
|
+
private lidToPhone;
|
|
29
|
+
private phoneToLid;
|
|
30
|
+
/**
|
|
31
|
+
* Load mappings from Baileys auth state directory.
|
|
32
|
+
* Looks for lid-mapping-*_reverse.json files.
|
|
33
|
+
*/
|
|
34
|
+
loadFromAuthDir(authDir: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Learn a new mapping from a Baileys event (messages, contacts, etc.).
|
|
37
|
+
* Accepts partial data — will extract what it can.
|
|
38
|
+
*/
|
|
39
|
+
learnFromEvent(event: any): void;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve phone number or phone JID to LID JID.
|
|
42
|
+
* Returns null if not known.
|
|
43
|
+
*/
|
|
44
|
+
resolveToLID(phoneOrJid: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve LID JID to phone number.
|
|
47
|
+
* Returns null if not known.
|
|
48
|
+
*/
|
|
49
|
+
resolveToPhone(lid: string): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Get full mapping for a given JID (either LID or phone).
|
|
52
|
+
* Returns null if not known.
|
|
53
|
+
*/
|
|
54
|
+
getMapping(jid: string): LidPhoneMapping | null;
|
|
55
|
+
/**
|
|
56
|
+
* Get total number of known mappings.
|
|
57
|
+
*/
|
|
58
|
+
size(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Clear all mappings.
|
|
61
|
+
*/
|
|
62
|
+
clear(): void;
|
|
63
|
+
private learnJid;
|
|
64
|
+
private extractPhone;
|
|
65
|
+
private normalizeLid;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Factory function for creating a singleton resolver instance.
|
|
69
|
+
* Useful for shared state across modules.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createLidFirstResolver(): LidFirstResolver;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LidFirstResolver — Standalone LID↔Phone mapper for Baileys auth state
|
|
3
|
+
*
|
|
4
|
+
* Lightweight drop-in utility that:
|
|
5
|
+
* - Loads LID↔phone mappings from Baileys' auth state directory
|
|
6
|
+
* - Resolves phone numbers to LID JIDs and vice versa
|
|
7
|
+
* - Learns new mappings from Baileys events
|
|
8
|
+
* - Works independently of the full AntiBan system
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { LidFirstResolver } from 'baileys-antiban';
|
|
13
|
+
* const resolver = new LidFirstResolver();
|
|
14
|
+
* resolver.loadFromAuthDir('./whatsapp-auth/my-session');
|
|
15
|
+
* const jid = resolver.resolveToLID('27825651069'); // → "210543692497008@lid" or null
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
19
|
+
* @license MIT
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
export class LidFirstResolver {
|
|
24
|
+
lidToPhone = new Map();
|
|
25
|
+
phoneToLid = new Map(); // phone → lid (quick reverse lookup)
|
|
26
|
+
/**
|
|
27
|
+
* Load mappings from Baileys auth state directory.
|
|
28
|
+
* Looks for lid-mapping-*_reverse.json files.
|
|
29
|
+
*/
|
|
30
|
+
loadFromAuthDir(authDir) {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(authDir)) {
|
|
33
|
+
return; // Directory doesn't exist, nothing to load
|
|
34
|
+
}
|
|
35
|
+
const files = fs.readdirSync(authDir);
|
|
36
|
+
const reverseMappingFiles = files.filter(f => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'));
|
|
37
|
+
for (const file of reverseMappingFiles) {
|
|
38
|
+
const filePath = path.join(authDir, file);
|
|
39
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
40
|
+
const data = JSON.parse(content);
|
|
41
|
+
// Baileys reverse mapping format: { "lid@lid": "phone@s.whatsapp.net" }
|
|
42
|
+
for (const [lid, pnJid] of Object.entries(data)) {
|
|
43
|
+
if (typeof pnJid === 'string') {
|
|
44
|
+
const phone = this.extractPhone(pnJid);
|
|
45
|
+
if (phone && lid.endsWith('@lid')) {
|
|
46
|
+
const mapping = {
|
|
47
|
+
lid: this.normalizeLid(lid),
|
|
48
|
+
phone,
|
|
49
|
+
learnedAt: Date.now(),
|
|
50
|
+
source: 'auth-dir',
|
|
51
|
+
};
|
|
52
|
+
this.lidToPhone.set(mapping.lid, mapping);
|
|
53
|
+
this.phoneToLid.set(phone, mapping.lid);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Silently fail — don't crash if auth dir is malformed
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Learn a new mapping from a Baileys event (messages, contacts, etc.).
|
|
65
|
+
* Accepts partial data — will extract what it can.
|
|
66
|
+
*/
|
|
67
|
+
learnFromEvent(event) {
|
|
68
|
+
try {
|
|
69
|
+
// Extract from message event structure
|
|
70
|
+
if (event.key?.remoteJid) {
|
|
71
|
+
const jid = event.key.remoteJid;
|
|
72
|
+
this.learnJid(jid, 'event');
|
|
73
|
+
}
|
|
74
|
+
// Extract from participant field (group messages)
|
|
75
|
+
if (event.key?.participant) {
|
|
76
|
+
const jid = event.key.participant;
|
|
77
|
+
this.learnJid(jid, 'event');
|
|
78
|
+
}
|
|
79
|
+
// Extract from contact event
|
|
80
|
+
if (event.id) {
|
|
81
|
+
this.learnJid(event.id, 'event');
|
|
82
|
+
}
|
|
83
|
+
// Extract from pushName field (has phone)
|
|
84
|
+
if (event.pushName && event.key?.remoteJid) {
|
|
85
|
+
this.learnJid(event.key.remoteJid, 'event');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// Silently fail — don't crash on malformed events
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve phone number or phone JID to LID JID.
|
|
94
|
+
* Returns null if not known.
|
|
95
|
+
*/
|
|
96
|
+
resolveToLID(phoneOrJid) {
|
|
97
|
+
const phone = this.extractPhone(phoneOrJid);
|
|
98
|
+
if (!phone)
|
|
99
|
+
return null;
|
|
100
|
+
return this.phoneToLid.get(phone) || null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve LID JID to phone number.
|
|
104
|
+
* Returns null if not known.
|
|
105
|
+
*/
|
|
106
|
+
resolveToPhone(lid) {
|
|
107
|
+
const normalized = this.normalizeLid(lid);
|
|
108
|
+
const mapping = this.lidToPhone.get(normalized);
|
|
109
|
+
return mapping ? mapping.phone : null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get full mapping for a given JID (either LID or phone).
|
|
113
|
+
* Returns null if not known.
|
|
114
|
+
*/
|
|
115
|
+
getMapping(jid) {
|
|
116
|
+
const normalized = this.normalizeLid(jid);
|
|
117
|
+
// Try as LID
|
|
118
|
+
const byLid = this.lidToPhone.get(normalized);
|
|
119
|
+
if (byLid)
|
|
120
|
+
return byLid;
|
|
121
|
+
// Try as phone
|
|
122
|
+
const phone = this.extractPhone(jid);
|
|
123
|
+
if (phone) {
|
|
124
|
+
const lid = this.phoneToLid.get(phone);
|
|
125
|
+
if (lid)
|
|
126
|
+
return this.lidToPhone.get(lid) || null;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get total number of known mappings.
|
|
132
|
+
*/
|
|
133
|
+
size() {
|
|
134
|
+
return this.lidToPhone.size;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clear all mappings.
|
|
138
|
+
*/
|
|
139
|
+
clear() {
|
|
140
|
+
this.lidToPhone.clear();
|
|
141
|
+
this.phoneToLid.clear();
|
|
142
|
+
}
|
|
143
|
+
// Private helpers
|
|
144
|
+
learnJid(_jid, _source) {
|
|
145
|
+
// Check if this is a LID JID with a phone equivalent we can learn
|
|
146
|
+
// For now, we can only learn from auth dir or from paired data
|
|
147
|
+
// Single JID without context can't create a mapping
|
|
148
|
+
// This is intentionally limited — real learning happens in loadFromAuthDir
|
|
149
|
+
}
|
|
150
|
+
extractPhone(jid) {
|
|
151
|
+
if (!jid)
|
|
152
|
+
return null;
|
|
153
|
+
// Remove @s.whatsapp.net suffix if present
|
|
154
|
+
let cleaned = jid.replace('@s.whatsapp.net', '');
|
|
155
|
+
// Remove device suffix :N if present
|
|
156
|
+
cleaned = cleaned.replace(/:\d+$/, '');
|
|
157
|
+
// Check if it's a phone number (digits only)
|
|
158
|
+
if (/^\d+$/.test(cleaned)) {
|
|
159
|
+
return cleaned;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
normalizeLid(lid) {
|
|
164
|
+
// Remove device suffix :N if present
|
|
165
|
+
return lid.replace(/:\d+@/, '@');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Factory function for creating a singleton resolver instance.
|
|
170
|
+
* Useful for shared state across modules.
|
|
171
|
+
*/
|
|
172
|
+
export function createLidFirstResolver() {
|
|
173
|
+
return new LidFirstResolver();
|
|
174
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypedMessageRetryReason — Typed enum for WhatsApp's retry reason codes
|
|
3
|
+
*
|
|
4
|
+
* Based on protocol research from whatsapp-rust and Baileys source.
|
|
5
|
+
* These codes appear in message retry events when encryption fails.
|
|
6
|
+
*
|
|
7
|
+
* Common scenarios:
|
|
8
|
+
* - SignalErrorBadMac (7) — Most common, indicates encryption session mismatch
|
|
9
|
+
* - SignalErrorNoSession (5) — Peer hasn't established session yet
|
|
10
|
+
* - SignalErrorInvalidKeyId (3) — Peer's prekey rotated
|
|
11
|
+
* - MessageExpired (8) — Message too old to decrypt
|
|
12
|
+
*
|
|
13
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* WhatsApp message retry reason codes.
|
|
18
|
+
* Based on Signal protocol error codes + WhatsApp extensions.
|
|
19
|
+
*/
|
|
20
|
+
export declare enum MessageRetryReason {
|
|
21
|
+
UnknownError = 0,
|
|
22
|
+
GenericError = 1,
|
|
23
|
+
SignalErrorInvalidKeyId = 3,
|
|
24
|
+
SignalErrorInvalidMessage = 4,
|
|
25
|
+
SignalErrorNoSession = 5,
|
|
26
|
+
SignalErrorBadMac = 7,
|
|
27
|
+
MessageExpired = 8,
|
|
28
|
+
DecryptionError = 9
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set of retry reasons that indicate MAC verification failure.
|
|
32
|
+
* These are the most common causes of "Bad MAC" errors in Baileys.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAC_ERROR_CODES: Set<MessageRetryReason>;
|
|
35
|
+
/**
|
|
36
|
+
* Parse a retry reason code from various input formats.
|
|
37
|
+
* Returns UnknownError if code is not recognized.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseRetryReason(code: string | number | undefined): MessageRetryReason;
|
|
40
|
+
/**
|
|
41
|
+
* Check if a retry reason indicates a MAC error.
|
|
42
|
+
* MAC errors are typically caused by encryption session mismatches,
|
|
43
|
+
* often due to LID/PN race conditions.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isMacError(reason: MessageRetryReason): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Get a human-readable description of a retry reason.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getRetryReasonDescription(reason: MessageRetryReason): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypedMessageRetryReason — Typed enum for WhatsApp's retry reason codes
|
|
3
|
+
*
|
|
4
|
+
* Based on protocol research from whatsapp-rust and Baileys source.
|
|
5
|
+
* These codes appear in message retry events when encryption fails.
|
|
6
|
+
*
|
|
7
|
+
* Common scenarios:
|
|
8
|
+
* - SignalErrorBadMac (7) — Most common, indicates encryption session mismatch
|
|
9
|
+
* - SignalErrorNoSession (5) — Peer hasn't established session yet
|
|
10
|
+
* - SignalErrorInvalidKeyId (3) — Peer's prekey rotated
|
|
11
|
+
* - MessageExpired (8) — Message too old to decrypt
|
|
12
|
+
*
|
|
13
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* WhatsApp message retry reason codes.
|
|
18
|
+
* Based on Signal protocol error codes + WhatsApp extensions.
|
|
19
|
+
*/
|
|
20
|
+
export var MessageRetryReason;
|
|
21
|
+
(function (MessageRetryReason) {
|
|
22
|
+
MessageRetryReason[MessageRetryReason["UnknownError"] = 0] = "UnknownError";
|
|
23
|
+
MessageRetryReason[MessageRetryReason["GenericError"] = 1] = "GenericError";
|
|
24
|
+
MessageRetryReason[MessageRetryReason["SignalErrorInvalidKeyId"] = 3] = "SignalErrorInvalidKeyId";
|
|
25
|
+
MessageRetryReason[MessageRetryReason["SignalErrorInvalidMessage"] = 4] = "SignalErrorInvalidMessage";
|
|
26
|
+
MessageRetryReason[MessageRetryReason["SignalErrorNoSession"] = 5] = "SignalErrorNoSession";
|
|
27
|
+
MessageRetryReason[MessageRetryReason["SignalErrorBadMac"] = 7] = "SignalErrorBadMac";
|
|
28
|
+
MessageRetryReason[MessageRetryReason["MessageExpired"] = 8] = "MessageExpired";
|
|
29
|
+
MessageRetryReason[MessageRetryReason["DecryptionError"] = 9] = "DecryptionError";
|
|
30
|
+
})(MessageRetryReason || (MessageRetryReason = {}));
|
|
31
|
+
/**
|
|
32
|
+
* Set of retry reasons that indicate MAC verification failure.
|
|
33
|
+
* These are the most common causes of "Bad MAC" errors in Baileys.
|
|
34
|
+
*/
|
|
35
|
+
export const MAC_ERROR_CODES = new Set([
|
|
36
|
+
MessageRetryReason.SignalErrorBadMac,
|
|
37
|
+
MessageRetryReason.SignalErrorInvalidMessage,
|
|
38
|
+
MessageRetryReason.SignalErrorNoSession,
|
|
39
|
+
MessageRetryReason.SignalErrorInvalidKeyId,
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Parse a retry reason code from various input formats.
|
|
43
|
+
* Returns UnknownError if code is not recognized.
|
|
44
|
+
*/
|
|
45
|
+
export function parseRetryReason(code) {
|
|
46
|
+
if (code === undefined || code === null) {
|
|
47
|
+
return MessageRetryReason.UnknownError;
|
|
48
|
+
}
|
|
49
|
+
const n = typeof code === 'string' ? parseInt(code, 10) : code;
|
|
50
|
+
if (isNaN(n)) {
|
|
51
|
+
return MessageRetryReason.UnknownError;
|
|
52
|
+
}
|
|
53
|
+
// Check if the number is a valid enum value
|
|
54
|
+
if (Object.values(MessageRetryReason).includes(n)) {
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
return MessageRetryReason.UnknownError;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a retry reason indicates a MAC error.
|
|
61
|
+
* MAC errors are typically caused by encryption session mismatches,
|
|
62
|
+
* often due to LID/PN race conditions.
|
|
63
|
+
*/
|
|
64
|
+
export function isMacError(reason) {
|
|
65
|
+
return MAC_ERROR_CODES.has(reason);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a human-readable description of a retry reason.
|
|
69
|
+
*/
|
|
70
|
+
export function getRetryReasonDescription(reason) {
|
|
71
|
+
switch (reason) {
|
|
72
|
+
case MessageRetryReason.UnknownError:
|
|
73
|
+
return 'Unknown error';
|
|
74
|
+
case MessageRetryReason.GenericError:
|
|
75
|
+
return 'Generic error';
|
|
76
|
+
case MessageRetryReason.SignalErrorInvalidKeyId:
|
|
77
|
+
return 'Invalid key ID — peer prekey rotated';
|
|
78
|
+
case MessageRetryReason.SignalErrorInvalidMessage:
|
|
79
|
+
return 'Invalid message format';
|
|
80
|
+
case MessageRetryReason.SignalErrorNoSession:
|
|
81
|
+
return 'No session — peer not initialized';
|
|
82
|
+
case MessageRetryReason.SignalErrorBadMac:
|
|
83
|
+
return 'Bad MAC — encryption session mismatch';
|
|
84
|
+
case MessageRetryReason.MessageExpired:
|
|
85
|
+
return 'Message expired — too old to decrypt';
|
|
86
|
+
case MessageRetryReason.DecryptionError:
|
|
87
|
+
return 'Decryption failed';
|
|
88
|
+
default:
|
|
89
|
+
return `Unknown reason code ${reason}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Stability Module — Middleware layer for Baileys socket stability
|
|
3
|
+
*
|
|
4
|
+
* Wraps Baileys socket to provide:
|
|
5
|
+
* 1. Canonical JID normalization before sendMessage (reduces mutex race triggers)
|
|
6
|
+
* 2. Typed disconnect reason classification with recovery recommendations
|
|
7
|
+
* 3. Session health monitoring (Bad MAC detection and degradation alerts)
|
|
8
|
+
*
|
|
9
|
+
* This is a pure middleware layer — cannot modify Baileys internals, but can wrap
|
|
10
|
+
* the socket interface to provide stability improvements.
|
|
11
|
+
*
|
|
12
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
import { LidResolver } from './lidResolver.js';
|
|
16
|
+
export type DisconnectCategory = 'fatal' | 'recoverable' | 'rate-limited' | 'unknown';
|
|
17
|
+
export interface DisconnectClassification {
|
|
18
|
+
category: DisconnectCategory;
|
|
19
|
+
shouldReconnect: boolean;
|
|
20
|
+
backoffMs?: number;
|
|
21
|
+
message: string;
|
|
22
|
+
code: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Classify Baileys DisconnectReason codes into typed categories.
|
|
26
|
+
* Based on PR #2367 and observed behavior from production bots.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifyDisconnect(statusCode: number): DisconnectClassification;
|
|
29
|
+
export interface SessionHealthStats {
|
|
30
|
+
decryptSuccess: number;
|
|
31
|
+
decryptFail: number;
|
|
32
|
+
badMacCount: number;
|
|
33
|
+
lastBadMac?: Date;
|
|
34
|
+
isDegraded: boolean;
|
|
35
|
+
degradedSince?: Date;
|
|
36
|
+
}
|
|
37
|
+
export interface SessionHealthConfig {
|
|
38
|
+
/** Threshold for Bad MAC errors in window before declaring degraded (default: 3) */
|
|
39
|
+
badMacThreshold?: number;
|
|
40
|
+
/** Time window for Bad MAC threshold in ms (default: 60000 = 1 minute) */
|
|
41
|
+
badMacWindowMs?: number;
|
|
42
|
+
/** Callback when session enters degraded state */
|
|
43
|
+
onDegraded?: (stats: SessionHealthStats) => void;
|
|
44
|
+
/** Callback when session recovers from degraded state */
|
|
45
|
+
onRecovered?: (stats: SessionHealthStats) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Track session health via decrypt success/failure ratio.
|
|
49
|
+
* Emits 'session:degraded' event when Bad MAC rate exceeds threshold.
|
|
50
|
+
*/
|
|
51
|
+
export declare class SessionHealthMonitor {
|
|
52
|
+
private config;
|
|
53
|
+
private onDegraded?;
|
|
54
|
+
private onRecovered?;
|
|
55
|
+
private stats;
|
|
56
|
+
private badMacTimestamps;
|
|
57
|
+
constructor(config?: SessionHealthConfig);
|
|
58
|
+
/**
|
|
59
|
+
* Record successful decrypt
|
|
60
|
+
*/
|
|
61
|
+
recordDecryptSuccess(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Record failed decrypt (Bad MAC or similar)
|
|
64
|
+
*/
|
|
65
|
+
recordDecryptFail(isBadMac?: boolean): void;
|
|
66
|
+
/**
|
|
67
|
+
* Check if session has recovered from degraded state
|
|
68
|
+
*/
|
|
69
|
+
private checkRecovery;
|
|
70
|
+
/**
|
|
71
|
+
* Get current health stats
|
|
72
|
+
*/
|
|
73
|
+
getStats(): SessionHealthStats;
|
|
74
|
+
/**
|
|
75
|
+
* Reset all counters
|
|
76
|
+
*/
|
|
77
|
+
reset(): void;
|
|
78
|
+
}
|
|
79
|
+
export interface SessionStabilityConfig {
|
|
80
|
+
/** Enable canonical JID normalization before sendMessage (default: true) */
|
|
81
|
+
canonicalJidNormalization?: boolean;
|
|
82
|
+
/** Enable session health monitoring (default: true) */
|
|
83
|
+
healthMonitoring?: boolean;
|
|
84
|
+
/** Session health config (only used if healthMonitoring enabled) */
|
|
85
|
+
health?: SessionHealthConfig;
|
|
86
|
+
/** LID resolver instance (required for canonicalJidNormalization) */
|
|
87
|
+
lidResolver?: LidResolver;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wrap a Baileys socket with session stability features.
|
|
91
|
+
* Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
|
|
92
|
+
*/
|
|
93
|
+
export declare function wrapWithSessionStability<T extends Record<string, any>>(sock: T, config?: SessionStabilityConfig): T;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Stability Module — Middleware layer for Baileys socket stability
|
|
3
|
+
*
|
|
4
|
+
* Wraps Baileys socket to provide:
|
|
5
|
+
* 1. Canonical JID normalization before sendMessage (reduces mutex race triggers)
|
|
6
|
+
* 2. Typed disconnect reason classification with recovery recommendations
|
|
7
|
+
* 3. Session health monitoring (Bad MAC detection and degradation alerts)
|
|
8
|
+
*
|
|
9
|
+
* This is a pure middleware layer — cannot modify Baileys internals, but can wrap
|
|
10
|
+
* the socket interface to provide stability improvements.
|
|
11
|
+
*
|
|
12
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Classify Baileys DisconnectReason codes into typed categories.
|
|
17
|
+
* Based on PR #2367 and observed behavior from production bots.
|
|
18
|
+
*/
|
|
19
|
+
export function classifyDisconnect(statusCode) {
|
|
20
|
+
// Fatal errors — logged out or banned, need QR restart
|
|
21
|
+
if (statusCode === 401 || statusCode === 440) {
|
|
22
|
+
return {
|
|
23
|
+
category: 'fatal',
|
|
24
|
+
shouldReconnect: false,
|
|
25
|
+
message: 'Logged out — restart with QR code required',
|
|
26
|
+
code: statusCode,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (statusCode === 515) {
|
|
30
|
+
return {
|
|
31
|
+
category: 'fatal',
|
|
32
|
+
shouldReconnect: false,
|
|
33
|
+
message: 'Restart required by WhatsApp — client too old or protocol mismatch',
|
|
34
|
+
code: statusCode,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Method not allowed — server rejecting connection method
|
|
38
|
+
if (statusCode === 405) {
|
|
39
|
+
return {
|
|
40
|
+
category: 'fatal',
|
|
41
|
+
shouldReconnect: false,
|
|
42
|
+
message: 'Method not allowed — server rejected connection method',
|
|
43
|
+
code: statusCode,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Conflict / Connection replaced — user logged in elsewhere or multi-device conflict
|
|
47
|
+
if (statusCode === 409 || statusCode === 428) {
|
|
48
|
+
return {
|
|
49
|
+
category: 'fatal',
|
|
50
|
+
shouldReconnect: false,
|
|
51
|
+
message: 'Connection replaced — another device took over',
|
|
52
|
+
code: statusCode,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Precondition failed — auth state mismatch, retry after delay
|
|
56
|
+
if (statusCode === 412) {
|
|
57
|
+
return {
|
|
58
|
+
category: 'recoverable',
|
|
59
|
+
shouldReconnect: true,
|
|
60
|
+
backoffMs: 30_000, // 30 seconds
|
|
61
|
+
message: 'Precondition failed — auth state mismatch, retry after delay',
|
|
62
|
+
code: statusCode,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Rate limited — back off before reconnecting
|
|
66
|
+
if (statusCode === 429) {
|
|
67
|
+
return {
|
|
68
|
+
category: 'rate-limited',
|
|
69
|
+
shouldReconnect: true,
|
|
70
|
+
backoffMs: 300_000, // 5 minutes
|
|
71
|
+
message: 'Rate limited by WhatsApp — cool-off period required',
|
|
72
|
+
code: statusCode,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (statusCode === 503) {
|
|
76
|
+
return {
|
|
77
|
+
category: 'rate-limited',
|
|
78
|
+
shouldReconnect: true,
|
|
79
|
+
backoffMs: 60_000, // 1 minute
|
|
80
|
+
message: 'WhatsApp service unavailable — temporary outage',
|
|
81
|
+
code: statusCode,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Timeout — transient network issue
|
|
85
|
+
if (statusCode === 408) {
|
|
86
|
+
return {
|
|
87
|
+
category: 'recoverable',
|
|
88
|
+
shouldReconnect: true,
|
|
89
|
+
backoffMs: 5_000, // 5 seconds
|
|
90
|
+
message: 'Connection timeout — network issue, safe to retry',
|
|
91
|
+
code: statusCode,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Internal server error — WhatsApp hiccup
|
|
95
|
+
if (statusCode === 500) {
|
|
96
|
+
return {
|
|
97
|
+
category: 'recoverable',
|
|
98
|
+
shouldReconnect: true,
|
|
99
|
+
backoffMs: 10_000, // 10 seconds
|
|
100
|
+
message: 'WhatsApp internal error — temporary server issue',
|
|
101
|
+
code: statusCode,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Connection closed gracefully
|
|
105
|
+
if (statusCode === 1000) {
|
|
106
|
+
return {
|
|
107
|
+
category: 'recoverable',
|
|
108
|
+
shouldReconnect: true,
|
|
109
|
+
backoffMs: 2_000, // 2 seconds
|
|
110
|
+
message: 'Connection closed gracefully — safe to reconnect',
|
|
111
|
+
code: statusCode,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Unknown code — conservative approach
|
|
115
|
+
return {
|
|
116
|
+
category: 'unknown',
|
|
117
|
+
shouldReconnect: true,
|
|
118
|
+
backoffMs: 15_000, // 15 seconds
|
|
119
|
+
message: `Unknown disconnect reason (code ${statusCode}) — reconnect with caution`,
|
|
120
|
+
code: statusCode,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const DEFAULT_HEALTH_CONFIG = {
|
|
124
|
+
badMacThreshold: 3,
|
|
125
|
+
badMacWindowMs: 60_000,
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Track session health via decrypt success/failure ratio.
|
|
129
|
+
* Emits 'session:degraded' event when Bad MAC rate exceeds threshold.
|
|
130
|
+
*/
|
|
131
|
+
export class SessionHealthMonitor {
|
|
132
|
+
config;
|
|
133
|
+
onDegraded;
|
|
134
|
+
onRecovered;
|
|
135
|
+
stats = {
|
|
136
|
+
decryptSuccess: 0,
|
|
137
|
+
decryptFail: 0,
|
|
138
|
+
badMacCount: 0,
|
|
139
|
+
isDegraded: false,
|
|
140
|
+
};
|
|
141
|
+
badMacTimestamps = [];
|
|
142
|
+
constructor(config = {}) {
|
|
143
|
+
this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
|
|
144
|
+
this.onDegraded = config.onDegraded;
|
|
145
|
+
this.onRecovered = config.onRecovered;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Record successful decrypt
|
|
149
|
+
*/
|
|
150
|
+
recordDecryptSuccess() {
|
|
151
|
+
this.stats.decryptSuccess++;
|
|
152
|
+
this.checkRecovery();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Record failed decrypt (Bad MAC or similar)
|
|
156
|
+
*/
|
|
157
|
+
recordDecryptFail(isBadMac = false) {
|
|
158
|
+
this.stats.decryptFail++;
|
|
159
|
+
if (isBadMac) {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
this.stats.badMacCount++;
|
|
162
|
+
this.stats.lastBadMac = new Date(now);
|
|
163
|
+
this.badMacTimestamps.push(now);
|
|
164
|
+
// Clean up old timestamps outside window
|
|
165
|
+
const cutoff = now - this.config.badMacWindowMs;
|
|
166
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
167
|
+
// Check if we've crossed threshold
|
|
168
|
+
if (!this.stats.isDegraded && this.badMacTimestamps.length >= this.config.badMacThreshold) {
|
|
169
|
+
this.stats.isDegraded = true;
|
|
170
|
+
this.stats.degradedSince = new Date(now);
|
|
171
|
+
this.onDegraded?.(this.getStats());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if session has recovered from degraded state
|
|
177
|
+
*/
|
|
178
|
+
checkRecovery() {
|
|
179
|
+
if (!this.stats.isDegraded)
|
|
180
|
+
return;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const cutoff = now - this.config.badMacWindowMs;
|
|
183
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
184
|
+
// Recovered if Bad MAC count dropped below threshold
|
|
185
|
+
if (this.badMacTimestamps.length < this.config.badMacThreshold) {
|
|
186
|
+
this.stats.isDegraded = false;
|
|
187
|
+
this.stats.degradedSince = undefined;
|
|
188
|
+
this.onRecovered?.(this.getStats());
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get current health stats
|
|
193
|
+
*/
|
|
194
|
+
getStats() {
|
|
195
|
+
return { ...this.stats };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reset all counters
|
|
199
|
+
*/
|
|
200
|
+
reset() {
|
|
201
|
+
this.stats = {
|
|
202
|
+
decryptSuccess: 0,
|
|
203
|
+
decryptFail: 0,
|
|
204
|
+
badMacCount: 0,
|
|
205
|
+
isDegraded: false,
|
|
206
|
+
};
|
|
207
|
+
this.badMacTimestamps = [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Wrap a Baileys socket with session stability features.
|
|
212
|
+
* Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
|
|
213
|
+
*/
|
|
214
|
+
export function wrapWithSessionStability(sock, config = {}) {
|
|
215
|
+
const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver, } = config;
|
|
216
|
+
// Initialize health monitor if enabled
|
|
217
|
+
const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null;
|
|
218
|
+
// Return a Proxy that intercepts method calls
|
|
219
|
+
return new Proxy(sock, {
|
|
220
|
+
get(target, prop) {
|
|
221
|
+
// Intercept sendMessage for JID canonicalization
|
|
222
|
+
if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
|
|
223
|
+
return async (jid, content, options) => {
|
|
224
|
+
// Canonicalize JID using LID resolver
|
|
225
|
+
const canonical = lidResolver.resolveCanonical(jid);
|
|
226
|
+
return target.sendMessage(canonical, content, options);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Expose health monitor stats via a getter
|
|
230
|
+
if (prop === 'sessionHealthStats' && healthMonitor) {
|
|
231
|
+
return healthMonitor.getStats();
|
|
232
|
+
}
|
|
233
|
+
// Expose health monitor instance
|
|
234
|
+
if (prop === 'sessionHealthMonitor' && healthMonitor) {
|
|
235
|
+
return healthMonitor;
|
|
236
|
+
}
|
|
237
|
+
// Pass through everything else
|
|
238
|
+
return target[prop];
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Transport-agnostic anti-ban middleware for Baileys and baileyrs — human-like messaging patterns to protect your WhatsApp number",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|