baileys-antiban 3.9.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/antiban.d.ts CHANGED
@@ -25,7 +25,10 @@ import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThro
25
25
  import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
26
26
  import { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
27
27
  import { SessionHealthMonitor, type SessionHealthStats } from './sessionStability.js';
28
+ import { BanRecoveryOrchestrator, type RecoveryStatus } from './banRecoveryOrchestrator.js';
28
29
  import { type AntiBanInput, type ResolvedConfig } from './presets.js';
30
+ import { type DeliveryTrackerStats } from './deliveryTracker.js';
31
+ import { type InstanceCoordinatorStats } from './instanceCoordinator.js';
29
32
  export interface AntiBanConfigLegacy {
30
33
  rateLimiter?: Partial<RateLimiterConfig>;
31
34
  warmUp?: Partial<WarmUpConfig>;
@@ -70,6 +73,9 @@ export interface AntiBanStats {
70
73
  lidResolver?: LidResolverStats | null;
71
74
  jidCanonicalizer?: JidCanonicalizerStats | null;
72
75
  sessionStability?: SessionHealthStats | null;
76
+ banRecovery?: RecoveryStatus | null;
77
+ deliveryTracker: DeliveryTrackerStats;
78
+ instanceCoordinator?: InstanceCoordinatorStats | null;
73
79
  }
74
80
  export declare class AntiBan {
75
81
  private rateLimiter;
@@ -84,6 +90,9 @@ export declare class AntiBan {
84
90
  private lidResolverModule;
85
91
  private jidCanonicalizerModule;
86
92
  private sessionStabilityMonitor;
93
+ private banRecovery;
94
+ private deliveryTracker;
95
+ private instanceCoordinator;
87
96
  private stateManager;
88
97
  private resolvedConfig;
89
98
  private logging;
@@ -98,7 +107,7 @@ export declare class AntiBan {
98
107
  * Record a successfully sent message.
99
108
  * Call this AFTER every successful sendMessage().
100
109
  */
101
- afterSend(recipient: string, content: string): void;
110
+ afterSend(recipient: string, content: string, msgId?: string): void;
102
111
  /**
103
112
  * Record a failed message send
104
113
  */
@@ -119,6 +128,11 @@ export declare class AntiBan {
119
128
  shouldReply: boolean;
120
129
  suggestedText?: string;
121
130
  };
131
+ /**
132
+ * Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
133
+ * Call from messages.update handler when delivery status is received.
134
+ */
135
+ onDeliveryReceipt(msgId: string): void;
122
136
  /**
123
137
  * Get the resolved configuration
124
138
  */
@@ -145,6 +159,8 @@ export declare class AntiBan {
145
159
  get jidCanonicalizer(): JidCanonicalizer | null;
146
160
  /** Get the session stability monitor for direct access */
147
161
  get sessionStability(): SessionHealthMonitor | null;
162
+ /** Get the ban recovery orchestrator for direct access */
163
+ get recoveryOrchestrator(): BanRecoveryOrchestrator;
148
164
  /**
149
165
  * Export warm-up state for persistence between restarts
150
166
  */
@@ -161,6 +177,7 @@ export declare class AntiBan {
161
177
  * Reset everything (use after a ban period)
162
178
  */
163
179
  reset(): void;
180
+ private runAdaptiveCheck;
164
181
  private persistStateDebounced;
165
182
  private persistStateImmediate;
166
183
  /**
package/dist/antiban.js CHANGED
@@ -25,9 +25,12 @@ import { PostReconnectThrottle } from './reconnectThrottle.js';
25
25
  import { LidResolver } from './lidResolver.js';
26
26
  import { JidCanonicalizer } from './jidCanonicalizer.js';
27
27
  import { SessionHealthMonitor } from './sessionStability.js';
28
+ import { BanRecoveryOrchestrator } from './banRecoveryOrchestrator.js';
28
29
  import { resolveConfig } from './presets.js';
29
30
  import { StateManager } from './persist.js';
30
31
  import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
32
+ import { DeliveryTracker } from './deliveryTracker.js';
33
+ import { InstanceCoordinator } from './instanceCoordinator.js';
31
34
  function isLegacyConfig(cfg) {
32
35
  if (typeof cfg !== 'object' || cfg === null)
33
36
  return false;
@@ -103,6 +106,9 @@ export class AntiBan {
103
106
  lidResolverModule = null;
104
107
  jidCanonicalizerModule = null;
105
108
  sessionStabilityMonitor = null;
109
+ banRecovery;
110
+ deliveryTracker;
111
+ instanceCoordinator = null;
106
112
  stateManager = null;
107
113
  resolvedConfig;
108
114
  logging;
@@ -173,14 +179,40 @@ export class AntiBan {
173
179
  if ((status.risk === 'high' || status.risk === 'critical') && cfg.onAtRisk) {
174
180
  cfg.onAtRisk(status);
175
181
  }
182
+ // Trigger recovery orchestrator on critical risk
183
+ if (status.risk === 'critical') {
184
+ this.banRecovery.recordBanEvent('soft_ban');
185
+ }
176
186
  cfg.onRiskChange?.(status);
177
187
  legacyPassthrough?.health?.onRiskChange?.(status);
178
188
  },
179
189
  });
190
+ // Initialize ban recovery orchestrator
191
+ this.banRecovery = new BanRecoveryOrchestrator({
192
+ onPhaseChange: (phase, plan) => {
193
+ if (this.logging) {
194
+ console.log(`[baileys-antiban] 🔄 Recovery phase: ${phase} — ${plan.description}`);
195
+ }
196
+ },
197
+ onHardBan: () => {
198
+ if (this.logging) {
199
+ console.log(`[baileys-antiban] 💀 HARD BAN detected — account likely dead, replace SIM`);
200
+ }
201
+ },
202
+ });
203
+ // Initialize delivery tracker
204
+ this.deliveryTracker = new DeliveryTracker({
205
+ onLowDeliveryRate: (rate) => {
206
+ if (this.logging) {
207
+ console.log(`[baileys-antiban] ⚠️ Low delivery rate detected: ${Math.round(rate * 100)}% — possible soft ban`);
208
+ }
209
+ },
210
+ });
180
211
  this.timelockGuard = new TimelockGuard({
181
212
  ...(legacyPassthrough?.timelock || {}),
182
213
  onTimelockDetected: (state) => {
183
214
  this.health.recordReachoutTimelock(state.enforcementType);
215
+ this.banRecovery.recordBanEvent('timelock');
184
216
  if (this.logging) {
185
217
  console.log(`[baileys-antiban] REACHOUT TIMELOCKED — ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
186
218
  }
@@ -254,6 +286,17 @@ export class AntiBan {
254
286
  };
255
287
  this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
256
288
  }
289
+ // Initialize instance coordinator if configured
290
+ if (cfg.instanceCoordinator) {
291
+ this.instanceCoordinator = new InstanceCoordinator({
292
+ sharedFilePath: cfg.instanceCoordinator,
293
+ poolMaxPerMinute: cfg.instancePoolMaxPerMinute,
294
+ poolMaxPerHour: cfg.instancePoolMaxPerHour,
295
+ });
296
+ if (this.logging) {
297
+ console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
298
+ }
299
+ }
257
300
  }
258
301
  /**
259
302
  * Check if a message can be sent and get required delay.
@@ -274,6 +317,17 @@ export class AntiBan {
274
317
  health: healthStatus,
275
318
  };
276
319
  }
320
+ // Recovery orchestrator rate multiplier
321
+ const recoveryStatus = this.banRecovery.getStatus();
322
+ if (recoveryStatus.phase === 'paused') {
323
+ this.stats.messagesBlocked++;
324
+ return {
325
+ allowed: false,
326
+ delayMs: recoveryStatus.pauseRemainingMs || 0,
327
+ reason: `Ban recovery: ${recoveryStatus.recommendation}`,
328
+ health: healthStatus,
329
+ };
330
+ }
277
331
  // Timelock guard (allows existing chats, blocks new contacts)
278
332
  const timelockDecision = this.timelockGuard.canSend(recipient);
279
333
  if (!timelockDecision.allowed) {
@@ -345,6 +399,22 @@ export class AntiBan {
345
399
  health: healthStatus,
346
400
  };
347
401
  }
402
+ // Cross-instance coordination — check shared IP-level pool
403
+ if (this.instanceCoordinator) {
404
+ const slot = this.instanceCoordinator.tryAcquireSlot();
405
+ if (!slot.allowed) {
406
+ this.stats.messagesBlocked++;
407
+ if (this.logging) {
408
+ console.log(`[baileys-antiban] 🌐 BLOCKED — instance pool exhausted (shared IP limit), retry in ${slot.retryAfterMs}ms`);
409
+ }
410
+ return {
411
+ allowed: false,
412
+ delayMs: slot.retryAfterMs || 5000,
413
+ reason: 'Cross-instance rate pool exhausted',
414
+ health: healthStatus,
415
+ };
416
+ }
417
+ }
348
418
  // Group profile rate check (runs before rateLimiter.getDelay for timing)
349
419
  if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
350
420
  const groupLimits = applyGroupMultiplier({
@@ -384,6 +454,24 @@ export class AntiBan {
384
454
  const multiplier = Math.min(5, 1 / activityFactor);
385
455
  delay = Math.floor(delay * multiplier);
386
456
  }
457
+ // Per-contact risk multiplier — cold contacts need longer delays
458
+ // Only apply when contact graph is enabled, otherwise all contacts appear as 'stranger'
459
+ if (this.contactGraphWarmer['config']?.enabled) {
460
+ const contactState = this.contactGraphWarmer.getContactState(recipient);
461
+ const coldMultiplier = {
462
+ stranger: 2.5,
463
+ handshake_sent: 1.8,
464
+ handshake_complete: 1.3,
465
+ known: 1.0,
466
+ };
467
+ const contactRiskMult = coldMultiplier[contactState] ?? 1.0;
468
+ if (contactRiskMult > 1.0) {
469
+ delay = Math.floor(delay * contactRiskMult);
470
+ if (this.logging && contactRiskMult >= 2.0) {
471
+ console.log(`[baileys-antiban] ⚠️ Cold contact ${recipient} — ${contactState}, delay ×${contactRiskMult}`);
472
+ }
473
+ }
474
+ }
387
475
  // Roll for distraction pause
388
476
  const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
389
477
  if (distractionCheck.pause) {
@@ -411,11 +499,15 @@ export class AntiBan {
411
499
  * Record a successfully sent message.
412
500
  * Call this AFTER every successful sendMessage().
413
501
  */
414
- afterSend(recipient, content) {
502
+ afterSend(recipient, content, msgId) {
415
503
  this.rateLimiter.record(recipient, content);
416
504
  this.warmUp.record();
417
505
  this.replyRatioGuard.recordSent(recipient);
418
506
  this.stats.messagesAllowed++;
507
+ if (msgId) {
508
+ this.deliveryTracker.onMessageSent(msgId);
509
+ }
510
+ this.runAdaptiveCheck();
419
511
  this.persistStateDebounced();
420
512
  }
421
513
  /**
@@ -441,6 +533,7 @@ export class AntiBan {
441
533
  onReconnect() {
442
534
  this.health.recordReconnect();
443
535
  this.reconnectThrottleModule.onReconnect();
536
+ this.rateLimiter.adaptLimits(1.0);
444
537
  }
445
538
  /**
446
539
  * Handle incoming message — record in reply ratio + contact graph.
@@ -451,6 +544,13 @@ export class AntiBan {
451
544
  this.contactGraphWarmer.onIncomingMessage(jid);
452
545
  return this.replyRatioGuard.suggestReply(jid, msgText);
453
546
  }
547
+ /**
548
+ * Record a delivery receipt (status 3 = DELIVERY_ACK, status 4 = READ).
549
+ * Call from messages.update handler when delivery status is received.
550
+ */
551
+ onDeliveryReceipt(msgId) {
552
+ this.deliveryTracker.onDeliveryReceipt(msgId);
553
+ }
454
554
  /**
455
555
  * Get the resolved configuration
456
556
  */
@@ -466,6 +566,8 @@ export class AntiBan {
466
566
  health: this.health.getStatus(),
467
567
  warmUp: this.warmUp.getStatus(),
468
568
  rateLimiter: this.rateLimiter.getStats(),
569
+ banRecovery: this.banRecovery.getStatus(),
570
+ deliveryTracker: this.deliveryTracker.getStats(),
469
571
  };
470
572
  // Only include new stats if enabled
471
573
  if (this.replyRatioGuard['config']?.enabled) {
@@ -492,6 +594,9 @@ export class AntiBan {
492
594
  if (this.sessionStabilityMonitor) {
493
595
  stats.sessionStability = this.sessionStabilityMonitor.getStats();
494
596
  }
597
+ if (this.instanceCoordinator) {
598
+ stats.instanceCoordinator = this.instanceCoordinator.getStats();
599
+ }
495
600
  return stats;
496
601
  }
497
602
  /** Get the timelock guard for direct access */
@@ -530,6 +635,10 @@ export class AntiBan {
530
635
  get sessionStability() {
531
636
  return this.sessionStabilityMonitor;
532
637
  }
638
+ /** Get the ban recovery orchestrator for direct access */
639
+ get recoveryOrchestrator() {
640
+ return this.banRecovery;
641
+ }
533
642
  /**
534
643
  * Export warm-up state for persistence between restarts
535
644
  */
@@ -571,6 +680,35 @@ export class AntiBan {
571
680
  console.log('[baileys-antiban] 🔄 Reset — starting fresh warm-up');
572
681
  }
573
682
  }
683
+ runAdaptiveCheck() {
684
+ const delivery = this.deliveryTracker.getStats();
685
+ // Need min sample to be meaningful
686
+ if (delivery.deliveryRate === null)
687
+ return;
688
+ const rate = delivery.deliveryRate;
689
+ let targetFactor;
690
+ if (rate >= 0.85) {
691
+ targetFactor = 1.0; // Excellent — full speed
692
+ }
693
+ else if (rate >= 0.70) {
694
+ targetFactor = 0.75; // Good — slight reduction
695
+ }
696
+ else if (rate >= 0.55) {
697
+ targetFactor = 0.50; // Poor — halve throughput
698
+ }
699
+ else {
700
+ targetFactor = 0.25; // Bad — severe throttle (soft ban likely)
701
+ }
702
+ const current = this.rateLimiter.getCurrentFactor();
703
+ // Only log + adjust when factor changes meaningfully (>5% delta)
704
+ if (Math.abs(targetFactor - current) > 0.05) {
705
+ if (this.logging) {
706
+ const dir = targetFactor > current ? '📈' : '📉';
707
+ console.log(`[baileys-antiban] ${dir} Adaptive rate: delivery=${(rate * 100).toFixed(0)}% → factor ${current.toFixed(2)}→${targetFactor.toFixed(2)}`);
708
+ }
709
+ this.rateLimiter.adaptLimits(targetFactor);
710
+ }
711
+ }
574
712
  persistStateDebounced() {
575
713
  if (!this.stateManager)
576
714
  return;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * BanRecoveryOrchestrator — Structured recovery after ban/restriction events
3
+ *
4
+ * When WhatsApp restricts your account, the worst thing to do is immediately
5
+ * resume normal activity. This module provides a evidence-based recovery protocol:
6
+ *
7
+ * - Timelocked (reachout_restricted): 24h pause, resume at 10% rate, ramp 15%/week
8
+ * - Rate-overlimit (429): 4h pause, resume at 25% rate, ramp 25%/week
9
+ * - Soft-ban (repeated disconnects): 48h pause, resume at 5% rate, ramp 10%/week
10
+ * - Hard-ban (loggedOut): account is dead, signal for replacement
11
+ *
12
+ * Based on observed recovery times from community reports. Not guaranteed —
13
+ * WA's enforcement is non-deterministic. Treat as best-effort guidance.
14
+ */
15
+ export type BanEventType = 'timelock' | 'rate_overlimit' | 'soft_ban' | 'hard_ban';
16
+ export type RecoveryPhase = 'paused' | 'recovering' | 'ramping' | 'graduated' | 'dead';
17
+ export interface RecoveryPlan {
18
+ eventType: BanEventType;
19
+ pauseDurationMs: number;
20
+ resumeRateMultiplier: number;
21
+ weeklyRampPercent: number;
22
+ estimatedRecoveryDays: number;
23
+ description: string;
24
+ }
25
+ export interface BanRecoveryConfig {
26
+ /** Custom recovery plans per event type (overrides defaults) */
27
+ plans?: Partial<Record<BanEventType, Partial<RecoveryPlan>>>;
28
+ /** Called when recovery phase changes */
29
+ onPhaseChange?: (phase: RecoveryPhase, plan: RecoveryPlan) => void;
30
+ /** Called when account appears dead (hard ban) — signal to replace SIM */
31
+ onHardBan?: () => void;
32
+ /** Max weeks before giving up on recovery and declaring dead (default: 8) */
33
+ maxRecoveryWeeks?: number;
34
+ }
35
+ export interface RecoveryState {
36
+ active: boolean;
37
+ eventType?: BanEventType;
38
+ phase: RecoveryPhase;
39
+ banDetectedAt?: number;
40
+ pauseUntil?: number;
41
+ currentRateMultiplier: number;
42
+ weeksSinceResume: number;
43
+ banCount30d: number;
44
+ lastBanAt?: number;
45
+ }
46
+ export interface RecoveryStatus {
47
+ phase: RecoveryPhase;
48
+ rateMultiplier: number;
49
+ pauseRemainingMs?: number;
50
+ estimatedFullRecoveryDate?: number;
51
+ recommendation: string;
52
+ shouldReplaceNumber: boolean;
53
+ }
54
+ export declare class BanRecoveryOrchestrator {
55
+ private config;
56
+ private state;
57
+ private plans;
58
+ constructor(config?: BanRecoveryConfig);
59
+ /**
60
+ * Record a ban event and start recovery protocol
61
+ */
62
+ recordBanEvent(eventType: BanEventType): RecoveryStatus;
63
+ /**
64
+ * Get current recovery status (call before sending to check rate)
65
+ */
66
+ getStatus(): RecoveryStatus;
67
+ /**
68
+ * Current rate multiplier — multiply your normal limits by this
69
+ */
70
+ getRateMultiplier(): number;
71
+ /**
72
+ * Should be called daily/weekly to advance the ramp
73
+ */
74
+ tick(): void;
75
+ /**
76
+ * Classify a raw error into a BanEventType
77
+ */
78
+ static classifyError(err: unknown): BanEventType | null;
79
+ /**
80
+ * Serializable state for persistence
81
+ */
82
+ getState(): RecoveryState;
83
+ /**
84
+ * Restore from persisted state
85
+ */
86
+ static fromState(state: RecoveryState, config?: BanRecoveryConfig): BanRecoveryOrchestrator;
87
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * BanRecoveryOrchestrator — Structured recovery after ban/restriction events
3
+ *
4
+ * When WhatsApp restricts your account, the worst thing to do is immediately
5
+ * resume normal activity. This module provides a evidence-based recovery protocol:
6
+ *
7
+ * - Timelocked (reachout_restricted): 24h pause, resume at 10% rate, ramp 15%/week
8
+ * - Rate-overlimit (429): 4h pause, resume at 25% rate, ramp 25%/week
9
+ * - Soft-ban (repeated disconnects): 48h pause, resume at 5% rate, ramp 10%/week
10
+ * - Hard-ban (loggedOut): account is dead, signal for replacement
11
+ *
12
+ * Based on observed recovery times from community reports. Not guaranteed —
13
+ * WA's enforcement is non-deterministic. Treat as best-effort guidance.
14
+ */
15
+ const DEFAULT_PLANS = {
16
+ timelock: {
17
+ eventType: 'timelock',
18
+ pauseDurationMs: 24 * 60 * 60 * 1000, // 24 hours
19
+ resumeRateMultiplier: 0.10, // 10% of normal
20
+ weeklyRampPercent: 15, // +15% per week
21
+ estimatedRecoveryDays: 14,
22
+ description: 'WA reachout timelock — 24h pause then slow ramp',
23
+ },
24
+ rate_overlimit: {
25
+ eventType: 'rate_overlimit',
26
+ pauseDurationMs: 4 * 60 * 60 * 1000, // 4 hours
27
+ resumeRateMultiplier: 0.25, // 25% of normal
28
+ weeklyRampPercent: 25, // +25% per week
29
+ estimatedRecoveryDays: 7,
30
+ description: 'Rate limit hit — 4h pause then moderate ramp',
31
+ },
32
+ soft_ban: {
33
+ eventType: 'soft_ban',
34
+ pauseDurationMs: 48 * 60 * 60 * 1000, // 48 hours
35
+ resumeRateMultiplier: 0.05, // 5% of normal
36
+ weeklyRampPercent: 10, // +10% per week
37
+ estimatedRecoveryDays: 21,
38
+ description: 'Soft ban detected — 48h pause then very slow ramp',
39
+ },
40
+ hard_ban: {
41
+ eventType: 'hard_ban',
42
+ pauseDurationMs: Infinity,
43
+ resumeRateMultiplier: 0,
44
+ weeklyRampPercent: 0,
45
+ estimatedRecoveryDays: Infinity,
46
+ description: 'Hard ban — number is dead, replace SIM',
47
+ },
48
+ };
49
+ export class BanRecoveryOrchestrator {
50
+ config;
51
+ state;
52
+ plans;
53
+ constructor(config = {}) {
54
+ this.config = {
55
+ plans: config.plans || {},
56
+ onPhaseChange: config.onPhaseChange || (() => { }),
57
+ onHardBan: config.onHardBan || (() => { }),
58
+ maxRecoveryWeeks: config.maxRecoveryWeeks ?? 8,
59
+ };
60
+ // Merge custom plans with defaults
61
+ this.plans = { ...DEFAULT_PLANS };
62
+ if (config.plans) {
63
+ for (const eventType in config.plans) {
64
+ const customPlan = config.plans[eventType];
65
+ if (customPlan) {
66
+ this.plans[eventType] = {
67
+ ...this.plans[eventType],
68
+ ...customPlan,
69
+ };
70
+ }
71
+ }
72
+ }
73
+ this.state = {
74
+ active: false,
75
+ phase: 'graduated',
76
+ currentRateMultiplier: 1.0,
77
+ weeksSinceResume: 0,
78
+ banCount30d: 0,
79
+ };
80
+ }
81
+ /**
82
+ * Record a ban event and start recovery protocol
83
+ */
84
+ recordBanEvent(eventType) {
85
+ const now = Date.now();
86
+ const plan = this.plans[eventType];
87
+ // Update ban count (reset if >30 days since last ban)
88
+ if (this.state.lastBanAt) {
89
+ const daysSinceLastBan = (now - this.state.lastBanAt) / (86400000);
90
+ if (daysSinceLastBan > 30) {
91
+ this.state.banCount30d = 0;
92
+ }
93
+ }
94
+ this.state.banCount30d++;
95
+ this.state.lastBanAt = now;
96
+ // Upgrade to hard_ban if 3+ bans in 30 days (unless already hard_ban)
97
+ if (this.state.banCount30d >= 3 && eventType !== 'hard_ban') {
98
+ eventType = 'hard_ban';
99
+ }
100
+ this.state.active = true;
101
+ this.state.eventType = eventType;
102
+ this.state.banDetectedAt = now;
103
+ this.state.pauseUntil = plan.pauseDurationMs === Infinity ? Infinity : now + plan.pauseDurationMs;
104
+ this.state.currentRateMultiplier = plan.resumeRateMultiplier;
105
+ this.state.weeksSinceResume = 0;
106
+ this.state.phase = eventType === 'hard_ban' ? 'dead' : 'paused';
107
+ this.config.onPhaseChange(this.state.phase, plan);
108
+ if (eventType === 'hard_ban') {
109
+ this.config.onHardBan();
110
+ }
111
+ return this.getStatus();
112
+ }
113
+ /**
114
+ * Get current recovery status (call before sending to check rate)
115
+ */
116
+ getStatus() {
117
+ if (!this.state.active || !this.state.eventType) {
118
+ return {
119
+ phase: 'graduated',
120
+ rateMultiplier: 1.0,
121
+ recommendation: 'No active recovery — operating normally',
122
+ shouldReplaceNumber: false,
123
+ };
124
+ }
125
+ const now = Date.now();
126
+ const plan = this.plans[this.state.eventType];
127
+ // Check if pause period has ended
128
+ if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
129
+ this.state.phase = 'recovering';
130
+ this.config.onPhaseChange(this.state.phase, plan);
131
+ }
132
+ // Check if we've exceeded max recovery weeks (give up)
133
+ if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
134
+ if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
135
+ this.state.phase = 'dead';
136
+ this.config.onPhaseChange(this.state.phase, plan);
137
+ this.config.onHardBan();
138
+ }
139
+ }
140
+ const shouldReplaceNumber = this.state.phase === 'dead' || this.state.banCount30d >= 3;
141
+ let recommendation;
142
+ switch (this.state.phase) {
143
+ case 'dead':
144
+ recommendation = 'Account is permanently restricted. Replace number and start fresh.';
145
+ break;
146
+ case 'paused':
147
+ if (this.state.pauseUntil === Infinity) {
148
+ recommendation = 'Account is dead. Do not attempt to send messages.';
149
+ }
150
+ else {
151
+ const remainingMs = this.state.pauseUntil - now;
152
+ const remainingHours = Math.ceil(remainingMs / 3600000);
153
+ recommendation = `Pause period active. Wait ${remainingHours}h before resuming. ${plan.description}`;
154
+ }
155
+ break;
156
+ case 'recovering':
157
+ recommendation = `Recovery phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
158
+ break;
159
+ case 'ramping':
160
+ recommendation = `Ramping phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
161
+ break;
162
+ case 'graduated':
163
+ recommendation = 'Recovery complete. Operating at full capacity.';
164
+ break;
165
+ }
166
+ const pauseRemainingMs = this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now < this.state.pauseUntil
167
+ ? this.state.pauseUntil - now
168
+ : undefined;
169
+ let estimatedFullRecoveryDate;
170
+ if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
171
+ const rateToGain = 1.0 - this.state.currentRateMultiplier;
172
+ const weeksNeeded = Math.ceil((rateToGain / (plan.weeklyRampPercent / 100)) * (1 / this.state.currentRateMultiplier));
173
+ estimatedFullRecoveryDate = now + weeksNeeded * 7 * 86400000;
174
+ }
175
+ return {
176
+ phase: this.state.phase,
177
+ rateMultiplier: this.state.currentRateMultiplier,
178
+ pauseRemainingMs,
179
+ estimatedFullRecoveryDate,
180
+ recommendation,
181
+ shouldReplaceNumber,
182
+ };
183
+ }
184
+ /**
185
+ * Current rate multiplier — multiply your normal limits by this
186
+ */
187
+ getRateMultiplier() {
188
+ return this.state.currentRateMultiplier;
189
+ }
190
+ /**
191
+ * Should be called daily/weekly to advance the ramp
192
+ */
193
+ tick() {
194
+ if (!this.state.active || !this.state.eventType) {
195
+ return;
196
+ }
197
+ const now = Date.now();
198
+ const plan = this.plans[this.state.eventType];
199
+ // Transition from paused to recovering
200
+ if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
201
+ this.state.phase = 'recovering';
202
+ this.config.onPhaseChange(this.state.phase, plan);
203
+ }
204
+ // Advance the ramp if in recovering/ramping phase
205
+ if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
206
+ this.state.weeksSinceResume++;
207
+ // Check if exceeded max recovery weeks
208
+ if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
209
+ this.state.phase = 'dead';
210
+ this.state.currentRateMultiplier = 0;
211
+ this.config.onPhaseChange(this.state.phase, plan);
212
+ this.config.onHardBan();
213
+ return;
214
+ }
215
+ // Apply weekly ramp
216
+ const rampMultiplier = 1 + (plan.weeklyRampPercent / 100);
217
+ const newMultiplier = this.state.currentRateMultiplier * rampMultiplier;
218
+ if (newMultiplier >= 1.0) {
219
+ // Graduated
220
+ this.state.currentRateMultiplier = 1.0;
221
+ this.state.phase = 'graduated';
222
+ this.state.active = false;
223
+ this.config.onPhaseChange(this.state.phase, plan);
224
+ }
225
+ else {
226
+ // Still ramping
227
+ this.state.currentRateMultiplier = newMultiplier;
228
+ this.state.phase = 'ramping';
229
+ }
230
+ }
231
+ }
232
+ /**
233
+ * Classify a raw error into a BanEventType
234
+ */
235
+ static classifyError(err) {
236
+ if (!err)
237
+ return null;
238
+ const errorStr = String(err).toLowerCase();
239
+ const errorMsg = err.message?.toLowerCase() || '';
240
+ const errorCode = err.code || '';
241
+ // Timelock patterns
242
+ if (errorStr.includes('reachout') ||
243
+ errorStr.includes('account_reachout_restricted') ||
244
+ errorMsg.includes('reachout') ||
245
+ errorCode === 463 ||
246
+ errorCode === '463') {
247
+ return 'timelock';
248
+ }
249
+ // Rate overlimit patterns
250
+ if (errorStr.includes('rate-overlimit') ||
251
+ errorStr.includes('rate_overlimit') ||
252
+ errorStr.includes('429') ||
253
+ errorCode === 429 ||
254
+ errorCode === '429') {
255
+ return 'rate_overlimit';
256
+ }
257
+ // Hard ban patterns
258
+ if (errorStr.includes('loggedout') ||
259
+ errorStr.includes('logged_out') ||
260
+ errorStr.includes('logged out') ||
261
+ errorMsg.includes('loggedout') ||
262
+ errorCode === 401 ||
263
+ errorCode === '401') {
264
+ return 'hard_ban';
265
+ }
266
+ // Note: soft_ban detection requires context (multiple disconnects)
267
+ // and should be determined by the caller using HealthMonitor
268
+ return null;
269
+ }
270
+ /**
271
+ * Serializable state for persistence
272
+ */
273
+ getState() {
274
+ return { ...this.state };
275
+ }
276
+ /**
277
+ * Restore from persisted state
278
+ */
279
+ static fromState(state, config) {
280
+ const orchestrator = new BanRecoveryOrchestrator(config);
281
+ orchestrator.state = { ...state };
282
+ return orchestrator;
283
+ }
284
+ }