baileys-antiban 3.8.3 → 3.8.5
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 +18 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/observability.d.ts +85 -0
- package/dist/observability.js +145 -0
- package/dist/sessionStability.d.ts +71 -0
- package/dist/sessionStability.js +89 -0
- package/dist/wrapper.d.ts +7 -0
- package/dist/wrapper.js +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.5] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Observability module** (`src/observability.ts`) — Prometheus metrics export and pluggable structured logging.
|
|
12
|
+
- `exportPrometheusMetrics(stats, labels?)` — exports 27 metrics (3 counters, 24 gauges) in Prometheus text exposition format v0.0.4. Covers health score/risk, warmup progress, rate limiter windows, known chats, reply ratio, contact graph, retry spirals, reconnect throttle. Accepts custom labels (`instance`, `region`, etc.).
|
|
13
|
+
- `createMetricsHandler(getStats, labels?)` — returns Express/Fastify-compatible `handle(req, res)` + `text()` helpers for a `/metrics` endpoint.
|
|
14
|
+
- `createPeriodicExporter(getStats, config)` — push-based exporter that calls `onMetrics(text)` on a configurable interval (default 30s). Returns `stop()` handle.
|
|
15
|
+
- `createConsoleLogger(prefix?)` — structured console logger compatible with winston/pino interface (`debug`, `info`, `warn`, `error` with ISO timestamps and JSON meta).
|
|
16
|
+
- `AntiBanLogger` interface — plug in any logger: `winston`, `pino`, or the built-in console logger.
|
|
17
|
+
- New exports: `createConsoleLogger`, `exportPrometheusMetrics`, `createMetricsHandler`, `createPeriodicExporter`, `AntiBanLogger`, `PeriodicExporterConfig`, `PeriodicExporterHandle`.
|
|
18
|
+
|
|
19
|
+
## [3.8.4] - 2026-05-09
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`DeafSessionDetector`** — detects WebSocket sessions that stay open but stop delivering `messages.upsert` / `messages.update` events (Baileys issue #2491). Root cause: `messageMutex` holding ACKs under Redis latency spikes triggers WhatsApp server-side flow control, silently stopping message delivery while keepAlive pings still succeed. The detector runs a 30-second interval, fires `onDeafSession` callback with silence duration info, and optionally calls `sock.end(new Error('deaf-session'))` for auto-reconnect. Configurable `timeoutMs` (default 5 min), `minUptimeMs` warmup guard (default 2 min), `autoReconnect` flag.
|
|
23
|
+
- **`wrapSocket` `deafSession` option** — pass `deafSession: DeafSessionConfig` to `wrapOptions` to enable automatic deaf-session detection on the wrapped socket. Activity signals are wired to all `messages.upsert` and `messages.update` events; cleanup is tied to `connection.update` close events.
|
|
24
|
+
- New exports: `DeafSessionDetector`, `DeafSessionConfig`, `DeafSessionInfo`.
|
|
25
|
+
|
|
8
26
|
## [3.8.3] - 2026-05-09
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ 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';
|
|
22
|
+
export { SessionHealthMonitor, type SessionHealthStats, type SessionHealthConfig, wrapWithSessionStability, type SessionStabilityConfig, classifyDisconnect, type DisconnectClassification, type DisconnectCategory, DeafSessionDetector, type DeafSessionConfig, type DeafSessionInfo, } from './sessionStability.js';
|
|
23
23
|
export { LidFirstResolver, createLidFirstResolver, type LidPhoneMapping, } from './lidFirstResolver.js';
|
|
24
24
|
export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
|
|
25
25
|
export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
|
|
@@ -38,3 +38,4 @@ export { readReceiptVariance, type ReadReceiptVariance, type ReadReceiptVariance
|
|
|
38
38
|
export { proxyRotator, type ProxyEndpoint, type ProxyRotatorConfig, type ProxyRotatorStats, type ProxyRotatorHandle, } from './proxyRotator.js';
|
|
39
39
|
export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, type SessionFingerprint, type SessionFingerprintConfig, } from './sessionFingerprint.js';
|
|
40
40
|
export { getStealthSocketConfig, rampPresenceAfterConnect, STEALTH_BROWSER_POOL, AbortError, type BrowserTuple, type StealthSocketConfig, type GetStealthSocketConfigOptions, type RampPresenceOptions, type PresenceCapableSocket, } from './stealthConnect.js';
|
|
41
|
+
export { createConsoleLogger, exportPrometheusMetrics, createMetricsHandler, createPeriodicExporter, type AntiBanLogger, type PeriodicExporterConfig, type PeriodicExporterHandle, } from './observability.js';
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
|
24
24
|
export { LidResolver } from './lidResolver.js';
|
|
25
25
|
export { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
26
26
|
// v2.0 new modules
|
|
27
|
-
export { SessionHealthMonitor, wrapWithSessionStability, classifyDisconnect, } from './sessionStability.js';
|
|
27
|
+
export { SessionHealthMonitor, wrapWithSessionStability, classifyDisconnect, DeafSessionDetector, } from './sessionStability.js';
|
|
28
28
|
// v2.1 new modules
|
|
29
29
|
export { LidFirstResolver, createLidFirstResolver, } from './lidFirstResolver.js';
|
|
30
30
|
export { MessageRetryReason, MAC_ERROR_CODES, parseRetryReason, isMacError, getRetryReasonDescription, } from './retryReason.js';
|
|
@@ -53,3 +53,5 @@ export { proxyRotator, } from './proxyRotator.js';
|
|
|
53
53
|
export { generateSessionFingerprint, applySessionFingerprint, getMessageSendJitter, getTypingJitter, getRetryJitter, getVoiceNoteMetadata, getBatteryState, createStealthFingerprint, } from './sessionFingerprint.js';
|
|
54
54
|
// v3.8 new modules
|
|
55
55
|
export { getStealthSocketConfig, rampPresenceAfterConnect, STEALTH_BROWSER_POOL, AbortError, } from './stealthConnect.js';
|
|
56
|
+
// Observability
|
|
57
|
+
export { createConsoleLogger, exportPrometheusMetrics, createMetricsHandler, createPeriodicExporter, } from './observability.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability — Prometheus metrics export + pluggable structured logging
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* // Logging
|
|
6
|
+
* import { createConsoleLogger } from 'baileys-antiban';
|
|
7
|
+
* const logger = createConsoleLogger('[my-bot]');
|
|
8
|
+
* logger.info('Message sent', { recipient: jid });
|
|
9
|
+
*
|
|
10
|
+
* // Metrics export for Express
|
|
11
|
+
* import { createMetricsHandler } from 'baileys-antiban';
|
|
12
|
+
* const metricsHandler = createMetricsHandler(() => antiban.getStats());
|
|
13
|
+
* app.get('/metrics', metricsHandler.handle);
|
|
14
|
+
*
|
|
15
|
+
* // Periodic push to external system
|
|
16
|
+
* import { createPeriodicExporter } from 'baileys-antiban';
|
|
17
|
+
* const exporter = createPeriodicExporter(() => antiban.getStats(), {
|
|
18
|
+
* intervalMs: 30_000,
|
|
19
|
+
* onMetrics: (text) => pushToVictoriaMetrics(text),
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
import type { AntiBanStats } from './antiban.js';
|
|
23
|
+
/**
|
|
24
|
+
* Pluggable logger interface compatible with winston, pino, console
|
|
25
|
+
*/
|
|
26
|
+
export interface AntiBanLogger {
|
|
27
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
28
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
29
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
30
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a simple console logger with timestamps and prefix
|
|
34
|
+
*/
|
|
35
|
+
export declare function createConsoleLogger(prefix?: string): AntiBanLogger;
|
|
36
|
+
/**
|
|
37
|
+
* Export AntiBan stats as Prometheus text format (exposition format v0.0.4)
|
|
38
|
+
*/
|
|
39
|
+
export declare function exportPrometheusMetrics(stats: AntiBanStats, labels?: Record<string, string>): string;
|
|
40
|
+
/**
|
|
41
|
+
* Create an HTTP handler for Prometheus metrics (Express/Fastify compatible)
|
|
42
|
+
*/
|
|
43
|
+
export declare function createMetricsHandler(getStats: () => AntiBanStats, labels?: Record<string, string>): {
|
|
44
|
+
/**
|
|
45
|
+
* HTTP request handler (Express/Fastify compatible)
|
|
46
|
+
*/
|
|
47
|
+
handle: (_req: unknown, res: {
|
|
48
|
+
setHeader: (name: string, value: string) => void;
|
|
49
|
+
end: (data: string) => void;
|
|
50
|
+
}) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Get metrics as plain text (for non-HTTP usage)
|
|
53
|
+
*/
|
|
54
|
+
text: () => string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Configuration for periodic metrics export
|
|
58
|
+
*/
|
|
59
|
+
export interface PeriodicExporterConfig {
|
|
60
|
+
/**
|
|
61
|
+
* Export interval in milliseconds (default: 30000)
|
|
62
|
+
*/
|
|
63
|
+
intervalMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Callback invoked with Prometheus text on each interval
|
|
66
|
+
*/
|
|
67
|
+
onMetrics: (text: string) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Additional labels to attach to all metrics
|
|
70
|
+
*/
|
|
71
|
+
labels?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Handle for controlling periodic export
|
|
75
|
+
*/
|
|
76
|
+
export interface PeriodicExporterHandle {
|
|
77
|
+
/**
|
|
78
|
+
* Stop the periodic exporter
|
|
79
|
+
*/
|
|
80
|
+
stop(): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a periodic metrics exporter that calls onMetrics on an interval
|
|
84
|
+
*/
|
|
85
|
+
export declare function createPeriodicExporter(getStats: () => AntiBanStats, config: PeriodicExporterConfig): PeriodicExporterHandle;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability — Prometheus metrics export + pluggable structured logging
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* // Logging
|
|
6
|
+
* import { createConsoleLogger } from 'baileys-antiban';
|
|
7
|
+
* const logger = createConsoleLogger('[my-bot]');
|
|
8
|
+
* logger.info('Message sent', { recipient: jid });
|
|
9
|
+
*
|
|
10
|
+
* // Metrics export for Express
|
|
11
|
+
* import { createMetricsHandler } from 'baileys-antiban';
|
|
12
|
+
* const metricsHandler = createMetricsHandler(() => antiban.getStats());
|
|
13
|
+
* app.get('/metrics', metricsHandler.handle);
|
|
14
|
+
*
|
|
15
|
+
* // Periodic push to external system
|
|
16
|
+
* import { createPeriodicExporter } from 'baileys-antiban';
|
|
17
|
+
* const exporter = createPeriodicExporter(() => antiban.getStats(), {
|
|
18
|
+
* intervalMs: 30_000,
|
|
19
|
+
* onMetrics: (text) => pushToVictoriaMetrics(text),
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Create a simple console logger with timestamps and prefix
|
|
24
|
+
*/
|
|
25
|
+
export function createConsoleLogger(prefix = '[baileys-antiban]') {
|
|
26
|
+
const format = (level, msg, meta) => {
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const metaStr = meta ? ' ' + JSON.stringify(meta) : '';
|
|
29
|
+
return `${timestamp} ${level} ${prefix} ${msg}${metaStr}`;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
debug: (msg, meta) => console.log(format('DEBUG', msg, meta)),
|
|
33
|
+
info: (msg, meta) => console.log(format('INFO', msg, meta)),
|
|
34
|
+
warn: (msg, meta) => console.warn(format('WARN', msg, meta)),
|
|
35
|
+
error: (msg, meta) => console.error(format('ERROR', msg, meta)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Export AntiBan stats as Prometheus text format (exposition format v0.0.4)
|
|
40
|
+
*/
|
|
41
|
+
export function exportPrometheusMetrics(stats, labels) {
|
|
42
|
+
const labelMap = { instance: 'default', ...labels };
|
|
43
|
+
const labelStr = Object.entries(labelMap)
|
|
44
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
45
|
+
.join(',');
|
|
46
|
+
const lines = [];
|
|
47
|
+
const addMetric = (name, type, help, value) => {
|
|
48
|
+
lines.push(`# HELP ${name} ${help}`);
|
|
49
|
+
lines.push(`# TYPE ${name} ${type}`);
|
|
50
|
+
lines.push(`${name}{${labelStr}} ${value}`);
|
|
51
|
+
};
|
|
52
|
+
// Counters
|
|
53
|
+
addMetric('antiban_messages_allowed_total', 'counter', 'Total messages allowed by AntiBan', stats.messagesAllowed);
|
|
54
|
+
addMetric('antiban_messages_blocked_total', 'counter', 'Total messages blocked by AntiBan', stats.messagesBlocked);
|
|
55
|
+
addMetric('antiban_total_delay_ms_total', 'counter', 'Total delay imposed (milliseconds)', stats.totalDelayMs);
|
|
56
|
+
// Health gauges
|
|
57
|
+
const riskMap = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
58
|
+
addMetric('antiban_health_score', 'gauge', 'Current health score (0=best, 100=worst)', stats.health.score);
|
|
59
|
+
addMetric('antiban_health_risk', 'gauge', 'Risk level encoded as number (0=low, 1=medium, 2=high, 3=critical)', riskMap[stats.health.risk] ?? 0);
|
|
60
|
+
addMetric('antiban_disconnects_last_hour', 'gauge', 'Disconnects in last hour', stats.health.stats.disconnectsLastHour);
|
|
61
|
+
addMetric('antiban_failed_messages_last_hour', 'gauge', 'Failed messages in last hour', stats.health.stats.failedMessagesLastHour);
|
|
62
|
+
addMetric('antiban_uptime_ms', 'gauge', 'Session uptime in milliseconds', stats.health.stats.uptimeMs);
|
|
63
|
+
// Warm-up gauges
|
|
64
|
+
addMetric('antiban_warmup_progress', 'gauge', 'Warm-up progress (0.0 to 1.0)', stats.warmUp.progress);
|
|
65
|
+
addMetric('antiban_warmup_day', 'gauge', 'Current warm-up day', stats.warmUp.day);
|
|
66
|
+
addMetric('antiban_warmup_today_sent', 'gauge', 'Messages sent today during warm-up', stats.warmUp.todaySent);
|
|
67
|
+
addMetric('antiban_warmup_today_limit', 'gauge', 'Message limit for today during warm-up', stats.warmUp.todayLimit);
|
|
68
|
+
// Rate limiter gauges
|
|
69
|
+
addMetric('antiban_rate_last_minute', 'gauge', 'Messages sent in last minute', stats.rateLimiter.lastMinute);
|
|
70
|
+
addMetric('antiban_rate_last_hour', 'gauge', 'Messages sent in last hour', stats.rateLimiter.lastHour);
|
|
71
|
+
addMetric('antiban_rate_last_day', 'gauge', 'Messages sent in last day', stats.rateLimiter.lastDay);
|
|
72
|
+
addMetric('antiban_rate_limit_per_minute', 'gauge', 'Rate limit per minute', stats.rateLimiter.limits.perMinute);
|
|
73
|
+
addMetric('antiban_rate_limit_per_hour', 'gauge', 'Rate limit per hour', stats.rateLimiter.limits.perHour);
|
|
74
|
+
addMetric('antiban_rate_limit_per_day', 'gauge', 'Rate limit per day', stats.rateLimiter.limits.perDay);
|
|
75
|
+
addMetric('antiban_known_chats', 'gauge', 'Number of known chats', stats.rateLimiter.knownChats);
|
|
76
|
+
// Reply ratio gauges (optional)
|
|
77
|
+
if (stats.replyRatio) {
|
|
78
|
+
addMetric('antiban_contacts_on_cooldown', 'gauge', 'Contacts currently on reply cooldown', stats.replyRatio.contactsOnCooldown);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
addMetric('antiban_contacts_on_cooldown', 'gauge', 'Contacts currently on reply cooldown', 0);
|
|
82
|
+
}
|
|
83
|
+
// Contact graph gauges (optional)
|
|
84
|
+
if (stats.contactGraph) {
|
|
85
|
+
addMetric('antiban_known_contacts', 'gauge', 'Number of known contacts', stats.contactGraph.knownContacts);
|
|
86
|
+
addMetric('antiban_pending_handshakes', 'gauge', 'Pending handshakes', stats.contactGraph.pendingHandshakes);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
addMetric('antiban_known_contacts', 'gauge', 'Number of known contacts', 0);
|
|
90
|
+
addMetric('antiban_pending_handshakes', 'gauge', 'Pending handshakes', 0);
|
|
91
|
+
}
|
|
92
|
+
// Retry tracker gauges (optional)
|
|
93
|
+
if (stats.retryTracker) {
|
|
94
|
+
addMetric('antiban_retry_spirals', 'gauge', 'Number of retry spirals detected', stats.retryTracker.spiralsDetected);
|
|
95
|
+
addMetric('antiban_active_retries', 'gauge', 'Currently active retries', stats.retryTracker.activeRetries);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
addMetric('antiban_retry_spirals', 'gauge', 'Number of retry spirals detected', 0);
|
|
99
|
+
addMetric('antiban_active_retries', 'gauge', 'Currently active retries', 0);
|
|
100
|
+
}
|
|
101
|
+
// Reconnect throttle gauges (optional)
|
|
102
|
+
if (stats.reconnectThrottle) {
|
|
103
|
+
addMetric('antiban_reconnect_throttled', 'gauge', 'Whether reconnect is currently throttled (0=no, 1=yes)', stats.reconnectThrottle.isThrottled ? 1 : 0);
|
|
104
|
+
addMetric('antiban_lifetime_reconnects', 'gauge', 'Total reconnects in session lifetime', stats.reconnectThrottle.lifetimeReconnects);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
addMetric('antiban_reconnect_throttled', 'gauge', 'Whether reconnect is currently throttled (0=no, 1=yes)', 0);
|
|
108
|
+
addMetric('antiban_lifetime_reconnects', 'gauge', 'Total reconnects in session lifetime', 0);
|
|
109
|
+
}
|
|
110
|
+
return lines.join('\n') + '\n';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create an HTTP handler for Prometheus metrics (Express/Fastify compatible)
|
|
114
|
+
*/
|
|
115
|
+
export function createMetricsHandler(getStats, labels) {
|
|
116
|
+
return {
|
|
117
|
+
/**
|
|
118
|
+
* HTTP request handler (Express/Fastify compatible)
|
|
119
|
+
*/
|
|
120
|
+
handle: (_req, res) => {
|
|
121
|
+
const metrics = exportPrometheusMetrics(getStats(), labels);
|
|
122
|
+
res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
123
|
+
res.end(metrics);
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Get metrics as plain text (for non-HTTP usage)
|
|
127
|
+
*/
|
|
128
|
+
text: () => {
|
|
129
|
+
return exportPrometheusMetrics(getStats(), labels);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Create a periodic metrics exporter that calls onMetrics on an interval
|
|
135
|
+
*/
|
|
136
|
+
export function createPeriodicExporter(getStats, config) {
|
|
137
|
+
const intervalMs = config.intervalMs ?? 30_000;
|
|
138
|
+
const intervalId = setInterval(() => {
|
|
139
|
+
const metrics = exportPrometheusMetrics(getStats(), config.labels);
|
|
140
|
+
config.onMetrics(metrics);
|
|
141
|
+
}, intervalMs);
|
|
142
|
+
return {
|
|
143
|
+
stop: () => clearInterval(intervalId),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -76,6 +76,77 @@ export declare class SessionHealthMonitor {
|
|
|
76
76
|
*/
|
|
77
77
|
reset(): void;
|
|
78
78
|
}
|
|
79
|
+
export interface DeafSessionConfig {
|
|
80
|
+
/**
|
|
81
|
+
* How long the session must be silent (no messages.upsert or messages.update)
|
|
82
|
+
* while the WS connection is open before it is declared "deaf".
|
|
83
|
+
* Default: 5 minutes.
|
|
84
|
+
*/
|
|
85
|
+
timeoutMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Minimum uptime before the detector starts checking.
|
|
88
|
+
* Avoids false positives immediately after a fresh connect.
|
|
89
|
+
* Default: 2 minutes.
|
|
90
|
+
*/
|
|
91
|
+
minUptimeMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Called when a deaf session is detected, before any auto-reconnect.
|
|
94
|
+
* Use this to log, alert, or run custom recovery logic.
|
|
95
|
+
*/
|
|
96
|
+
onDeafSession?: (info: DeafSessionInfo) => void;
|
|
97
|
+
/**
|
|
98
|
+
* If true, call sock.end(new Error('deaf-session')) automatically.
|
|
99
|
+
* Set false if you want to handle reconnection yourself in onDeafSession.
|
|
100
|
+
* Default: true.
|
|
101
|
+
*/
|
|
102
|
+
autoReconnect?: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface DeafSessionInfo {
|
|
105
|
+
/** Timestamp of last observed message activity, or null if none since connect. */
|
|
106
|
+
lastMessageAt: Date | null;
|
|
107
|
+
/** How long the session has been silent in ms. */
|
|
108
|
+
silenceDurationMs: number;
|
|
109
|
+
/** How long the WS has been open in ms. */
|
|
110
|
+
connectedSinceMs: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Detects "deaf sessions" — WebSocket connections that stay open but stop
|
|
114
|
+
* delivering messages.upsert / messages.update events.
|
|
115
|
+
*
|
|
116
|
+
* Root cause (Baileys issue #2491): messageMutex holding ACKs hostage under
|
|
117
|
+
* Redis latency spikes causes WhatsApp's server-side flow control to stop
|
|
118
|
+
* delivering messages to that client, while keepAlive pings still succeed.
|
|
119
|
+
*
|
|
120
|
+
* Usage: call onConnect() / onDisconnect() from connection.update events,
|
|
121
|
+
* onMessageActivity() from messages.upsert and messages.update events.
|
|
122
|
+
* Pass a sock reference via attach() so auto-reconnect can call sock.end().
|
|
123
|
+
*/
|
|
124
|
+
export declare class DeafSessionDetector {
|
|
125
|
+
private readonly timeoutMs;
|
|
126
|
+
private readonly minUptimeMs;
|
|
127
|
+
private readonly autoReconnect;
|
|
128
|
+
private readonly onDeafSessionCb?;
|
|
129
|
+
private lastMessageAt;
|
|
130
|
+
private connectedAt;
|
|
131
|
+
private timer;
|
|
132
|
+
private sockRef;
|
|
133
|
+
constructor(config?: DeafSessionConfig);
|
|
134
|
+
/** Attach a socket so auto-reconnect can call sock.end() */
|
|
135
|
+
attach(sock: {
|
|
136
|
+
end: (err?: Error) => void;
|
|
137
|
+
}): void;
|
|
138
|
+
/** Call when connection.update → connection === 'open' */
|
|
139
|
+
onConnect(): void;
|
|
140
|
+
/** Call when connection.update → connection === 'close' */
|
|
141
|
+
onDisconnect(): void;
|
|
142
|
+
/** Call on every messages.upsert and messages.update event */
|
|
143
|
+
onMessageActivity(): void;
|
|
144
|
+
/** Release the interval — call when discarding the socket */
|
|
145
|
+
destroy(): void;
|
|
146
|
+
private startTimer;
|
|
147
|
+
private stopTimer;
|
|
148
|
+
private check;
|
|
149
|
+
}
|
|
79
150
|
export interface SessionStabilityConfig {
|
|
80
151
|
/** Enable canonical JID normalization before sendMessage (default: true) */
|
|
81
152
|
canonicalJidNormalization?: boolean;
|
package/dist/sessionStability.js
CHANGED
|
@@ -207,6 +207,95 @@ export class SessionHealthMonitor {
|
|
|
207
207
|
this.badMacTimestamps = [];
|
|
208
208
|
}
|
|
209
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
|
+
}
|
|
210
299
|
/**
|
|
211
300
|
* Wrap a Baileys socket with session stability features.
|
|
212
301
|
* Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
|
package/dist/wrapper.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* Timelock guard will operate in detection-only mode (relies on 463 errors only).
|
|
29
29
|
*/
|
|
30
30
|
import { AntiBan, type AntiBanConfig } from './antiban.js';
|
|
31
|
+
import { type DeafSessionConfig } from './sessionStability.js';
|
|
31
32
|
import type { WarmUpState } from './warmup.js';
|
|
32
33
|
export type WASocket = {
|
|
33
34
|
sendMessage: (jid: string, content: any, options?: any) => Promise<any>;
|
|
@@ -44,6 +45,12 @@ export type WASocket = {
|
|
|
44
45
|
export interface WrapSocketOptions {
|
|
45
46
|
/** Auto-respond to incoming messages when reply ratio suggests it (default: false) */
|
|
46
47
|
autoRespondToIncoming?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Deaf session detection — monitors for WS connections that stop delivering
|
|
50
|
+
* messages while keepAlive pings still succeed (Baileys issue #2491).
|
|
51
|
+
* Pass a config object to enable; omit to disable.
|
|
52
|
+
*/
|
|
53
|
+
deafSession?: DeafSessionConfig;
|
|
47
54
|
}
|
|
48
55
|
export type WrappedSocket<T extends WASocket = WASocket> = T & {
|
|
49
56
|
antiban: AntiBan;
|
package/dist/wrapper.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* Timelock guard will operate in detection-only mode (relies on 463 errors only).
|
|
29
29
|
*/
|
|
30
30
|
import { AntiBan } from './antiban.js';
|
|
31
|
+
import { DeafSessionDetector } from './sessionStability.js';
|
|
31
32
|
/**
|
|
32
33
|
* Wrap a Baileys socket with anti-ban protection.
|
|
33
34
|
* The returned socket has the same API but sendMessage() is protected.
|
|
@@ -38,6 +39,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
38
39
|
autoRespondToIncoming: false,
|
|
39
40
|
...wrapOptions,
|
|
40
41
|
};
|
|
42
|
+
// Deaf session detector — optional, enabled via wrapOptions.deafSession
|
|
43
|
+
const deafDetector = options.deafSession
|
|
44
|
+
? new DeafSessionDetector(options.deafSession)
|
|
45
|
+
: null;
|
|
46
|
+
if (deafDetector)
|
|
47
|
+
deafDetector.attach(sock);
|
|
41
48
|
// Hook into Baileys events for health monitoring
|
|
42
49
|
// Prefer ev.process() (Baileys ≥ late 2022) for batched event handling
|
|
43
50
|
// Fall back to ev.on() for older versions
|
|
@@ -50,9 +57,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
50
57
|
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
|
|
51
58
|
antiban.onDisconnect(reason);
|
|
52
59
|
antiban.destroy(); // Clean up all timers
|
|
60
|
+
deafDetector?.onDisconnect();
|
|
61
|
+
deafDetector?.destroy();
|
|
53
62
|
}
|
|
54
63
|
if (update.connection === 'open') {
|
|
55
64
|
antiban.onReconnect();
|
|
65
|
+
deafDetector?.onConnect();
|
|
56
66
|
}
|
|
57
67
|
// Reachout timelock detection
|
|
58
68
|
if (update.reachoutTimeLock) {
|
|
@@ -66,6 +76,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
66
76
|
// Catch 463 errors from message updates + track retries + learn LID mappings
|
|
67
77
|
if (events['messages.update']) {
|
|
68
78
|
const updates = events['messages.update'];
|
|
79
|
+
deafDetector?.onMessageActivity();
|
|
69
80
|
for (const update of updates) {
|
|
70
81
|
// 463 error detection
|
|
71
82
|
if (update?.update?.messageStubParameters) {
|
|
@@ -85,6 +96,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
85
96
|
// Register known chats from incoming messages + handle reply suggestions + learn LID mappings
|
|
86
97
|
if (events['messages.upsert']) {
|
|
87
98
|
const { messages } = events['messages.upsert'];
|
|
99
|
+
deafDetector?.onMessageActivity();
|
|
88
100
|
// Learn LID mappings FIRST (before any other processing)
|
|
89
101
|
antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert']);
|
|
90
102
|
for (const msg of messages || []) {
|
|
@@ -129,9 +141,12 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
129
141
|
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
|
|
130
142
|
antiban.onDisconnect(reason);
|
|
131
143
|
antiban.destroy(); // Clean up all timers
|
|
144
|
+
deafDetector?.onDisconnect();
|
|
145
|
+
deafDetector?.destroy();
|
|
132
146
|
}
|
|
133
147
|
if (update.connection === 'open') {
|
|
134
148
|
antiban.onReconnect();
|
|
149
|
+
deafDetector?.onConnect();
|
|
135
150
|
}
|
|
136
151
|
// Reachout timelock detection
|
|
137
152
|
if (update.reachoutTimeLock) {
|
|
@@ -144,6 +159,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
144
159
|
});
|
|
145
160
|
// Catch 463 errors from message updates + track retries + learn LID mappings
|
|
146
161
|
sock.ev.on('messages.update', (updates) => {
|
|
162
|
+
deafDetector?.onMessageActivity();
|
|
147
163
|
for (const update of updates) {
|
|
148
164
|
// 463 error detection
|
|
149
165
|
if (update?.update?.messageStubParameters) {
|
|
@@ -161,6 +177,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
161
177
|
// Register known chats from incoming messages + handle reply suggestions + learn LID mappings
|
|
162
178
|
sock.ev.on('messages.upsert', (upsert) => {
|
|
163
179
|
const { messages } = upsert;
|
|
180
|
+
deafDetector?.onMessageActivity();
|
|
164
181
|
// Learn LID mappings FIRST (before any other processing)
|
|
165
182
|
antiban.jidCanonicalizer?.onIncomingEvent(upsert);
|
|
166
183
|
for (const msg of messages || []) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.5",
|
|
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",
|