@prototypevip/baileys 0.0.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/lib/Socket/index.js +12 -135
  2. package/lib/Socket/messages-send.js +27 -44
  3. package/package.json +2 -2
  4. package/lib/AntiBan/antiban.js +0 -922
  5. package/lib/AntiBan/banRecoveryOrchestrator.js +0 -284
  6. package/lib/AntiBan/cli.js +0 -177
  7. package/lib/AntiBan/contactGraph.js +0 -236
  8. package/lib/AntiBan/contentVariator.js +0 -150
  9. package/lib/AntiBan/credsSnapshot.js +0 -121
  10. package/lib/AntiBan/deliveryTracker.js +0 -107
  11. package/lib/AntiBan/deviceFingerprint.js +0 -106
  12. package/lib/AntiBan/fleetEventStore.js +0 -157
  13. package/lib/AntiBan/groupOperationGuard.js +0 -116
  14. package/lib/AntiBan/health.js +0 -207
  15. package/lib/AntiBan/humanEntropy.js +0 -337
  16. package/lib/AntiBan/index.js +0 -77
  17. package/lib/AntiBan/instanceCoordinator.js +0 -175
  18. package/lib/AntiBan/jidCanonicalizer.js +0 -256
  19. package/lib/AntiBan/jidCircuitBreaker.js +0 -186
  20. package/lib/AntiBan/legitimacySignalInjector.js +0 -231
  21. package/lib/AntiBan/lidFirstResolver.js +0 -174
  22. package/lib/AntiBan/lidResolver.js +0 -324
  23. package/lib/AntiBan/messageQueue.js +0 -187
  24. package/lib/AntiBan/messageRecovery.js +0 -283
  25. package/lib/AntiBan/messageTypeRegistry.js +0 -369
  26. package/lib/AntiBan/observability.js +0 -145
  27. package/lib/AntiBan/patch.js +0 -302
  28. package/lib/AntiBan/persist.js +0 -87
  29. package/lib/AntiBan/presenceChoreographer.js +0 -430
  30. package/lib/AntiBan/presets.js +0 -105
  31. package/lib/AntiBan/profiles.js +0 -31
  32. package/lib/AntiBan/proxyRotator.js +0 -306
  33. package/lib/AntiBan/rateLimiter.js +0 -261
  34. package/lib/AntiBan/readReceiptVariance.js +0 -88
  35. package/lib/AntiBan/reconnectThrottle.js +0 -180
  36. package/lib/AntiBan/replyRatio.js +0 -161
  37. package/lib/AntiBan/reputationVoucher.js +0 -373
  38. package/lib/AntiBan/retryReason.js +0 -91
  39. package/lib/AntiBan/retryTracker.js +0 -172
  40. package/lib/AntiBan/scheduler.js +0 -111
  41. package/lib/AntiBan/sessionFingerprint.js +0 -248
  42. package/lib/AntiBan/sessionStability.js +0 -330
  43. package/lib/AntiBan/stateAdapter.js +0 -73
  44. package/lib/AntiBan/stateExport.js +0 -152
  45. package/lib/AntiBan/stealthConnect.js +0 -130
  46. package/lib/AntiBan/timelockGuard.js +0 -181
  47. package/lib/AntiBan/topologyThrottler.js +0 -339
  48. package/lib/AntiBan/warmup.js +0 -140
  49. package/lib/AntiBan/webhooks.js +0 -80
  50. package/lib/AntiBan/wrapper.js +0 -456
@@ -1,111 +0,0 @@
1
- /**
2
- * Smart Scheduler — Send messages during safe hours only
3
- *
4
- * WhatsApp is more suspicious of messages sent at 3 AM.
5
- * This module ensures messages go out during "business hours"
6
- * and adds realistic daily activity patterns.
7
- */
8
- const DEFAULT_CONFIG = {
9
- timezone: 'UTC',
10
- activeHours: [8, 21],
11
- weekendFactor: 0.5,
12
- peakHours: [10, 14],
13
- peakFactor: 1.3,
14
- lunchBreak: [12, 13],
15
- lunchFactor: 0.5,
16
- };
17
- export class Scheduler {
18
- config;
19
- constructor(config = {}) {
20
- this.config = { ...DEFAULT_CONFIG, ...config };
21
- }
22
- /**
23
- * Check if now is within active hours
24
- */
25
- isActiveTime() {
26
- const hour = this.getCurrentHour();
27
- const [start, end] = this.config.activeHours;
28
- return hour >= start && hour < end;
29
- }
30
- /**
31
- * Get the speed multiplier for current time
32
- * > 1 = faster, < 1 = slower, 0 = don't send
33
- */
34
- getSpeedFactor() {
35
- if (!this.isActiveTime())
36
- return 0;
37
- const hour = this.getCurrentHour();
38
- const day = this.getCurrentDay();
39
- let factor = 1.0;
40
- // Weekend reduction
41
- if (day === 0 || day === 6) {
42
- factor *= this.config.weekendFactor;
43
- }
44
- // Peak hours boost
45
- const [peakStart, peakEnd] = this.config.peakHours;
46
- if (hour >= peakStart && hour < peakEnd) {
47
- factor *= this.config.peakFactor;
48
- }
49
- // Lunch slowdown
50
- const [lunchStart, lunchEnd] = this.config.lunchBreak;
51
- if (hour >= lunchStart && hour < lunchEnd) {
52
- factor *= this.config.lunchFactor;
53
- }
54
- return factor;
55
- }
56
- /**
57
- * Get ms until next active window
58
- */
59
- msUntilActive() {
60
- if (this.isActiveTime())
61
- return 0;
62
- const now = new Date();
63
- const hour = now.getHours();
64
- const [start] = this.config.activeHours;
65
- let nextActive;
66
- if (hour >= this.config.activeHours[1]) {
67
- // After hours — next active is tomorrow
68
- nextActive = new Date(now);
69
- nextActive.setDate(nextActive.getDate() + 1);
70
- nextActive.setHours(start, 0, 0, 0);
71
- }
72
- else {
73
- // Before hours — next active is today
74
- nextActive = new Date(now);
75
- nextActive.setHours(start, 0, 0, 0);
76
- }
77
- return nextActive.getTime() - now.getTime();
78
- }
79
- /**
80
- * Adjust a delay based on current time factors
81
- */
82
- adjustDelay(baseDelayMs) {
83
- const factor = this.getSpeedFactor();
84
- if (factor === 0)
85
- return -1; // Don't send
86
- return Math.round(baseDelayMs / factor);
87
- }
88
- /**
89
- * Get current schedule status
90
- */
91
- getStatus() {
92
- const hour = this.getCurrentHour();
93
- const day = this.getCurrentDay();
94
- const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
95
- return {
96
- active: this.isActiveTime(),
97
- currentHour: hour,
98
- day: dayNames[day],
99
- isWeekend: day === 0 || day === 6,
100
- speedFactor: this.getSpeedFactor(),
101
- msUntilActive: this.msUntilActive(),
102
- activeWindow: `${this.config.activeHours[0]}:00 - ${this.config.activeHours[1]}:00`,
103
- };
104
- }
105
- getCurrentHour() {
106
- return new Date().getHours();
107
- }
108
- getCurrentDay() {
109
- return new Date().getDay();
110
- }
111
- }
@@ -1,248 +0,0 @@
1
- /**
2
- * Session Fingerprint Randomization (Obscura-inspired)
3
- *
4
- * Per-session fingerprint randomization to prevent device tracking.
5
- * Scavenged patterns from Obscura headless browser's stealth mode.
6
- *
7
- * Key principles from Obscura:
8
- * 1. Per-session randomization (not per-request)
9
- * 2. Consistent within session (same session = same fingerprint)
10
- * 3. Feature-flag pattern for optional anti-detection
11
- * 4. Emulation of real device profiles (not synthetic values)
12
- *
13
- * Browser fingerprint → WhatsApp signal mapping:
14
- * - TLS fingerprint → WA protocol version
15
- * - Canvas noise → message timing jitter
16
- * - Audio fingerprint → voice note metadata
17
- * - GPU info → device model/brand
18
- * - Battery → connection state variation
19
- * - User agent → WA client version
20
- *
21
- * @author Kobus Wentzel <kobie@pop.co.za>
22
- * @license MIT
23
- */
24
- import { generateFingerprint } from './deviceFingerprint.js';
25
- /**
26
- * Simple deterministic PRNG using mulberry32
27
- * Same as deviceFingerprint.ts for consistency
28
- */
29
- class SeededRandom {
30
- state;
31
- constructor(seed) {
32
- let hash = 0;
33
- for (let i = 0; i < seed.length; i++) {
34
- hash = (hash << 5) - hash + seed.charCodeAt(i);
35
- hash = hash & hash;
36
- }
37
- this.state = Math.abs(hash) || 1;
38
- }
39
- next() {
40
- let t = (this.state += 0x6d2b79f5);
41
- t = Math.imul(t ^ (t >>> 15), t | 1);
42
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
43
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
44
- }
45
- range(min, max) {
46
- return Math.floor(this.next() * (max - min + 1)) + min;
47
- }
48
- rangeFloat(min, max) {
49
- return this.next() * (max - min) + min;
50
- }
51
- pick(array) {
52
- return array[Math.floor(this.next() * array.length)];
53
- }
54
- boolean(probability = 0.5) {
55
- return this.next() < probability;
56
- }
57
- }
58
- // Default configuration values (conservative, realistic)
59
- const DEFAULT_SEND_JITTER_MS = [50, 300];
60
- const DEFAULT_TYPING_JITTER_MS = [30, 150];
61
- const DEFAULT_RETRY_JITTER_MS = [100, 500];
62
- const DEFAULT_DURATION_JITTER_MS = 200;
63
- const DEFAULT_SAMPLE_RATE_POOL = [8000, 16000, 44100, 48000];
64
- const DEFAULT_IDLE_TIMEOUT_JITTER_MS = [25000, 35000];
65
- const DEFAULT_KEEPALIVE_JITTER_MS = [15000, 25000];
66
- const DEFAULT_BATTERY_LEVEL_POOL = [20, 35, 50, 65, 80, 95, 100];
67
- const DEFAULT_PROTOCOL_VERSION_POOL = ['2.24.5', '2.24.4', '2.24.3'];
68
- /**
69
- * Generate a comprehensive session fingerprint.
70
- * Call once per session (socket initialization).
71
- *
72
- * Obscura pattern: consistent per session, randomized across sessions.
73
- */
74
- export function generateSessionFingerprint(config = {}, sessionId) {
75
- const { enabled = true, deviceProfile = {}, networkTiming = {}, voiceNote = {}, connectionState = {}, protocolVersion = {}, seed, } = config;
76
- const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`;
77
- const rng = new SeededRandom(seed || finalSessionId);
78
- // Generate base device fingerprint (delegates to deviceFingerprint.ts)
79
- const device = generateFingerprint({
80
- enabled,
81
- randomizeAppVersion: deviceProfile.randomizeAppVersion ?? true,
82
- randomizeOsVersion: deviceProfile.randomizeOsVersion ?? true,
83
- randomizeDeviceModel: deviceProfile.randomizeDeviceModel ?? true,
84
- seed: seed || finalSessionId,
85
- appVersionPool: deviceProfile.appVersionPool,
86
- osVersionPool: deviceProfile.osVersionPool,
87
- deviceModelPool: deviceProfile.deviceModelPool,
88
- }, finalSessionId);
89
- // Network timing variances (Obscura: prevent timing pattern detection)
90
- const sendJitterRange = networkTiming.sendJitterMs || DEFAULT_SEND_JITTER_MS;
91
- const typingJitterRange = networkTiming.typingJitterMs || DEFAULT_TYPING_JITTER_MS;
92
- const retryJitterRange = networkTiming.retryJitterMs || DEFAULT_RETRY_JITTER_MS;
93
- const networkTimingProfile = enabled
94
- ? {
95
- sendJitterMs: rng.range(sendJitterRange[0], sendJitterRange[1]),
96
- typingJitterMs: rng.range(typingJitterRange[0], typingJitterRange[1]),
97
- retryJitterMs: rng.range(retryJitterRange[0], retryJitterRange[1]),
98
- }
99
- : {
100
- sendJitterMs: 0,
101
- typingJitterMs: 0,
102
- retryJitterMs: 0,
103
- };
104
- // Voice note profile (Obscura: audio fingerprint variance)
105
- const sampleRatePool = voiceNote.sampleRatePool || DEFAULT_SAMPLE_RATE_POOL;
106
- const voiceNoteProfile = {
107
- waveformSeed: enabled ? rng.range(0, 2147483647) : 0,
108
- durationJitterMs: enabled && voiceNote.randomizeWaveform !== false
109
- ? rng.range(0, voiceNote.durationJitterMs || DEFAULT_DURATION_JITTER_MS)
110
- : 0,
111
- sampleRate: enabled ? rng.pick(sampleRatePool) : sampleRatePool[0],
112
- };
113
- // Connection state profile (Obscura: battery/network state variance)
114
- const idleTimeoutRange = connectionState.idleTimeoutJitterMs || DEFAULT_IDLE_TIMEOUT_JITTER_MS;
115
- const keepaliveRange = connectionState.keepaliveJitterMs || DEFAULT_KEEPALIVE_JITTER_MS;
116
- const batteryLevelPool = connectionState.batteryLevelPool || DEFAULT_BATTERY_LEVEL_POOL;
117
- const connectionStateProfile = {
118
- idleTimeoutMs: enabled
119
- ? rng.range(idleTimeoutRange[0], idleTimeoutRange[1])
120
- : 30000,
121
- keepaliveMs: enabled
122
- ? rng.range(keepaliveRange[0], keepaliveRange[1])
123
- : 20000,
124
- batteryLevel: enabled && connectionState.randomizeBattery !== false
125
- ? rng.pick(batteryLevelPool)
126
- : 100,
127
- batteryCharging: enabled ? rng.boolean(0.3) : false, // 30% charging probability
128
- };
129
- // Protocol version (Obscura: TLS fingerprint variance → WA protocol variance)
130
- const versionPool = protocolVersion.versionPool || DEFAULT_PROTOCOL_VERSION_POOL;
131
- const protocolVersionStr = enabled && protocolVersion.randomizeSubVersion !== false
132
- ? rng.pick(versionPool)
133
- : versionPool[0];
134
- return {
135
- device,
136
- networkTiming: networkTimingProfile,
137
- voiceNote: voiceNoteProfile,
138
- connectionState: connectionStateProfile,
139
- protocolVersion: protocolVersionStr,
140
- sessionId: finalSessionId,
141
- createdAt: Date.now(),
142
- };
143
- }
144
- /**
145
- * Apply session fingerprint to Baileys socket config.
146
- *
147
- * Usage:
148
- * const fingerprint = generateSessionFingerprint({ enabled: true });
149
- * const sock = makeWASocket(applySessionFingerprint(config, fingerprint));
150
- */
151
- export function applySessionFingerprint(socketConfig, fingerprint) {
152
- const config = { ...socketConfig };
153
- // Apply device profile (appVersion, browser tuple)
154
- config.version = fingerprint.device.appVersion;
155
- config.browser = [
156
- fingerprint.device.deviceModel,
157
- fingerprint.device.osVersion,
158
- `WhatsApp/${fingerprint.device.appVersion.join('.')}`,
159
- ];
160
- // Apply connection timeouts if fields exist
161
- if ('connectTimeoutMs' in config || config.connectTimeoutMs !== undefined) {
162
- // Add idle timeout jitter to connection config
163
- config.connectTimeoutMs = fingerprint.connectionState.idleTimeoutMs;
164
- }
165
- // Apply keepalive if supported
166
- if ('keepAliveIntervalMs' in config || config.keepAliveIntervalMs !== undefined) {
167
- config.keepAliveIntervalMs = fingerprint.connectionState.keepaliveMs;
168
- }
169
- // Store fingerprint for runtime access (helpers can read this)
170
- config.__sessionFingerprint = fingerprint;
171
- return config;
172
- }
173
- /**
174
- * Get timing jitter for message send (helper for presenceChoreographer/rateLimiter)
175
- *
176
- * Usage in beforeSend():
177
- * const jitter = getMessageSendJitter(fingerprint);
178
- * await sleep(baseDelay + jitter);
179
- */
180
- export function getMessageSendJitter(fingerprint) {
181
- // Return a random value within ±50% of the session's base jitter
182
- // This adds per-message variance while staying within session profile
183
- const base = fingerprint.networkTiming.sendJitterMs;
184
- return Math.floor(base * 0.5 + Math.random() * base * 0.5);
185
- }
186
- /**
187
- * Get typing indicator jitter (helper for presenceChoreographer)
188
- */
189
- export function getTypingJitter(fingerprint) {
190
- const base = fingerprint.networkTiming.typingJitterMs;
191
- return Math.floor(base * 0.5 + Math.random() * base * 0.5);
192
- }
193
- /**
194
- * Get retry backoff jitter (helper for reconnectThrottle)
195
- */
196
- export function getRetryJitter(fingerprint) {
197
- const base = fingerprint.networkTiming.retryJitterMs;
198
- return Math.floor(base * 0.5 + Math.random() * base * 0.5);
199
- }
200
- /**
201
- * Get voice note metadata (helper for voice message encoding)
202
- *
203
- * Returns suggested sample rate and duration adjustment based on session fingerprint.
204
- */
205
- export function getVoiceNoteMetadata(fingerprint) {
206
- return {
207
- sampleRate: fingerprint.voiceNote.sampleRate,
208
- durationJitterMs: fingerprint.voiceNote.durationJitterMs,
209
- waveformSeed: fingerprint.voiceNote.waveformSeed,
210
- };
211
- }
212
- /**
213
- * Get battery state (helper for presence/connection state signals)
214
- */
215
- export function getBatteryState(fingerprint) {
216
- return {
217
- level: fingerprint.connectionState.batteryLevel,
218
- charging: fingerprint.connectionState.batteryCharging,
219
- };
220
- }
221
- /**
222
- * Create a session fingerprint preset (Obscura-inspired feature flag pattern)
223
- */
224
- export function createStealthFingerprint(sessionId) {
225
- return generateSessionFingerprint({
226
- enabled: true,
227
- deviceProfile: {
228
- randomizeAppVersion: true,
229
- randomizeOsVersion: true,
230
- randomizeDeviceModel: true,
231
- },
232
- networkTiming: {
233
- sendJitterMs: [100, 500],
234
- typingJitterMs: [50, 200],
235
- retryJitterMs: [200, 800],
236
- },
237
- voiceNote: {
238
- randomizeWaveform: true,
239
- durationJitterMs: 300,
240
- },
241
- connectionState: {
242
- randomizeBattery: true,
243
- },
244
- protocolVersion: {
245
- randomizeSubVersion: true,
246
- },
247
- }, sessionId);
248
- }
@@ -1,330 +0,0 @@
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
- * Detects "deaf sessions" — WebSocket connections that stay open but stop
212
- * delivering messages.upsert / messages.update events.
213
- *
214
- * Root cause (Baileys issue #2491): messageMutex holding ACKs hostage under
215
- * Redis latency spikes causes WhatsApp's server-side flow control to stop
216
- * delivering messages to that client, while keepAlive pings still succeed.
217
- *
218
- * Usage: call onConnect() / onDisconnect() from connection.update events,
219
- * onMessageActivity() from messages.upsert and messages.update events.
220
- * Pass a sock reference via attach() so auto-reconnect can call sock.end().
221
- */
222
- export class DeafSessionDetector {
223
- timeoutMs;
224
- minUptimeMs;
225
- autoReconnect;
226
- onDeafSessionCb;
227
- lastMessageAt = null;
228
- connectedAt = null;
229
- timer = null;
230
- sockRef = null;
231
- constructor(config = {}) {
232
- this.timeoutMs = config.timeoutMs ?? 5 * 60_000;
233
- this.minUptimeMs = config.minUptimeMs ?? 2 * 60_000;
234
- this.autoReconnect = config.autoReconnect ?? true;
235
- this.onDeafSessionCb = config.onDeafSession;
236
- }
237
- /** Attach a socket so auto-reconnect can call sock.end() */
238
- attach(sock) {
239
- this.sockRef = sock;
240
- }
241
- /** Call when connection.update → connection === 'open' */
242
- onConnect() {
243
- this.connectedAt = Date.now();
244
- this.lastMessageAt = Date.now(); // reset — fresh connect is not silence
245
- this.startTimer();
246
- }
247
- /** Call when connection.update → connection === 'close' */
248
- onDisconnect() {
249
- this.connectedAt = null;
250
- this.stopTimer();
251
- }
252
- /** Call on every messages.upsert and messages.update event */
253
- onMessageActivity() {
254
- this.lastMessageAt = Date.now();
255
- }
256
- /** Release the interval — call when discarding the socket */
257
- destroy() {
258
- this.stopTimer();
259
- this.sockRef = null;
260
- }
261
- startTimer() {
262
- this.stopTimer();
263
- this.timer = setInterval(() => this.check(), 30_000);
264
- }
265
- stopTimer() {
266
- if (this.timer !== null) {
267
- clearInterval(this.timer);
268
- this.timer = null;
269
- }
270
- }
271
- check() {
272
- if (this.connectedAt === null)
273
- return;
274
- const now = Date.now();
275
- const connectedSinceMs = now - this.connectedAt;
276
- if (connectedSinceMs < this.minUptimeMs)
277
- return;
278
- const silenceDurationMs = now - (this.lastMessageAt ?? this.connectedAt);
279
- if (silenceDurationMs < this.timeoutMs)
280
- return;
281
- const info = {
282
- lastMessageAt: this.lastMessageAt !== null ? new Date(this.lastMessageAt) : null,
283
- silenceDurationMs,
284
- connectedSinceMs,
285
- };
286
- this.onDeafSessionCb?.(info);
287
- if (this.autoReconnect && this.sockRef) {
288
- try {
289
- this.sockRef.end(new Error('deaf-session'));
290
- }
291
- catch {
292
- // socket may already be closing
293
- }
294
- }
295
- // Stop checking after triggering — let onConnect reset on next reconnect
296
- this.stopTimer();
297
- }
298
- }
299
- /**
300
- * Wrap a Baileys socket with session stability features.
301
- * Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
302
- */
303
- export function wrapWithSessionStability(sock, config = {}) {
304
- const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver, } = config;
305
- // Initialize health monitor if enabled
306
- const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null;
307
- // Return a Proxy that intercepts method calls
308
- return new Proxy(sock, {
309
- get(target, prop) {
310
- // Intercept sendMessage for JID canonicalization
311
- if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
312
- return async (jid, content, options) => {
313
- // Canonicalize JID using LID resolver
314
- const canonical = lidResolver.resolveCanonical(jid);
315
- return target.sendMessage(canonical, content, options);
316
- };
317
- }
318
- // Expose health monitor stats via a getter
319
- if (prop === 'sessionHealthStats' && healthMonitor) {
320
- return healthMonitor.getStats();
321
- }
322
- // Expose health monitor instance
323
- if (prop === 'sessionHealthMonitor' && healthMonitor) {
324
- return healthMonitor;
325
- }
326
- // Pass through everything else
327
- return target[prop];
328
- },
329
- });
330
- }