@prototypevip/baileys 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +132 -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,373 @@
1
+ /**
2
+ * ReputationVoucher — High-trust accounts vouch for new numbers
3
+ *
4
+ * Establishes genuine conversation history between trusted accounts and new numbers
5
+ * before those new numbers contact customers. Reduces warmup time and makes new
6
+ * accounts appear established with real bidirectional message history.
7
+ *
8
+ * Key design principles:
9
+ * - Dedicated sacrificial vouch accounts (separate from business accounts)
10
+ * - Max 5 vouches per week per vouching account (not per day)
11
+ * - Targets must complete 3 qualifying events before vouching
12
+ * - Strike system: 3 failed vouches = 90-day suspension for voucher
13
+ * - Blast radius containment: vouching accounts isolated from main business
14
+ *
15
+ * Usage:
16
+ * const rv = new ReputationVoucher();
17
+ *
18
+ * // Register a trusted account
19
+ * rv.registerVoucher({
20
+ * jid: '27123456789@s.whatsapp.net',
21
+ * trustScore: 85,
22
+ * accountAgeDays: 240
23
+ * });
24
+ *
25
+ * // Queue a new number for vouching
26
+ * rv.queueTarget({ jid: '27987654321@s.whatsapp.net' });
27
+ *
28
+ * // Record qualifying events (auction completion, payment, etc)
29
+ * rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
30
+ * rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
31
+ * rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
32
+ *
33
+ * // Check if target qualifies
34
+ * const check = rv.targetQualifies('27987654321@s.whatsapp.net');
35
+ * if (check.qualified) {
36
+ * const voucher = rv.getAvailableVoucher();
37
+ * if (voucher) {
38
+ * // Plan the conversation
39
+ * const conversation = rv.planVouchConversation(voucher.jid, '27987654321@s.whatsapp.net');
40
+ *
41
+ * // Execute sends (caller must have separate socket for voucher account)
42
+ * // ... send conversation.messages ...
43
+ *
44
+ * // After target replies
45
+ * rv.recordVouchOutcome('27987654321@s.whatsapp.net', true);
46
+ *
47
+ * // Calculate warmup credit
48
+ * const daysCredit = rv.calculateWarmupCredit('27987654321@s.whatsapp.net');
49
+ * // Skip daysCredit days of warmup for this target
50
+ * }
51
+ * }
52
+ */
53
+ const DEFAULT_CONFIG = {
54
+ maxVouchesPerWeek: 5,
55
+ qualifyingEventsRequired: 3,
56
+ strikesForSuspension: 3,
57
+ suspensionDurationMs: 90 * 24 * 60 * 60 * 1000, // 90 days
58
+ minVoucherTrustScore: 60,
59
+ minVoucherAgeDays: 180, // 6 months
60
+ warmupMessages: [
61
+ "Hey, just checking this is the right number?",
62
+ "Hi! Got your details from the group",
63
+ "Morning! Are you available this week?",
64
+ "Hey there, hope this is a good time to connect",
65
+ "Hi, just wanted to reach out",
66
+ ],
67
+ };
68
+ export class ReputationVoucher {
69
+ config;
70
+ vouchers = new Map();
71
+ targets = new Map();
72
+ conversations = [];
73
+ constructor(config) {
74
+ this.config = { ...DEFAULT_CONFIG, ...config };
75
+ }
76
+ /**
77
+ * Register a vouching account (high-trust, established number)
78
+ */
79
+ registerVoucher(account) {
80
+ // Validate account meets minimum requirements
81
+ if (account.trustScore < this.config.minVoucherTrustScore) {
82
+ throw new Error(`Voucher trust score ${account.trustScore} below minimum ${this.config.minVoucherTrustScore}`);
83
+ }
84
+ if (account.accountAgeDays < this.config.minVoucherAgeDays) {
85
+ throw new Error(`Voucher account age ${account.accountAgeDays} days below minimum ${this.config.minVoucherAgeDays} days`);
86
+ }
87
+ const existing = this.vouchers.get(account.jid);
88
+ if (existing) {
89
+ // Update existing voucher (preserve counters)
90
+ this.vouchers.set(account.jid, {
91
+ ...account,
92
+ vouchesThisWeek: existing.vouchesThisWeek,
93
+ totalVouches: existing.totalVouches,
94
+ failedVouches: existing.failedVouches,
95
+ strikes: existing.strikes,
96
+ suspendedUntil: existing.suspendedUntil,
97
+ lastVouchAt: existing.lastVouchAt,
98
+ });
99
+ }
100
+ else {
101
+ // New voucher
102
+ this.vouchers.set(account.jid, {
103
+ ...account,
104
+ vouchesThisWeek: 0,
105
+ totalVouches: 0,
106
+ failedVouches: 0,
107
+ strikes: 0,
108
+ });
109
+ }
110
+ }
111
+ /**
112
+ * Queue a target for vouching
113
+ */
114
+ queueTarget(target) {
115
+ const existing = this.targets.get(target.jid);
116
+ if (existing && existing.status !== 'failed') {
117
+ // Already queued or in progress
118
+ return;
119
+ }
120
+ this.targets.set(target.jid, {
121
+ ...target,
122
+ qualifyingEvents: target.qualifyingEvents || 0,
123
+ requestedAt: Date.now(),
124
+ status: 'pending',
125
+ });
126
+ }
127
+ /**
128
+ * Check if a target qualifies for vouching
129
+ */
130
+ targetQualifies(jid) {
131
+ const target = this.targets.get(jid);
132
+ if (!target) {
133
+ return { qualified: false, reason: 'Target not queued', eventsNeeded: this.config.qualifyingEventsRequired };
134
+ }
135
+ const events = target.qualifyingEvents || 0;
136
+ if (events < this.config.qualifyingEventsRequired) {
137
+ return {
138
+ qualified: false,
139
+ reason: `Not enough qualifying events (${events}/${this.config.qualifyingEventsRequired})`,
140
+ eventsNeeded: this.config.qualifyingEventsRequired - events,
141
+ };
142
+ }
143
+ if (target.status === 'completed') {
144
+ return { qualified: false, reason: 'Already vouched' };
145
+ }
146
+ if (target.status === 'failed') {
147
+ return { qualified: false, reason: 'Previous vouch failed' };
148
+ }
149
+ return { qualified: true };
150
+ }
151
+ /**
152
+ * Record a qualifying event for a target (e.g., auction completion, payment cleared)
153
+ */
154
+ recordQualifyingEvent(jid) {
155
+ const target = this.targets.get(jid);
156
+ if (!target) {
157
+ // Auto-queue with first event
158
+ this.queueTarget({ jid, qualifyingEvents: 1 });
159
+ return;
160
+ }
161
+ target.qualifyingEvents = (target.qualifyingEvents || 0) + 1;
162
+ }
163
+ /**
164
+ * Get next available voucher for a target (respects limits + suspension)
165
+ */
166
+ getAvailableVoucher() {
167
+ const now = Date.now();
168
+ const weekMs = 7 * 24 * 60 * 60 * 1000;
169
+ // Find eligible vouchers
170
+ const eligible = [];
171
+ for (const voucher of this.vouchers.values()) {
172
+ // Check suspension
173
+ if (voucher.suspendedUntil && voucher.suspendedUntil > now) {
174
+ continue;
175
+ }
176
+ // Reset weekly counter if 7 days have passed since last vouch
177
+ if (voucher.lastVouchAt && now - voucher.lastVouchAt > weekMs) {
178
+ voucher.vouchesThisWeek = 0;
179
+ }
180
+ // Check weekly limit
181
+ if (voucher.vouchesThisWeek >= this.config.maxVouchesPerWeek) {
182
+ continue;
183
+ }
184
+ eligible.push(voucher);
185
+ }
186
+ if (eligible.length === 0) {
187
+ return null;
188
+ }
189
+ // Sort by:
190
+ // 1. Lowest vouchesThisWeek (spread load)
191
+ // 2. Highest trustScore (best vouchers first)
192
+ // 3. Lowest totalVouches (rotate usage)
193
+ eligible.sort((a, b) => {
194
+ if (a.vouchesThisWeek !== b.vouchesThisWeek) {
195
+ return a.vouchesThisWeek - b.vouchesThisWeek;
196
+ }
197
+ if (a.trustScore !== b.trustScore) {
198
+ return b.trustScore - a.trustScore;
199
+ }
200
+ return a.totalVouches - b.totalVouches;
201
+ });
202
+ return eligible[0];
203
+ }
204
+ /**
205
+ * Plan a vouch conversation.
206
+ * Returns a conversation plan — caller executes the sends.
207
+ */
208
+ planVouchConversation(voucherJid, targetJid) {
209
+ const voucher = this.vouchers.get(voucherJid);
210
+ if (!voucher) {
211
+ throw new Error(`Voucher ${voucherJid} not registered`);
212
+ }
213
+ const target = this.targets.get(targetJid);
214
+ if (!target) {
215
+ throw new Error(`Target ${targetJid} not queued`);
216
+ }
217
+ const check = this.targetQualifies(targetJid);
218
+ if (!check.qualified) {
219
+ throw new Error(`Target ${targetJid} not qualified: ${check.reason}`);
220
+ }
221
+ // Update voucher counters
222
+ voucher.vouchesThisWeek++;
223
+ voucher.totalVouches++;
224
+ voucher.lastVouchAt = Date.now();
225
+ // Update target status
226
+ target.status = 'active';
227
+ target.vouchedAt = Date.now();
228
+ target.vouchedBy = voucherJid;
229
+ // Pick 2-3 warmup messages randomly
230
+ const messageCount = 2 + Math.floor(Math.random() * 2); // 2-3 messages
231
+ const selectedMessages = [];
232
+ const pool = [...this.config.warmupMessages];
233
+ for (let i = 0; i < messageCount && pool.length > 0; i++) {
234
+ const idx = Math.floor(Math.random() * pool.length);
235
+ selectedMessages.push(pool[idx]);
236
+ pool.splice(idx, 1); // Remove to avoid duplicates
237
+ }
238
+ // Create conversation plan
239
+ const conversation = {
240
+ targetJid,
241
+ voucherJid,
242
+ messages: selectedMessages.map((text, idx) => ({
243
+ direction: 'outbound',
244
+ timestamp: Date.now() + idx * 5000, // Stagger by 5s
245
+ text,
246
+ })),
247
+ startedAt: Date.now(),
248
+ success: false, // Will be updated when recordVouchOutcome is called
249
+ };
250
+ this.conversations.push(conversation);
251
+ return conversation;
252
+ }
253
+ /**
254
+ * Record outcome of a vouch.
255
+ * success = true if target replied within reasonable time (e.g., 7 days)
256
+ * success = false if target got banned within 7 days of vouch
257
+ */
258
+ recordVouchOutcome(targetJid, success) {
259
+ const target = this.targets.get(targetJid);
260
+ if (!target || !target.vouchedBy) {
261
+ throw new Error(`Target ${targetJid} not vouched`);
262
+ }
263
+ const voucher = this.vouchers.get(target.vouchedBy);
264
+ if (!voucher) {
265
+ throw new Error(`Voucher ${target.vouchedBy} not found`);
266
+ }
267
+ // Find the conversation
268
+ const conversation = this.conversations.find((c) => c.targetJid === targetJid && c.voucherJid === target.vouchedBy);
269
+ if (conversation) {
270
+ conversation.success = success;
271
+ conversation.completedAt = Date.now();
272
+ }
273
+ if (success) {
274
+ target.status = 'completed';
275
+ }
276
+ else {
277
+ // Failed vouch — apply strike to voucher
278
+ target.status = 'failed';
279
+ voucher.failedVouches++;
280
+ voucher.strikes++;
281
+ // Check if suspension threshold reached
282
+ if (voucher.strikes >= this.config.strikesForSuspension) {
283
+ voucher.suspendedUntil = Date.now() + this.config.suspensionDurationMs;
284
+ }
285
+ }
286
+ }
287
+ /**
288
+ * Calculate warmup credit for a successfully vouched target.
289
+ * Returns number of warmup days that can be skipped (0-3).
290
+ *
291
+ * Credit logic:
292
+ * - 1 reply = 1 day credit
293
+ * - 2+ replies = 2 days credit
294
+ * - Reply + 3+ days elapsed = 3 days credit (max)
295
+ */
296
+ calculateWarmupCredit(targetJid) {
297
+ const target = this.targets.get(targetJid);
298
+ if (!target || target.status !== 'completed') {
299
+ return 0;
300
+ }
301
+ const conversation = this.conversations.find((c) => c.targetJid === targetJid && c.success);
302
+ if (!conversation) {
303
+ return 0;
304
+ }
305
+ // Count inbound messages (replies from target)
306
+ const inboundCount = conversation.messages.filter((m) => m.direction === 'inbound').length;
307
+ if (inboundCount === 0) {
308
+ return 0;
309
+ }
310
+ if (inboundCount === 1) {
311
+ return 1;
312
+ }
313
+ if (inboundCount >= 2) {
314
+ // Check if 3+ days elapsed
315
+ const daysElapsed = (Date.now() - conversation.startedAt) / (24 * 60 * 60 * 1000);
316
+ if (daysElapsed >= 3) {
317
+ return 3; // Max credit
318
+ }
319
+ return 2;
320
+ }
321
+ return 0;
322
+ }
323
+ /**
324
+ * Get stats for a specific voucher
325
+ */
326
+ getVoucherStats(jid) {
327
+ const voucher = this.vouchers.get(jid);
328
+ return voucher ? { ...voucher } : null;
329
+ }
330
+ /**
331
+ * Get all pending targets (awaiting vouching)
332
+ */
333
+ getPendingTargets() {
334
+ return Array.from(this.targets.values()).filter((t) => t.status === 'pending');
335
+ }
336
+ /**
337
+ * Get vouch history
338
+ */
339
+ getVouchHistory() {
340
+ return [...this.conversations];
341
+ }
342
+ /**
343
+ * Export state for persistence
344
+ */
345
+ exportState() {
346
+ return {
347
+ version: 1,
348
+ exportedAt: Date.now(),
349
+ vouchers: Array.from(this.vouchers.values()),
350
+ targets: Array.from(this.targets.values()),
351
+ conversations: [...this.conversations],
352
+ };
353
+ }
354
+ /**
355
+ * Import state from persistence
356
+ */
357
+ importState(state) {
358
+ // Clear existing state
359
+ this.vouchers.clear();
360
+ this.targets.clear();
361
+ this.conversations = [];
362
+ // Import vouchers
363
+ for (const voucher of state.vouchers) {
364
+ this.vouchers.set(voucher.jid, { ...voucher });
365
+ }
366
+ // Import targets
367
+ for (const target of state.targets) {
368
+ this.targets.set(target.jid, { ...target });
369
+ }
370
+ // Import conversations
371
+ this.conversations = [...state.conversations];
372
+ }
373
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * TypedMessageRetryReason — Typed enum for WhatsApp's retry reason codes
3
+ *
4
+ * Based on protocol research from whatsapp-rust and Baileys source.
5
+ * These codes appear in message retry events when encryption fails.
6
+ *
7
+ * Common scenarios:
8
+ * - SignalErrorBadMac (7) — Most common, indicates encryption session mismatch
9
+ * - SignalErrorNoSession (5) — Peer hasn't established session yet
10
+ * - SignalErrorInvalidKeyId (3) — Peer's prekey rotated
11
+ * - MessageExpired (8) — Message too old to decrypt
12
+ *
13
+ * @author Kobus Wentzel <kobie@pop.co.za>
14
+ * @license MIT
15
+ */
16
+ /**
17
+ * WhatsApp message retry reason codes.
18
+ * Based on Signal protocol error codes + WhatsApp extensions.
19
+ */
20
+ export var MessageRetryReason;
21
+ (function (MessageRetryReason) {
22
+ MessageRetryReason[MessageRetryReason["UnknownError"] = 0] = "UnknownError";
23
+ MessageRetryReason[MessageRetryReason["GenericError"] = 1] = "GenericError";
24
+ MessageRetryReason[MessageRetryReason["SignalErrorInvalidKeyId"] = 3] = "SignalErrorInvalidKeyId";
25
+ MessageRetryReason[MessageRetryReason["SignalErrorInvalidMessage"] = 4] = "SignalErrorInvalidMessage";
26
+ MessageRetryReason[MessageRetryReason["SignalErrorNoSession"] = 5] = "SignalErrorNoSession";
27
+ MessageRetryReason[MessageRetryReason["SignalErrorBadMac"] = 7] = "SignalErrorBadMac";
28
+ MessageRetryReason[MessageRetryReason["MessageExpired"] = 8] = "MessageExpired";
29
+ MessageRetryReason[MessageRetryReason["DecryptionError"] = 9] = "DecryptionError";
30
+ })(MessageRetryReason || (MessageRetryReason = {}));
31
+ /**
32
+ * Set of retry reasons that indicate MAC verification failure.
33
+ * These are the most common causes of "Bad MAC" errors in Baileys.
34
+ */
35
+ export const MAC_ERROR_CODES = new Set([
36
+ MessageRetryReason.SignalErrorBadMac,
37
+ MessageRetryReason.SignalErrorInvalidMessage,
38
+ MessageRetryReason.SignalErrorNoSession,
39
+ MessageRetryReason.SignalErrorInvalidKeyId,
40
+ ]);
41
+ /**
42
+ * Parse a retry reason code from various input formats.
43
+ * Returns UnknownError if code is not recognized.
44
+ */
45
+ export function parseRetryReason(code) {
46
+ if (code === undefined || code === null) {
47
+ return MessageRetryReason.UnknownError;
48
+ }
49
+ const n = typeof code === 'string' ? parseInt(code, 10) : code;
50
+ if (isNaN(n)) {
51
+ return MessageRetryReason.UnknownError;
52
+ }
53
+ // Check if the number is a valid enum value
54
+ if (Object.values(MessageRetryReason).includes(n)) {
55
+ return n;
56
+ }
57
+ return MessageRetryReason.UnknownError;
58
+ }
59
+ /**
60
+ * Check if a retry reason indicates a MAC error.
61
+ * MAC errors are typically caused by encryption session mismatches,
62
+ * often due to LID/PN race conditions.
63
+ */
64
+ export function isMacError(reason) {
65
+ return MAC_ERROR_CODES.has(reason);
66
+ }
67
+ /**
68
+ * Get a human-readable description of a retry reason.
69
+ */
70
+ export function getRetryReasonDescription(reason) {
71
+ switch (reason) {
72
+ case MessageRetryReason.UnknownError:
73
+ return 'Unknown error';
74
+ case MessageRetryReason.GenericError:
75
+ return 'Generic error';
76
+ case MessageRetryReason.SignalErrorInvalidKeyId:
77
+ return 'Invalid key ID — peer prekey rotated';
78
+ case MessageRetryReason.SignalErrorInvalidMessage:
79
+ return 'Invalid message format';
80
+ case MessageRetryReason.SignalErrorNoSession:
81
+ return 'No session — peer not initialized';
82
+ case MessageRetryReason.SignalErrorBadMac:
83
+ return 'Bad MAC — encryption session mismatch';
84
+ case MessageRetryReason.MessageExpired:
85
+ return 'Message expired — too old to decrypt';
86
+ case MessageRetryReason.DecryptionError:
87
+ return 'Decryption failed';
88
+ default:
89
+ return `Unknown reason code ${reason}`;
90
+ }
91
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * RetryReasonTracker — Track message retry reasons and detect retry spirals
3
+ *
4
+ * Inspired by whatsapp-rust's protocol/retry.rs module which defines 13 typed
5
+ * RetryReason codes with MAX_RETRY=5 and optimized key-include behavior.
6
+ *
7
+ * In the middleware layer, we can't control key inclusion (that's transport-level),
8
+ * but we CAN observe retry patterns from messages.update events and classify them.
9
+ * High retry rates per reason = ban signal precursor.
10
+ *
11
+ * Usage:
12
+ * const tracker = new RetryReasonTracker({ enabled: true, maxRetries: 5 });
13
+ *
14
+ * // In messages.update handler:
15
+ * tracker.onMessageUpdate(update);
16
+ *
17
+ * // Check for spirals:
18
+ * if (tracker.isSpiraling(msgId)) {
19
+ * console.warn('Message stuck in retry spiral, dropping');
20
+ * }
21
+ *
22
+ * // On successful send:
23
+ * tracker.clear(msgId);
24
+ *
25
+ * // Get stats:
26
+ * const stats = tracker.getStats();
27
+ */
28
+ const DEFAULT_CONFIG = {
29
+ enabled: false,
30
+ maxRetries: 5,
31
+ spiralThreshold: 3,
32
+ onSpiral: () => { },
33
+ };
34
+ export class RetryReasonTracker {
35
+ config;
36
+ retries = new Map();
37
+ totalRetries = 0;
38
+ reasonCounts = {
39
+ no_session: 0,
40
+ invalid_key: 0,
41
+ bad_mac: 0,
42
+ decryption_failure: 0,
43
+ server_error_463: 0,
44
+ server_error_429: 0,
45
+ timeout: 0,
46
+ no_route: 0,
47
+ node_malformed: 0,
48
+ unknown: 0,
49
+ };
50
+ spiralsDetected = 0;
51
+ constructor(config) {
52
+ this.config = { ...DEFAULT_CONFIG, ...config };
53
+ }
54
+ /**
55
+ * Call when a messages.update event arrives with a status/error.
56
+ * Classifies and records the retry.
57
+ */
58
+ onMessageUpdate(update) {
59
+ if (!this.config.enabled)
60
+ return;
61
+ const msgId = update.key?.id;
62
+ if (!msgId)
63
+ return;
64
+ // Only track error statuses (status 0 = error in Baileys WAMessageStatus)
65
+ if (update.status !== 0 && !update.error)
66
+ return;
67
+ // rc10 may surface error info in update.update (wrapped message); check all forms
68
+ const reason = this.classify(update.error || update.update || update);
69
+ this.recordRetry(msgId, reason);
70
+ }
71
+ /**
72
+ * Classify an arbitrary error object into a RetryReason
73
+ */
74
+ classify(err) {
75
+ if (!err)
76
+ return 'unknown';
77
+ // Check for HTTP status codes
78
+ const statusCode = err.output?.statusCode || err.statusCode || err.status;
79
+ if (statusCode === 463)
80
+ return 'server_error_463';
81
+ if (statusCode === 429)
82
+ return 'server_error_429';
83
+ // Extract error message/text for pattern matching
84
+ const errorMsg = (err.message || err.text || String(err)).toLowerCase();
85
+ // Pattern matching for known errors
86
+ if (errorMsg.includes('bad mac'))
87
+ return 'bad_mac';
88
+ if (errorMsg.includes('no session') || errorMsg.includes('session not found'))
89
+ return 'no_session';
90
+ if (errorMsg.includes('invalid key') || errorMsg.includes('key error'))
91
+ return 'invalid_key';
92
+ if (errorMsg.includes('decryption') || errorMsg.includes('decrypt'))
93
+ return 'decryption_failure';
94
+ if (errorMsg.includes('timeout') || errorMsg.includes('timed out'))
95
+ return 'timeout';
96
+ if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
97
+ return 'no_route';
98
+ if (errorMsg.includes('malformed') || errorMsg.includes('invalid node'))
99
+ return 'node_malformed';
100
+ return 'unknown';
101
+ }
102
+ /**
103
+ * Record a retry for a message
104
+ */
105
+ recordRetry(msgId, reason) {
106
+ const now = Date.now();
107
+ let record = this.retries.get(msgId);
108
+ if (!record) {
109
+ record = {
110
+ msgId,
111
+ count: 0,
112
+ reasons: [],
113
+ firstRetry: now,
114
+ lastRetry: now,
115
+ };
116
+ this.retries.set(msgId, record);
117
+ }
118
+ record.count++;
119
+ record.reasons.push(reason);
120
+ record.lastRetry = now;
121
+ this.totalRetries++;
122
+ this.reasonCounts[reason]++;
123
+ // Check for spiral
124
+ if (record.count >= this.config.spiralThreshold) {
125
+ this.spiralsDetected++;
126
+ this.config.onSpiral(msgId, reason);
127
+ }
128
+ }
129
+ /**
130
+ * Should we warn the user this message is spiraling?
131
+ */
132
+ isSpiraling(msgId) {
133
+ const record = this.retries.get(msgId);
134
+ return record ? record.count >= this.config.spiralThreshold : false;
135
+ }
136
+ /**
137
+ * Reset counters for a specific message (call on successful delivery)
138
+ */
139
+ clear(msgId) {
140
+ this.retries.delete(msgId);
141
+ }
142
+ /**
143
+ * Get current stats
144
+ */
145
+ getStats() {
146
+ return {
147
+ totalRetries: this.totalRetries,
148
+ byReason: { ...this.reasonCounts },
149
+ spiralsDetected: this.spiralsDetected,
150
+ activeRetries: this.retries.size,
151
+ };
152
+ }
153
+ /**
154
+ * Clean up old retry records (>5 minutes old)
155
+ */
156
+ cleanup() {
157
+ const now = Date.now();
158
+ const maxAge = 5 * 60 * 1000; // 5 minutes
159
+ for (const [msgId, record] of this.retries.entries()) {
160
+ if (now - record.lastRetry > maxAge) {
161
+ this.retries.delete(msgId);
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Destroy and clean up
167
+ */
168
+ destroy() {
169
+ this.retries.clear();
170
+ this.cleanup();
171
+ }
172
+ }