@prototypevip/baileys 0.0.8 → 0.0.9

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 (54) hide show
  1. package/lib/AntiBan/antiban.js +922 -0
  2. package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
  3. package/lib/AntiBan/cli.js +177 -0
  4. package/lib/AntiBan/contactGraph.js +236 -0
  5. package/lib/AntiBan/contentVariator.js +150 -0
  6. package/lib/AntiBan/credsSnapshot.js +121 -0
  7. package/lib/AntiBan/deliveryTracker.js +107 -0
  8. package/lib/AntiBan/deviceFingerprint.js +106 -0
  9. package/lib/AntiBan/fleetEventStore.js +157 -0
  10. package/lib/AntiBan/groupOperationGuard.js +116 -0
  11. package/lib/AntiBan/health.js +207 -0
  12. package/lib/AntiBan/humanEntropy.js +337 -0
  13. package/lib/AntiBan/index.js +77 -0
  14. package/lib/AntiBan/instanceCoordinator.js +175 -0
  15. package/lib/AntiBan/jidCanonicalizer.js +256 -0
  16. package/lib/AntiBan/jidCircuitBreaker.js +186 -0
  17. package/lib/AntiBan/legitimacySignalInjector.js +231 -0
  18. package/lib/AntiBan/lidFirstResolver.js +174 -0
  19. package/lib/AntiBan/lidResolver.js +324 -0
  20. package/lib/AntiBan/messageQueue.js +187 -0
  21. package/lib/AntiBan/messageRecovery.js +283 -0
  22. package/lib/AntiBan/messageTypeRegistry.js +369 -0
  23. package/lib/AntiBan/observability.js +145 -0
  24. package/lib/AntiBan/patch.js +302 -0
  25. package/lib/AntiBan/persist.js +87 -0
  26. package/lib/AntiBan/presenceChoreographer.js +430 -0
  27. package/lib/AntiBan/presets.js +105 -0
  28. package/lib/AntiBan/profiles.js +31 -0
  29. package/lib/AntiBan/proxyRotator.js +306 -0
  30. package/lib/AntiBan/rateLimiter.js +261 -0
  31. package/lib/AntiBan/readReceiptVariance.js +88 -0
  32. package/lib/AntiBan/reconnectThrottle.js +180 -0
  33. package/lib/AntiBan/replyRatio.js +161 -0
  34. package/lib/AntiBan/reputationVoucher.js +373 -0
  35. package/lib/AntiBan/retryReason.js +91 -0
  36. package/lib/AntiBan/retryTracker.js +172 -0
  37. package/lib/AntiBan/scheduler.js +111 -0
  38. package/lib/AntiBan/sessionFingerprint.js +248 -0
  39. package/lib/AntiBan/sessionStability.js +330 -0
  40. package/lib/AntiBan/stateAdapter.js +73 -0
  41. package/lib/AntiBan/stateExport.js +152 -0
  42. package/lib/AntiBan/stealthConnect.js +130 -0
  43. package/lib/AntiBan/timelockGuard.js +181 -0
  44. package/lib/AntiBan/topologyThrottler.js +339 -0
  45. package/lib/AntiBan/warmup.js +140 -0
  46. package/lib/AntiBan/webhooks.js +80 -0
  47. package/lib/AntiBan/wrapper.js +456 -0
  48. package/lib/Socket/groups.js +26 -21
  49. package/lib/Socket/index.js +139 -7
  50. package/lib/Socket/messages-send.js +68 -5
  51. package/lib/Utils/ai-rich-formatter.js +256 -0
  52. package/lib/Utils/index.js +1 -0
  53. package/lib/index.js +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,922 @@
1
+ /**
2
+ * AntiBan — Main orchestrator combining rate limiting, warm-up, and health monitoring
3
+ *
4
+ * Usage:
5
+ * import { AntiBan } from 'baileys-antiban';
6
+ * const antiban = new AntiBan();
7
+ *
8
+ * // Before sending a message:
9
+ * const result = await antiban.beforeSend(recipient, content);
10
+ * if (result.allowed) {
11
+ * await new Promise(r => setTimeout(r, result.delayMs));
12
+ * await sock.sendMessage(recipient, { text: content });
13
+ * antiban.afterSend(recipient, content);
14
+ * }
15
+ */
16
+ import { RateLimiter } from './rateLimiter.js';
17
+ import { WarmUp } from './warmup.js';
18
+ import { HealthMonitor } from './health.js';
19
+ import { TimelockGuard } from './timelockGuard.js';
20
+ import { ReplyRatioGuard } from './replyRatio.js';
21
+ import { ContactGraphWarmer } from './contactGraph.js';
22
+ import { PresenceChoreographer } from './presenceChoreographer.js';
23
+ import { RetryReasonTracker } from './retryTracker.js';
24
+ import { TopologyThrottler } from './topologyThrottler.js';
25
+ import { PostReconnectThrottle } from './reconnectThrottle.js';
26
+ import { LidResolver } from './lidResolver.js';
27
+ import { JidCanonicalizer } from './jidCanonicalizer.js';
28
+ import { SessionHealthMonitor } from './sessionStability.js';
29
+ import { BanRecoveryOrchestrator } from './banRecoveryOrchestrator.js';
30
+ import { resolveConfig } from './presets.js';
31
+ import { StateManager } from './persist.js';
32
+ import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
33
+ import { DeliveryTracker } from './deliveryTracker.js';
34
+ import { InstanceCoordinator } from './instanceCoordinator.js';
35
+ import { MessageTypeRegistry } from './messageTypeRegistry.js';
36
+ import { exportAntibanState, importAntibanState } from './stateExport.js';
37
+ import { JidCircuitBreaker } from './jidCircuitBreaker.js';
38
+ function isLegacyConfig(cfg) {
39
+ if (typeof cfg !== 'object' || cfg === null)
40
+ return false;
41
+ return 'rateLimiter' in cfg || 'warmUp' in cfg || 'health' in cfg || 'timelock' in cfg ||
42
+ 'replyRatio' in cfg || 'contactGraph' in cfg || 'presence' in cfg || 'retryTracker' in cfg ||
43
+ 'reconnectThrottle' in cfg || 'lidResolver' in cfg || 'jidCanonicalizer' in cfg ||
44
+ 'sessionStability' in cfg;
45
+ }
46
+ function mapLegacyToFlat(legacy) {
47
+ console.warn('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. ' +
48
+ 'Migrate to flat config: new AntiBan({ maxPerMinute: 8 }). ' +
49
+ 'See: https://github.com/kobie3717/baileys-antiban#migration');
50
+ const flat = {};
51
+ if (legacy.rateLimiter?.maxPerMinute !== undefined)
52
+ flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
53
+ if (legacy.rateLimiter?.maxPerHour !== undefined)
54
+ flat.maxPerHour = legacy.rateLimiter.maxPerHour;
55
+ if (legacy.rateLimiter?.maxPerDay !== undefined)
56
+ flat.maxPerDay = legacy.rateLimiter.maxPerDay;
57
+ if (legacy.rateLimiter?.minDelayMs !== undefined)
58
+ flat.minDelayMs = legacy.rateLimiter.minDelayMs;
59
+ if (legacy.rateLimiter?.maxDelayMs !== undefined)
60
+ flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
61
+ if (legacy.rateLimiter?.newChatDelayMs !== undefined)
62
+ flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
63
+ if (legacy.warmUp?.warmUpDays !== undefined)
64
+ flat.warmupDays = legacy.warmUp.warmUpDays;
65
+ if (legacy.warmUp?.day1Limit !== undefined)
66
+ flat.day1Limit = legacy.warmUp.day1Limit;
67
+ if (legacy.warmUp?.growthFactor !== undefined)
68
+ flat.growthFactor = legacy.warmUp.growthFactor;
69
+ if (legacy.logging !== undefined)
70
+ flat.logging = legacy.logging;
71
+ // Preserve flat top-level fields that coexist with nested keys
72
+ const legacyAsFlat = legacy;
73
+ if (flat.warmupDays === undefined && typeof legacyAsFlat.warmUpDays === 'number')
74
+ flat.warmupDays = legacyAsFlat.warmUpDays;
75
+ if (flat.warmupDays === undefined && typeof legacyAsFlat.warmupDays === 'number')
76
+ flat.warmupDays = legacyAsFlat.warmupDays;
77
+ if (flat.day1Limit === undefined && typeof legacyAsFlat.day1Limit === 'number')
78
+ flat.day1Limit = legacyAsFlat.day1Limit;
79
+ if (flat.growthFactor === undefined && typeof legacyAsFlat.growthFactor === 'number')
80
+ flat.growthFactor = legacyAsFlat.growthFactor;
81
+ if (flat.inactivityThresholdHours === undefined && typeof legacyAsFlat.inactivityThresholdHours === 'number')
82
+ flat.inactivityThresholdHours = legacyAsFlat.inactivityThresholdHours;
83
+ if (flat.maxIdenticalMessages === undefined && typeof legacyAsFlat.maxIdenticalMessages === 'number')
84
+ flat.maxIdenticalMessages = legacyAsFlat.maxIdenticalMessages;
85
+ if (flat.identicalMessageWindowMs === undefined && typeof legacyAsFlat.identicalMessageWindowMs === 'number')
86
+ flat.identicalMessageWindowMs = legacyAsFlat.identicalMessageWindowMs;
87
+ if (flat.burstAllowance === undefined && typeof legacyAsFlat.burstAllowance === 'number')
88
+ flat.burstAllowance = legacyAsFlat.burstAllowance;
89
+ // v3 fields that may coexist with legacy nested keys
90
+ if (flat.autoPauseAt === undefined && typeof legacyAsFlat.autoPauseAt === 'string')
91
+ flat.autoPauseAt = legacyAsFlat.autoPauseAt;
92
+ if (flat.groupMultiplier === undefined && typeof legacyAsFlat.groupMultiplier === 'number')
93
+ flat.groupMultiplier = legacyAsFlat.groupMultiplier;
94
+ if (flat.groupProfiles === undefined && typeof legacyAsFlat.groupProfiles === 'boolean')
95
+ flat.groupProfiles = legacyAsFlat.groupProfiles;
96
+ if (flat.persist === undefined && typeof legacyAsFlat.persist === 'string')
97
+ flat.persist = legacyAsFlat.persist;
98
+ return flat;
99
+ }
100
+ export class AntiBan {
101
+ rateLimiter;
102
+ warmUp;
103
+ health;
104
+ timelockGuard;
105
+ replyRatioGuard;
106
+ contactGraphWarmer;
107
+ presenceChoreographer;
108
+ retryTrackerModule;
109
+ reconnectThrottleModule;
110
+ topologyThrottlerModule = null;
111
+ lidResolverModule = null;
112
+ jidCanonicalizerModule = null;
113
+ sessionStabilityMonitor = null;
114
+ banRecovery;
115
+ deliveryTracker;
116
+ instanceCoordinator = null;
117
+ messageTypeRegistry = null;
118
+ jidCircuitBreakerModule = null;
119
+ stateManager = null;
120
+ resolvedConfig;
121
+ logging;
122
+ hasDisconnected = false; // BUG FIX 3: Track if we've ever disconnected
123
+ /** Optional reputation voucher (standalone — caller manages separate voucher sockets) */
124
+ reputationVoucher;
125
+ stats = {
126
+ messagesAllowed: 0,
127
+ messagesBlocked: 0,
128
+ totalDelayMs: 0,
129
+ };
130
+ constructor(input, warmUpStateArg) {
131
+ let flatConfig;
132
+ let legacyPassthrough = null;
133
+ let warmUpState = warmUpStateArg;
134
+ if (isLegacyConfig(input)) {
135
+ legacyPassthrough = input;
136
+ flatConfig = mapLegacyToFlat(legacyPassthrough);
137
+ }
138
+ else {
139
+ flatConfig = {};
140
+ legacyPassthrough = null;
141
+ }
142
+ const cfg = isLegacyConfig(input)
143
+ ? resolveConfig(flatConfig)
144
+ : resolveConfig(input);
145
+ this.resolvedConfig = cfg;
146
+ // Initialize persistence — load state before constructing modules
147
+ let savedState = null;
148
+ if (cfg.persist) {
149
+ this.stateManager = new StateManager(cfg.persist);
150
+ savedState = this.stateManager.load();
151
+ if (savedState) {
152
+ warmUpState = savedState.warmup;
153
+ }
154
+ }
155
+ this.logging = cfg.logging ?? true;
156
+ this.rateLimiter = new RateLimiter({
157
+ maxPerMinute: cfg.maxPerMinute,
158
+ maxPerHour: cfg.maxPerHour,
159
+ maxPerDay: cfg.maxPerDay,
160
+ minDelayMs: cfg.minDelayMs,
161
+ maxDelayMs: cfg.maxDelayMs,
162
+ newChatDelayMs: cfg.newChatDelayMs,
163
+ maxIdenticalMessages: cfg.maxIdenticalMessages,
164
+ identicalMessageWindowMs: cfg.identicalMessageWindowMs,
165
+ burstAllowance: cfg.burstAllowance,
166
+ ...(legacyPassthrough?.rateLimiter || {}),
167
+ });
168
+ // Restore knownChats from persisted state after rateLimiter is constructed
169
+ if (savedState?.knownChats) {
170
+ this.rateLimiter.restoreKnownChats(savedState.knownChats);
171
+ }
172
+ this.warmUp = new WarmUp({
173
+ warmUpDays: cfg.warmupDays,
174
+ day1Limit: cfg.day1Limit,
175
+ growthFactor: cfg.growthFactor,
176
+ inactivityThresholdHours: cfg.inactivityThresholdHours,
177
+ ...(legacyPassthrough?.warmUp || {}),
178
+ }, warmUpState);
179
+ this.health = new HealthMonitor({
180
+ autoPauseAt: cfg.autoPauseAt,
181
+ ...(legacyPassthrough?.health || {}),
182
+ onRiskChange: (status) => {
183
+ if (this.logging) {
184
+ const emoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' };
185
+ console.log(`[baileys-antiban] ${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`);
186
+ console.log(`[baileys-antiban] ${status.recommendation}`);
187
+ status.reasons.forEach(r => console.log(`[baileys-antiban] → ${r}`));
188
+ }
189
+ if ((status.risk === 'high' || status.risk === 'critical') && cfg.onAtRisk) {
190
+ cfg.onAtRisk(status);
191
+ }
192
+ // Trigger recovery orchestrator on critical risk
193
+ if (status.risk === 'critical') {
194
+ this.banRecovery.recordBanEvent('soft_ban');
195
+ }
196
+ cfg.onRiskChange?.(status);
197
+ legacyPassthrough?.health?.onRiskChange?.(status);
198
+ },
199
+ });
200
+ // Initialize ban recovery orchestrator
201
+ this.banRecovery = new BanRecoveryOrchestrator({
202
+ onPhaseChange: (phase, plan) => {
203
+ if (this.logging) {
204
+ console.log(`[baileys-antiban] 🔄 Recovery phase: ${phase} — ${plan.description}`);
205
+ }
206
+ },
207
+ onHardBan: () => {
208
+ if (this.logging) {
209
+ console.log(`[baileys-antiban] 💀 HARD BAN detected — account likely dead, replace SIM`);
210
+ }
211
+ },
212
+ });
213
+ // Initialize delivery tracker
214
+ this.deliveryTracker = new DeliveryTracker({
215
+ onLowDeliveryRate: (rate) => {
216
+ if (this.logging) {
217
+ console.log(`[baileys-antiban] ⚠️ Low delivery rate detected: ${Math.round(rate * 100)}% — possible soft ban`);
218
+ }
219
+ },
220
+ });
221
+ this.timelockGuard = new TimelockGuard({
222
+ ...(legacyPassthrough?.timelock || {}),
223
+ onTimelockDetected: (state) => {
224
+ this.health.recordReachoutTimelock(state.enforcementType);
225
+ this.banRecovery.recordBanEvent('timelock');
226
+ if (this.logging) {
227
+ console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
228
+ }
229
+ cfg.onTimelockDetected?.(state);
230
+ legacyPassthrough?.timelock?.onTimelockDetected?.(state);
231
+ },
232
+ onTimelockLifted: (state) => {
233
+ if (this.logging) {
234
+ console.log(`[baileys-antiban] Timelock lifted — resuming new contact messages`);
235
+ }
236
+ cfg.onTimelockLifted?.(state);
237
+ legacyPassthrough?.timelock?.onTimelockLifted?.(state);
238
+ },
239
+ });
240
+ this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio);
241
+ this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph);
242
+ this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence);
243
+ this.retryTrackerModule = new RetryReasonTracker({
244
+ ...(legacyPassthrough?.retryTracker || {}),
245
+ onSpiral: (msgId, reason) => {
246
+ if (this.logging) {
247
+ console.log(`[baileys-antiban] ⚠️ Message ${msgId} stuck in retry spiral (${reason})`);
248
+ }
249
+ legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
250
+ },
251
+ });
252
+ this.reconnectThrottleModule = new PostReconnectThrottle({
253
+ ...(legacyPassthrough?.reconnectThrottle || {}),
254
+ baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
255
+ });
256
+ // Initialize topology throttler if configured
257
+ if (legacyPassthrough?.topologyThrottler) {
258
+ this.topologyThrottlerModule = new TopologyThrottler(legacyPassthrough.topologyThrottler);
259
+ if (this.logging) {
260
+ console.log(`[baileys-antiban] 🌐 Topology throttler enabled — max ${legacyPassthrough.topologyThrottler.maxNewContactsPerHour || 5}/hr, ${legacyPassthrough.topologyThrottler.maxNewContactsPerDay || 20}/day new contacts`);
261
+ }
262
+ }
263
+ // Initialize LID resolver and canonicalizer if configured
264
+ // If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
265
+ if (legacyPassthrough?.jidCanonicalizer?.enabled) {
266
+ // Create or use provided resolver
267
+ if (legacyPassthrough.jidCanonicalizer.resolver) {
268
+ // User provided their own resolver
269
+ this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
270
+ this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
271
+ }
272
+ else {
273
+ // Create new resolver using lidResolver config if provided
274
+ const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
275
+ const resolver = new LidResolver(resolverConfig);
276
+ this.lidResolverModule = resolver;
277
+ this.jidCanonicalizerModule = new JidCanonicalizer({
278
+ ...legacyPassthrough.jidCanonicalizer,
279
+ resolver,
280
+ });
281
+ }
282
+ }
283
+ else if (legacyPassthrough?.lidResolver) {
284
+ // Standalone resolver without canonicalizer
285
+ this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver);
286
+ }
287
+ // Initialize session stability monitor if enabled
288
+ if (legacyPassthrough?.sessionStability?.enabled) {
289
+ const healthConfig = {
290
+ badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
291
+ badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
292
+ onDegraded: (stats) => {
293
+ if (this.logging) {
294
+ console.log(`[baileys-antiban] 🔴 SESSION DEGRADED — Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 60000}ms`);
295
+ console.log(`[baileys-antiban] Consider restarting session or switching to LID-based canonical form`);
296
+ }
297
+ },
298
+ onRecovered: () => {
299
+ if (this.logging) {
300
+ console.log(`[baileys-antiban] 🟢 SESSION RECOVERED — decrypt success rate improved`);
301
+ }
302
+ },
303
+ };
304
+ this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
305
+ }
306
+ // Initialize instance coordinator if configured
307
+ if (cfg.instanceCoordinator) {
308
+ this.instanceCoordinator = new InstanceCoordinator({
309
+ sharedFilePath: cfg.instanceCoordinator,
310
+ poolMaxPerMinute: cfg.instancePoolMaxPerMinute,
311
+ poolMaxPerHour: cfg.instancePoolMaxPerHour,
312
+ });
313
+ if (this.logging) {
314
+ console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
315
+ }
316
+ }
317
+ // Initialize message type registry if configured
318
+ if (cfg.messageTypeRegistry) {
319
+ this.messageTypeRegistry = new MessageTypeRegistry();
320
+ if (this.logging) {
321
+ console.log(`[baileys-antiban] 📝 Message type registry enabled`);
322
+ }
323
+ }
324
+ // Initialize JID circuit breaker if configured (BUG FIX 2)
325
+ if (cfg.circuitBreaker) {
326
+ const cbConfig = typeof cfg.circuitBreaker === 'object'
327
+ ? cfg.circuitBreaker
328
+ : {};
329
+ this.jidCircuitBreakerModule = new JidCircuitBreaker({
330
+ ...cbConfig,
331
+ logger: this.logging ? { warn: console.warn.bind(console), info: console.info.bind(console) } : undefined,
332
+ });
333
+ if (this.logging) {
334
+ console.log(`[baileys-antiban] 🔌 JID circuit breaker enabled`);
335
+ }
336
+ }
337
+ }
338
+ /**
339
+ * Check if a message can be sent and get required delay.
340
+ * Call this BEFORE every sendMessage().
341
+ */
342
+ async beforeSend(recipient, content) {
343
+ const healthStatus = this.health.getStatus();
344
+ // Health monitor says stop
345
+ if (this.health.isPaused()) {
346
+ this.stats.messagesBlocked++;
347
+ if (this.logging) {
348
+ console.log(`[baileys-antiban] ⛔ BLOCKED — health risk too high (${healthStatus.risk})`);
349
+ }
350
+ return {
351
+ allowed: false,
352
+ delayMs: 0,
353
+ reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
354
+ health: healthStatus,
355
+ };
356
+ }
357
+ // Recovery orchestrator rate multiplier
358
+ const recoveryStatus = this.banRecovery.getStatus();
359
+ if (recoveryStatus.phase === 'paused') {
360
+ this.stats.messagesBlocked++;
361
+ return {
362
+ allowed: false,
363
+ delayMs: recoveryStatus.pauseRemainingMs || 0,
364
+ reason: `Ban recovery: ${recoveryStatus.recommendation}`,
365
+ health: healthStatus,
366
+ };
367
+ }
368
+ // Timelock guard (allows existing chats, blocks new contacts)
369
+ const timelockDecision = this.timelockGuard.canSend(recipient);
370
+ if (!timelockDecision.allowed) {
371
+ this.stats.messagesBlocked++;
372
+ if (this.logging) {
373
+ console.log(`[baileys-antiban] TIMELOCKED — ${timelockDecision.reason}`);
374
+ }
375
+ return {
376
+ allowed: false,
377
+ delayMs: 0,
378
+ reason: timelockDecision.reason,
379
+ health: healthStatus,
380
+ };
381
+ }
382
+ // Warm-up limit check
383
+ if (!this.warmUp.canSend()) {
384
+ this.stats.messagesBlocked++;
385
+ const warmUpStatus = this.warmUp.getStatus();
386
+ if (this.logging) {
387
+ console.log(`[baileys-antiban] ⏳ BLOCKED — warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`);
388
+ }
389
+ return {
390
+ allowed: false,
391
+ delayMs: 0,
392
+ reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
393
+ health: healthStatus,
394
+ warmUpDay: warmUpStatus.day,
395
+ };
396
+ }
397
+ // Contact graph check
398
+ const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient);
399
+ if (!contactGraphDecision.allowed) {
400
+ this.stats.messagesBlocked++;
401
+ if (this.logging) {
402
+ console.log(`[baileys-antiban] 📊 BLOCKED — contact graph: ${contactGraphDecision.reason}`);
403
+ }
404
+ return {
405
+ allowed: false,
406
+ delayMs: 0,
407
+ reason: `Contact graph: ${contactGraphDecision.reason}`,
408
+ health: healthStatus,
409
+ };
410
+ }
411
+ // Topology throttler check — only applies to DMs to new/unknown contacts
412
+ if (this.topologyThrottlerModule && !this.isGroupJid(recipient)) {
413
+ const knownChats = this.rateLimiter.getKnownChats();
414
+ const isNewContact = !knownChats.has(recipient);
415
+ if (isNewContact) {
416
+ // Check if we can send to new contact based on topology limits
417
+ const topologyDecision = this.topologyThrottlerModule.canSendToNewContact();
418
+ if (!topologyDecision.allowed) {
419
+ this.stats.messagesBlocked++;
420
+ if (this.logging) {
421
+ console.log(`[baileys-antiban] 🌐 BLOCKED — topology: ${topologyDecision.reason}`);
422
+ }
423
+ return {
424
+ allowed: false,
425
+ delayMs: topologyDecision.retryAfterMs || 0,
426
+ reason: `Topology: ${topologyDecision.reason}`,
427
+ health: healthStatus,
428
+ };
429
+ }
430
+ // Assess contact risk
431
+ const riskAssessment = this.topologyThrottlerModule.assessContact(recipient, {
432
+ messageType: 'dm',
433
+ hasReplied: false,
434
+ knownGroups: [],
435
+ });
436
+ if (riskAssessment.recommendation === 'abort') {
437
+ this.stats.messagesBlocked++;
438
+ if (this.logging) {
439
+ console.log(`[baileys-antiban] ⛔ BLOCKED — contact risk ${riskAssessment.risk} (score ${riskAssessment.score}): ${riskAssessment.reasons.join(', ')}`);
440
+ }
441
+ return {
442
+ allowed: false,
443
+ delayMs: 0,
444
+ reason: `Contact risk too high: ${riskAssessment.reasons.join(', ')}`,
445
+ health: healthStatus,
446
+ };
447
+ }
448
+ // If delay recommended, we'll add it to the total delay later
449
+ }
450
+ }
451
+ // Reply ratio check
452
+ const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient);
453
+ if (!replyRatioDecision.allowed) {
454
+ this.stats.messagesBlocked++;
455
+ if (this.logging) {
456
+ console.log(`[baileys-antiban] 💬 BLOCKED — reply ratio: ${replyRatioDecision.reason}`);
457
+ }
458
+ return {
459
+ allowed: false,
460
+ delayMs: 0,
461
+ reason: `Reply ratio: ${replyRatioDecision.reason}`,
462
+ health: healthStatus,
463
+ };
464
+ }
465
+ // Reconnect throttle check
466
+ const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend();
467
+ if (!reconnectThrottleDecision.allowed) {
468
+ this.stats.messagesBlocked++;
469
+ if (this.logging) {
470
+ console.log(`[baileys-antiban] 🔄 BLOCKED — reconnect throttle: ${reconnectThrottleDecision.reason}`);
471
+ }
472
+ return {
473
+ allowed: false,
474
+ delayMs: reconnectThrottleDecision.retryAfterMs || 0,
475
+ reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
476
+ health: healthStatus,
477
+ };
478
+ }
479
+ // Cross-instance coordination — check shared IP-level pool
480
+ if (this.instanceCoordinator) {
481
+ const slot = this.instanceCoordinator.tryAcquireSlot();
482
+ if (!slot.allowed) {
483
+ this.stats.messagesBlocked++;
484
+ if (this.logging) {
485
+ console.log(`[baileys-antiban] 🌐 BLOCKED — instance pool exhausted (shared IP limit), retry in ${slot.retryAfterMs}ms`);
486
+ }
487
+ return {
488
+ allowed: false,
489
+ delayMs: slot.retryAfterMs || 5000,
490
+ reason: 'Cross-instance rate pool exhausted',
491
+ health: healthStatus,
492
+ };
493
+ }
494
+ }
495
+ // Group profile rate check (runs before rateLimiter.getDelay for timing)
496
+ if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
497
+ const groupLimits = applyGroupMultiplier({
498
+ maxPerMinute: this.resolvedConfig.maxPerMinute,
499
+ maxPerHour: this.resolvedConfig.maxPerHour,
500
+ maxPerDay: this.resolvedConfig.maxPerDay,
501
+ }, this.resolvedConfig.groupMultiplier);
502
+ const stats = this.rateLimiter.getStats();
503
+ if (stats.lastMinute >= groupLimits.maxPerMinute ||
504
+ stats.lastHour >= groupLimits.maxPerHour ||
505
+ stats.lastDay >= groupLimits.maxPerDay) {
506
+ this.stats.messagesBlocked++;
507
+ if (this.logging) {
508
+ console.log(`[baileys-antiban] 🚫 BLOCKED — group rate limit exceeded for ${recipient}`);
509
+ }
510
+ return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
511
+ }
512
+ }
513
+ // Rate limiter delay
514
+ let delay = await this.rateLimiter.getDelay(recipient, content);
515
+ if (delay === -1) {
516
+ this.stats.messagesBlocked++;
517
+ if (this.logging) {
518
+ console.log(`[baileys-antiban] 🚫 BLOCKED — rate limit or identical message spam`);
519
+ }
520
+ return {
521
+ allowed: false,
522
+ delayMs: 0,
523
+ reason: 'Rate limit exceeded or identical message spam detected',
524
+ health: healthStatus,
525
+ };
526
+ }
527
+ // Apply circadian rhythm multiplier to delay
528
+ const activityFactor = this.presenceChoreographer.getCurrentActivityFactor();
529
+ if (activityFactor < 1.0) {
530
+ // Lower activity = longer delays (cap at 5x)
531
+ const multiplier = Math.min(5, 1 / activityFactor);
532
+ delay = Math.floor(delay * multiplier);
533
+ }
534
+ // Per-contact risk multiplier — cold contacts need longer delays
535
+ // Only apply when contact graph is enabled, otherwise all contacts appear as 'stranger'
536
+ if (this.contactGraphWarmer['config']?.enabled) {
537
+ const contactState = this.contactGraphWarmer.getContactState(recipient);
538
+ const coldMultiplier = {
539
+ stranger: 2.5,
540
+ handshake_sent: 1.8,
541
+ handshake_complete: 1.3,
542
+ known: 1.0,
543
+ };
544
+ const contactRiskMult = coldMultiplier[contactState] ?? 1.0;
545
+ if (contactRiskMult > 1.0) {
546
+ delay = Math.floor(delay * contactRiskMult);
547
+ if (this.logging && contactRiskMult >= 2.0) {
548
+ console.log(`[baileys-antiban] ⚠️ Cold contact ${recipient} — ${contactState}, delay ×${contactRiskMult}`);
549
+ }
550
+ }
551
+ }
552
+ // Topology throttler recommended delay
553
+ if (this.topologyThrottlerModule && !this.isGroupJid(recipient)) {
554
+ const knownChats = this.rateLimiter.getKnownChats();
555
+ const isNewContact = !knownChats.has(recipient);
556
+ if (isNewContact) {
557
+ const riskAssessment = this.topologyThrottlerModule.assessContact(recipient, {
558
+ messageType: 'dm',
559
+ hasReplied: false,
560
+ knownGroups: [],
561
+ });
562
+ if (riskAssessment.recommendation === 'delay' && riskAssessment.suggestedDelayMs) {
563
+ delay += riskAssessment.suggestedDelayMs;
564
+ if (this.logging) {
565
+ console.log(`[baileys-antiban] ⚠️ Topology risk ${riskAssessment.risk} — adding ${Math.floor(riskAssessment.suggestedDelayMs / 60000)}min delay`);
566
+ }
567
+ }
568
+ }
569
+ }
570
+ // Roll for distraction pause
571
+ const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
572
+ if (distractionCheck.pause) {
573
+ delay += distractionCheck.durationMs;
574
+ if (this.logging) {
575
+ console.log(`[baileys-antiban] ⏸️ Distraction pause: +${Math.floor(distractionCheck.durationMs / 60000)}min`);
576
+ }
577
+ }
578
+ // Roll for offline gap
579
+ const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap();
580
+ if (offlineCheck.offline) {
581
+ delay += offlineCheck.durationMs;
582
+ if (this.logging) {
583
+ console.log(`[baileys-antiban] 📴 Offline gap: +${Math.floor(offlineCheck.durationMs / 60000)}min`);
584
+ }
585
+ }
586
+ this.stats.totalDelayMs += delay;
587
+ return {
588
+ allowed: true,
589
+ delayMs: delay,
590
+ health: healthStatus,
591
+ };
592
+ }
593
+ /**
594
+ * Record a successfully sent message.
595
+ * Call this AFTER every successful sendMessage().
596
+ */
597
+ afterSend(recipient, content, msgId) {
598
+ this.rateLimiter.record(recipient, content);
599
+ this.warmUp.record();
600
+ this.replyRatioGuard.recordSent(recipient);
601
+ this.topologyThrottlerModule?.recordSent(recipient);
602
+ this.stats.messagesAllowed++;
603
+ if (msgId) {
604
+ this.deliveryTracker.onMessageSent(msgId);
605
+ }
606
+ this.runAdaptiveCheck();
607
+ this.persistStateDebounced();
608
+ }
609
+ /**
610
+ * Record a failed message send
611
+ */
612
+ afterSendFailed(error) {
613
+ this.health.recordMessageFailed(error);
614
+ }
615
+ /**
616
+ * Record a disconnection (call from connection.update handler)
617
+ */
618
+ onDisconnect(reason) {
619
+ this.hasDisconnected = true; // BUG FIX 3: Mark that we've disconnected
620
+ this.health.recordDisconnect(reason);
621
+ this.reconnectThrottleModule.onDisconnect();
622
+ const reasonStr = String(reason);
623
+ if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
624
+ this.persistStateImmediate();
625
+ }
626
+ }
627
+ /**
628
+ * Record a successful reconnection
629
+ */
630
+ onReconnect() {
631
+ this.health.recordReconnect();
632
+ this.reconnectThrottleModule.onReconnect();
633
+ // BUG FIX 3: Sync local rate limiter with shared pool after reconnect
634
+ // This prevents the double-spend window where DeafSessionDetector triggers
635
+ // reconnect → in-memory rate limiter resets to 0 → thinks it has full budget
636
+ // → sends 20 messages → THEN reads shared file and discovers 18/20 already used
637
+ // Only sync if this is a TRUE reconnect (after disconnect), not first connect
638
+ if (this.hasDisconnected && this.instanceCoordinator) {
639
+ this.instanceCoordinator.syncLocalLimiter(this.rateLimiter);
640
+ }
641
+ this.rateLimiter.adaptLimits(1.0);
642
+ }
643
+ /**
644
+ * Handle incoming message — record in reply ratio + contact graph.
645
+ * Returns suggested reply if reply ratio suggests auto-reply.
646
+ */
647
+ onIncomingMessage(jid, msgText) {
648
+ this.replyRatioGuard.recordReceived(jid);
649
+ this.contactGraphWarmer.onIncomingMessage(jid);
650
+ this.topologyThrottlerModule?.recordReplied(jid);
651
+ return this.replyRatioGuard.suggestReply(jid, msgText);
652
+ }
653
+ /**
654
+ * Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
655
+ * Call from messages.update handler when delivery status is received.
656
+ */
657
+ onDeliveryReceipt(msgId) {
658
+ this.deliveryTracker.onDeliveryReceipt(msgId);
659
+ }
660
+ /**
661
+ * Get the resolved configuration
662
+ */
663
+ getConfig() {
664
+ return { ...this.resolvedConfig };
665
+ }
666
+ /**
667
+ * Get comprehensive stats
668
+ */
669
+ getStats() {
670
+ const stats = {
671
+ ...this.stats,
672
+ health: this.health.getStatus(),
673
+ warmUp: this.warmUp.getStatus(),
674
+ rateLimiter: this.rateLimiter.getStats(),
675
+ banRecovery: this.banRecovery.getStatus(),
676
+ deliveryTracker: this.deliveryTracker.getStats(),
677
+ };
678
+ // Only include new stats if enabled
679
+ if (this.replyRatioGuard['config']?.enabled) {
680
+ stats.replyRatio = this.replyRatioGuard.getStats();
681
+ }
682
+ if (this.contactGraphWarmer['config']?.enabled) {
683
+ stats.contactGraph = this.contactGraphWarmer.getStats();
684
+ }
685
+ if (this.presenceChoreographer['config']?.enabled) {
686
+ stats.presence = this.presenceChoreographer.getStats();
687
+ }
688
+ if (this.retryTrackerModule['config']?.enabled) {
689
+ stats.retryTracker = this.retryTrackerModule.getStats();
690
+ }
691
+ if (this.reconnectThrottleModule['config']?.enabled) {
692
+ stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
693
+ }
694
+ if (this.topologyThrottlerModule) {
695
+ stats.topologyThrottler = this.topologyThrottlerModule.getTopologyStats();
696
+ }
697
+ if (this.lidResolverModule) {
698
+ stats.lidResolver = this.lidResolverModule.getStats();
699
+ }
700
+ if (this.jidCanonicalizerModule) {
701
+ stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
702
+ }
703
+ if (this.sessionStabilityMonitor) {
704
+ stats.sessionStability = this.sessionStabilityMonitor.getStats();
705
+ }
706
+ if (this.instanceCoordinator) {
707
+ stats.instanceCoordinator = this.instanceCoordinator.getStats();
708
+ }
709
+ if (this.messageTypeRegistry) {
710
+ const warnings = this.messageTypeRegistry.getWarnings();
711
+ stats.messageRegistry = {
712
+ typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
713
+ warningCount: warnings.length,
714
+ };
715
+ }
716
+ return stats;
717
+ }
718
+ /** Get the timelock guard for direct access */
719
+ get timelock() {
720
+ return this.timelockGuard;
721
+ }
722
+ /** Get the reply ratio guard for direct access */
723
+ get replyRatio() {
724
+ return this.replyRatioGuard;
725
+ }
726
+ /** Get the contact graph warmer for direct access */
727
+ get contactGraph() {
728
+ return this.contactGraphWarmer;
729
+ }
730
+ /** Get the presence choreographer for direct access */
731
+ get presence() {
732
+ return this.presenceChoreographer;
733
+ }
734
+ /** Get the retry tracker for direct access */
735
+ get retryTracker() {
736
+ return this.retryTrackerModule;
737
+ }
738
+ /** Get the reconnect throttle for direct access */
739
+ get reconnectThrottle() {
740
+ return this.reconnectThrottleModule;
741
+ }
742
+ /** Get the topology throttler for direct access */
743
+ get topologyThrottler() {
744
+ return this.topologyThrottlerModule;
745
+ }
746
+ /** Get the topology throttler for direct access (alias) */
747
+ get topology() {
748
+ return this.topologyThrottlerModule;
749
+ }
750
+ /** Get the LID resolver for direct access */
751
+ get lidResolver() {
752
+ return this.lidResolverModule;
753
+ }
754
+ /** Get the JID canonicalizer for direct access */
755
+ get jidCanonicalizer() {
756
+ return this.jidCanonicalizerModule;
757
+ }
758
+ /** Get the session stability monitor for direct access */
759
+ get sessionStability() {
760
+ return this.sessionStabilityMonitor;
761
+ }
762
+ /** Get the ban recovery orchestrator for direct access */
763
+ get recoveryOrchestrator() {
764
+ return this.banRecovery;
765
+ }
766
+ /** Get the message type registry for direct access */
767
+ get messageRegistry() {
768
+ return this.messageTypeRegistry;
769
+ }
770
+ /** Get the JID circuit breaker for direct access (BUG FIX 2) */
771
+ get circuitBreaker() {
772
+ return this.jidCircuitBreakerModule;
773
+ }
774
+ /**
775
+ * Export warm-up state for persistence between restarts
776
+ */
777
+ exportWarmUpState() {
778
+ return this.warmUp.exportState();
779
+ }
780
+ /**
781
+ * Force pause all sending
782
+ */
783
+ pause() {
784
+ this.health.setPaused(true);
785
+ if (this.logging) {
786
+ console.log('[baileys-antiban] ⏸️ Sending paused manually');
787
+ }
788
+ }
789
+ /**
790
+ * Resume sending
791
+ */
792
+ resume() {
793
+ this.health.setPaused(false);
794
+ if (this.logging) {
795
+ console.log('[baileys-antiban] ▶️ Sending resumed');
796
+ }
797
+ }
798
+ /**
799
+ * Reset everything (use after a ban period)
800
+ */
801
+ reset() {
802
+ this.timelockGuard.reset();
803
+ this.health.reset();
804
+ this.warmUp.reset();
805
+ this.replyRatioGuard.reset();
806
+ this.contactGraphWarmer.reset();
807
+ this.presenceChoreographer.reset();
808
+ this.retryTrackerModule.destroy();
809
+ this.reconnectThrottleModule.destroy();
810
+ this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
811
+ if (this.logging) {
812
+ console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
813
+ }
814
+ }
815
+ isGroupJid(jid) {
816
+ return jid.endsWith('@g.us') || jid.endsWith('@newsletter');
817
+ }
818
+ runAdaptiveCheck() {
819
+ const delivery = this.deliveryTracker.getStats();
820
+ // Need min sample to be meaningful
821
+ if (delivery.deliveryRate === null)
822
+ return;
823
+ const rate = delivery.deliveryRate;
824
+ let targetFactor;
825
+ if (rate >= 0.85) {
826
+ targetFactor = 1.0; // Excellent — full speed
827
+ }
828
+ else if (rate >= 0.70) {
829
+ targetFactor = 0.75; // Good — slight reduction
830
+ }
831
+ else if (rate >= 0.55) {
832
+ targetFactor = 0.50; // Poor — halve throughput
833
+ }
834
+ else {
835
+ targetFactor = 0.25; // Bad — severe throttle (soft ban likely)
836
+ }
837
+ const current = this.rateLimiter.getCurrentFactor();
838
+ // Only log + adjust when factor changes meaningfully (>5% delta)
839
+ if (Math.abs(targetFactor - current) > 0.05) {
840
+ if (this.logging) {
841
+ const dir = targetFactor > current ? '📈' : '📉';
842
+ console.log(`[baileys-antiban] ${dir} Adaptive rate: delivery=${(rate * 100).toFixed(0)}% → factor ${current.toFixed(2)}→${targetFactor.toFixed(2)}`);
843
+ }
844
+ this.rateLimiter.adaptLimits(targetFactor);
845
+ }
846
+ }
847
+ persistStateDebounced() {
848
+ if (!this.stateManager)
849
+ return;
850
+ const state = {
851
+ warmup: this.warmUp.exportState(),
852
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
853
+ savedAt: Date.now(),
854
+ version: 3,
855
+ };
856
+ this.stateManager.saveDebounced(state);
857
+ }
858
+ persistStateImmediate() {
859
+ if (!this.stateManager)
860
+ return;
861
+ const state = {
862
+ warmup: this.warmUp.exportState(),
863
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
864
+ savedAt: Date.now(),
865
+ version: 3,
866
+ };
867
+ this.stateManager.saveImmediate(state);
868
+ }
869
+ /**
870
+ * Export unified state snapshot for Redis failover or cross-instance migration.
871
+ * Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
872
+ */
873
+ exportState() {
874
+ return exportAntibanState({
875
+ warmup: this.warmUp,
876
+ health: this.health,
877
+ rateLimiter: this.rateLimiter,
878
+ timelockGuard: this.timelockGuard,
879
+ messageRegistry: this.messageTypeRegistry || undefined,
880
+ topologyThrottler: this.topologyThrottlerModule || undefined,
881
+ reputationVoucher: this.reputationVoucher || undefined,
882
+ circuits: this.jidCircuitBreakerModule || undefined, // BUG FIX 2
883
+ instanceId: this.resolvedConfig.instanceId,
884
+ });
885
+ }
886
+ /**
887
+ * Import unified state snapshot.
888
+ * CRDT-safe for rate limiters (never overwrites higher counts).
889
+ */
890
+ importState(snapshot) {
891
+ importAntibanState(snapshot, {
892
+ warmup: this.warmUp,
893
+ health: this.health,
894
+ rateLimiter: this.rateLimiter,
895
+ timelockGuard: this.timelockGuard,
896
+ messageRegistry: this.messageTypeRegistry || undefined,
897
+ topologyThrottler: this.topologyThrottlerModule || undefined,
898
+ reputationVoucher: this.reputationVoucher || undefined,
899
+ circuits: this.jidCircuitBreakerModule || undefined, // BUG FIX 2
900
+ });
901
+ }
902
+ /**
903
+ * Clean up all timers and resources.
904
+ * Call this when disposing of the AntiBan instance or when the socket closes.
905
+ */
906
+ destroy() {
907
+ this.stateManager?.destroy();
908
+ this.timelockGuard.reset(); // Clears the resumeTimer
909
+ this.replyRatioGuard.reset();
910
+ this.contactGraphWarmer.reset();
911
+ this.presenceChoreographer.reset();
912
+ this.retryTrackerModule.destroy();
913
+ this.reconnectThrottleModule.destroy();
914
+ this.jidCanonicalizerModule?.destroy();
915
+ this.lidResolverModule?.destroy();
916
+ this.sessionStabilityMonitor?.reset();
917
+ this.messageTypeRegistry?.cleanup();
918
+ if (this.logging) {
919
+ console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
920
+ }
921
+ }
922
+ }