@bellaxchuu/flowred 0.8.6

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 (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/WAProto/GenerateStatics.sh +3 -0
  4. package/WAProto/WAProto.proto +6902 -0
  5. package/WAProto/fix-imports.js +85 -0
  6. package/WAProto/index.js +242946 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Defaults/index.js +190 -0
  9. package/lib/Defaults/phonenumber-mcc.json +223 -0
  10. package/lib/Signal/Group/ciphertext-message.js +15 -0
  11. package/lib/Signal/Group/group-session-builder.js +92 -0
  12. package/lib/Signal/Group/group_cipher.js +89 -0
  13. package/lib/Signal/Group/index.js +136 -0
  14. package/lib/Signal/Group/keyhelper.js +73 -0
  15. package/lib/Signal/Group/sender-chain-key.js +32 -0
  16. package/lib/Signal/Group/sender-key-distribution-message.js +66 -0
  17. package/lib/Signal/Group/sender-key-message.js +69 -0
  18. package/lib/Signal/Group/sender-key-name.js +50 -0
  19. package/lib/Signal/Group/sender-key-record.js +44 -0
  20. package/lib/Signal/Group/sender-key-state.js +97 -0
  21. package/lib/Signal/Group/sender-message-key.js +30 -0
  22. package/lib/Signal/libsignal.js +490 -0
  23. package/lib/Signal/lid-mapping.js +262 -0
  24. package/lib/Socket/Client/index.js +30 -0
  25. package/lib/Socket/Client/types.js +13 -0
  26. package/lib/Socket/Client/websocket.js +62 -0
  27. package/lib/Socket/aigroups.js +240 -0
  28. package/lib/Socket/business.js +422 -0
  29. package/lib/Socket/chats.js +1816 -0
  30. package/lib/Socket/communities.js +549 -0
  31. package/lib/Socket/graphql.js +656 -0
  32. package/lib/Socket/groups.js +764 -0
  33. package/lib/Socket/index.js +39 -0
  34. package/lib/Socket/interactive-handler.js +522 -0
  35. package/lib/Socket/interop.js +549 -0
  36. package/lib/Socket/luxu.js +387 -0
  37. package/lib/Socket/managed-account.js +183 -0
  38. package/lib/Socket/messages-recv.js +2440 -0
  39. package/lib/Socket/messages-send.js +1853 -0
  40. package/lib/Socket/mex.js +60 -0
  41. package/lib/Socket/newsletter.js +820 -0
  42. package/lib/Socket/privacy.js +449 -0
  43. package/lib/Socket/registration.js +427 -0
  44. package/lib/Socket/socket.js +1092 -0
  45. package/lib/Socket/text-router.js +83 -0
  46. package/lib/Socket/username.js +243 -0
  47. package/lib/Store/index.js +36 -0
  48. package/lib/Store/keyed-db.js +108 -0
  49. package/lib/Store/make-cache-manager-store.js +90 -0
  50. package/lib/Store/make-in-memory-store.js +604 -0
  51. package/lib/Store/make-ordered-dictionary.js +81 -0
  52. package/lib/Store/object-repository.js +29 -0
  53. package/lib/Types/Auth.js +38 -0
  54. package/lib/Types/Bussines.js +2 -0
  55. package/lib/Types/Call.js +2 -0
  56. package/lib/Types/Chat.js +4 -0
  57. package/lib/Types/Contact.js +2 -0
  58. package/lib/Types/Events.js +2 -0
  59. package/lib/Types/GroupMetadata.js +2 -0
  60. package/lib/Types/Label.js +27 -0
  61. package/lib/Types/LabelAssociation.js +9 -0
  62. package/lib/Types/Message.js +95 -0
  63. package/lib/Types/Mex.js +112 -0
  64. package/lib/Types/Newsletter.js +121 -0
  65. package/lib/Types/Product.js +2 -0
  66. package/lib/Types/Signal.js +2 -0
  67. package/lib/Types/Socket.js +2 -0
  68. package/lib/Types/State.js +70 -0
  69. package/lib/Types/USync.js +2 -0
  70. package/lib/Types/index.js +55 -0
  71. package/lib/Utils/auth-utils.js +306 -0
  72. package/lib/Utils/browser-utils.js +114 -0
  73. package/lib/Utils/business.js +247 -0
  74. package/lib/Utils/chat-utils.js +1208 -0
  75. package/lib/Utils/command-loader.js +100 -0
  76. package/lib/Utils/companion-reg-client-utils.js +42 -0
  77. package/lib/Utils/consumer-application.js +107 -0
  78. package/lib/Utils/crypto.js +155 -0
  79. package/lib/Utils/curve25519-js.js +275 -0
  80. package/lib/Utils/decode-wa-message.js +560 -0
  81. package/lib/Utils/event-buffer.js +607 -0
  82. package/lib/Utils/generics.js +563 -0
  83. package/lib/Utils/group-history.js +60 -0
  84. package/lib/Utils/history.js +244 -0
  85. package/lib/Utils/identity-change-handler.js +52 -0
  86. package/lib/Utils/index.js +57 -0
  87. package/lib/Utils/jid-display-normalization.js +218 -0
  88. package/lib/Utils/link-preview.js +143 -0
  89. package/lib/Utils/logger.js +9 -0
  90. package/lib/Utils/lt-hash.js +39 -0
  91. package/lib/Utils/make-mutex.js +36 -0
  92. package/lib/Utils/message-composer.js +479 -0
  93. package/lib/Utils/message-retry-manager.js +270 -0
  94. package/lib/Utils/messages-media.js +896 -0
  95. package/lib/Utils/messages.js +2904 -0
  96. package/lib/Utils/meta-ai-msmsg.js +262 -0
  97. package/lib/Utils/native-bridge.js +82 -0
  98. package/lib/Utils/noise-handler.js +196 -0
  99. package/lib/Utils/offline-node-processor.js +42 -0
  100. package/lib/Utils/pre-key-manager.js +107 -0
  101. package/lib/Utils/process-message.js +1048 -0
  102. package/lib/Utils/reporting-utils.js +262 -0
  103. package/lib/Utils/session-pool.js +94 -0
  104. package/lib/Utils/signal.js +224 -0
  105. package/lib/Utils/stanza-ack.js +40 -0
  106. package/lib/Utils/sticker.js +145 -0
  107. package/lib/Utils/sync-action-utils.js +54 -0
  108. package/lib/Utils/tc-token-utils.js +161 -0
  109. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  110. package/lib/Utils/validate-connection.js +219 -0
  111. package/lib/Utils/view-once-cache.js +79 -0
  112. package/lib/Utils/voip-rekey.js +22 -0
  113. package/lib/WABinary/constants.js +1304 -0
  114. package/lib/WABinary/decode.js +343 -0
  115. package/lib/WABinary/encode.js +228 -0
  116. package/lib/WABinary/generic-utils.js +240 -0
  117. package/lib/WABinary/index.js +33 -0
  118. package/lib/WABinary/jid-utils.js +361 -0
  119. package/lib/WABinary/types.js +2 -0
  120. package/lib/WAM/BinaryInfo.js +13 -0
  121. package/lib/WAM/constants.js +39486 -0
  122. package/lib/WAM/encode.js +142 -0
  123. package/lib/WAM/index.js +31 -0
  124. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +55 -0
  125. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  126. package/lib/WAUSync/Protocols/USyncContactProtocol.js +54 -0
  127. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
  128. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
  129. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +59 -0
  130. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +31 -0
  131. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  132. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +29 -0
  133. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +39 -0
  134. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  135. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  136. package/lib/WAUSync/Protocols/index.js +39 -0
  137. package/lib/WAUSync/USyncQuery.js +133 -0
  138. package/lib/WAUSync/USyncUser.js +50 -0
  139. package/lib/WAUSync/index.js +31 -0
  140. package/lib/antiban.js +4637 -0
  141. package/lib/index.js +45 -0
  142. package/package.json +107 -0
package/lib/antiban.js ADDED
@@ -0,0 +1,4637 @@
1
+ const __importMetaUrl = require('url').pathToFileURL(__filename).href
2
+ ;('use strict')
3
+ var __create = Object.create
4
+ var __defProp = Object.defineProperty
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor
6
+ var __getOwnPropNames = Object.getOwnPropertyNames
7
+ var __getProtoOf = Object.getPrototypeOf
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty
9
+ var __export = (target, all) => {
10
+ for (var name in all) __defProp(target, name, { get: all[name], enumerable: true })
11
+ }
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if ((from && typeof from === 'object') || typeof from === 'function') {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, {
17
+ get: () => from[key],
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ })
20
+ }
21
+ return to
22
+ }
23
+ var __toESM = (mod, isNodeMode, target) => (
24
+ (target = mod != null ? __create(__getProtoOf(mod)) : {}),
25
+ __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, 'default', { value: mod, enumerable: true }) : target,
31
+ mod
32
+ )
33
+ )
34
+ var __toCommonJS = mod => __copyProps(__defProp({}, '__esModule', { value: true }), mod)
35
+
36
+ // index.js
37
+ var index_exports = {}
38
+ __export(index_exports, {
39
+ AntiBan: () => AntiBan,
40
+ ContactGraphWarmer: () => ContactGraphWarmer,
41
+ ContentVariator: () => ContentVariator,
42
+ FileStateAdapter: () => FileStateAdapter,
43
+ HealthMonitor: () => HealthMonitor,
44
+ JidCanonicalizer: () => JidCanonicalizer,
45
+ LidFirstResolver: () => LidFirstResolver,
46
+ LidResolver: () => LidResolver,
47
+ MAC_ERROR_CODES: () => MAC_ERROR_CODES,
48
+ MessageQueue: () => MessageQueue,
49
+ MessageRetryReason: () => MessageRetryReason,
50
+ PRESETS: () => PRESETS,
51
+ PostReconnectThrottle: () => PostReconnectThrottle,
52
+ PresenceChoreographer: () => PresenceChoreographer,
53
+ RateLimiter: () => RateLimiter,
54
+ ReplyRatioGuard: () => ReplyRatioGuard,
55
+ RetryReasonTracker: () => RetryReasonTracker,
56
+ Scheduler: () => Scheduler,
57
+ SessionHealthMonitor: () => SessionHealthMonitor,
58
+ StateManager: () => StateManager,
59
+ TimelockGuard: () => TimelockGuard,
60
+ WarmUp: () => WarmUp,
61
+ WebhookAlerts: () => WebhookAlerts,
62
+ applyFingerprint: () => applyFingerprint,
63
+ applyGroupMultiplier: () => applyGroupMultiplier,
64
+ classifyDisconnect: () => classifyDisconnect,
65
+ createLidFirstResolver: () => createLidFirstResolver,
66
+ credsSnapshot: () => credsSnapshot,
67
+ generateFingerprint: () => generateFingerprint,
68
+ getCircadianMultiplier: () => getCircadianMultiplier,
69
+ getRetryReasonDescription: () => getRetryReasonDescription,
70
+ isBroadcast: () => isBroadcast,
71
+ isGroup: () => isGroup,
72
+ isMacError: () => isMacError,
73
+ isNewsletter: () => isNewsletter,
74
+ messageRecovery: () => messageRecovery,
75
+ parseRetryReason: () => parseRetryReason,
76
+ proxyRotator: () => proxyRotator,
77
+ readReceiptVariance: () => readReceiptVariance,
78
+ resolveConfig: () => resolveConfig,
79
+ shouldUseGroupProfile: () => shouldUseGroupProfile,
80
+ wrapSocket: () => wrapSocket,
81
+ wrapWithSessionStability: () => wrapWithSessionStability
82
+ })
83
+ module.exports = __toCommonJS(index_exports)
84
+
85
+ // rateLimiter.js
86
+ var TIME_CONSTANTS = {
87
+ MS_PER_SECOND: 1e3,
88
+ MS_PER_MINUTE: 6e4,
89
+ MS_PER_HOUR: 36e5,
90
+ MS_PER_DAY: 864e5,
91
+ BURST_RESET_MS: 3e4,
92
+ IDENTICAL_WINDOW_MS: 36e5
93
+ // 1 hour
94
+ }
95
+ var DEFAULT_CONFIG = {
96
+ maxPerMinute: 8,
97
+ maxPerHour: 200,
98
+ maxPerDay: 1500,
99
+ minDelayMs: 1500,
100
+ maxDelayMs: 5e3,
101
+ newChatDelayMs: 3e3,
102
+ maxIdenticalMessages: 3,
103
+ burstAllowance: 3,
104
+ identicalMessageWindowMs: TIME_CONSTANTS.IDENTICAL_WINDOW_MS
105
+ }
106
+ var RateLimiter = class {
107
+ config
108
+ messages = []
109
+ identicalCount = /* @__PURE__ */ new Map()
110
+ knownChats = /* @__PURE__ */ new Set()
111
+ burstCount = 0
112
+ lastMessageTime = 0
113
+ constructor(config = {}) {
114
+ this.config = { ...DEFAULT_CONFIG, ...config }
115
+ }
116
+ /**
117
+ * Calculate delay before next message can be sent.
118
+ * Returns 0 if message can be sent immediately.
119
+ * Returns -1 if message should be blocked entirely.
120
+ */
121
+ async getDelay(recipient, content) {
122
+ const now = Date.now()
123
+ this.cleanup(now)
124
+ const contentHash = this.hashContent(content)
125
+ const dayMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY)
126
+ if (dayMessages.length >= this.config.maxPerDay) {
127
+ return -1
128
+ }
129
+ const hourMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR)
130
+ if (hourMessages.length >= this.config.maxPerHour) {
131
+ hourMessages.sort((a, b) => a.timestamp - b.timestamp)
132
+ const oldestInHour = hourMessages[0]
133
+ const delay2 = oldestInHour
134
+ ? oldestInHour.timestamp + TIME_CONSTANTS.MS_PER_HOUR - now
135
+ : TIME_CONSTANTS.MS_PER_HOUR
136
+ return Math.max(delay2, TIME_CONSTANTS.MS_PER_MINUTE)
137
+ }
138
+ const minuteMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE)
139
+ if (minuteMessages.length >= this.config.maxPerMinute) {
140
+ minuteMessages.sort((a, b) => a.timestamp - b.timestamp)
141
+ const oldestInMinute = minuteMessages[0]
142
+ const delay2 = oldestInMinute
143
+ ? oldestInMinute.timestamp + TIME_CONSTANTS.MS_PER_MINUTE - now
144
+ : TIME_CONSTANTS.MS_PER_MINUTE
145
+ return Math.max(delay2, TIME_CONSTANTS.MS_PER_SECOND)
146
+ }
147
+ const tracker = this.identicalCount.get(contentHash)
148
+ if (tracker) {
149
+ if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
150
+ if (tracker.count >= this.config.maxIdenticalMessages) {
151
+ return -1
152
+ }
153
+ }
154
+ }
155
+ let delay = 0
156
+ if (this.burstCount < this.config.burstAllowance) {
157
+ this.burstCount++
158
+ delay = this.jitter(this.config.minDelayMs * 0.5, this.config.minDelayMs)
159
+ } else {
160
+ delay = this.jitter(this.config.minDelayMs, this.config.maxDelayMs)
161
+ }
162
+ const isInterop = recipient.endsWith('@interop')
163
+ if (!this.knownChats.has(recipient) || isInterop) {
164
+ delay += this.jitter(this.config.newChatDelayMs * 0.5, this.config.newChatDelayMs)
165
+ }
166
+ const timeSinceLast = now - this.lastMessageTime
167
+ if (timeSinceLast < this.config.minDelayMs) {
168
+ delay = Math.max(delay, this.config.minDelayMs - timeSinceLast)
169
+ }
170
+ const typingDelay = Math.min(content.length * 30, 3e3)
171
+ delay += this.jitter(typingDelay * 0.5, typingDelay)
172
+ return Math.round(delay)
173
+ }
174
+ /**
175
+ * Record a sent message
176
+ */
177
+ record(recipient, content) {
178
+ const now = Date.now()
179
+ const contentHash = this.hashContent(content)
180
+ const timeSinceLast = now - this.lastMessageTime
181
+ if (timeSinceLast > TIME_CONSTANTS.BURST_RESET_MS) {
182
+ this.burstCount = 0
183
+ }
184
+ this.messages.push({ timestamp: now, recipient, contentHash })
185
+ this.knownChats.add(recipient)
186
+ this.lastMessageTime = now
187
+ const tracker = this.identicalCount.get(contentHash)
188
+ if (tracker) {
189
+ if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
190
+ tracker.count++
191
+ tracker.lastSeen = now
192
+ } else {
193
+ this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now })
194
+ }
195
+ } else {
196
+ this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now })
197
+ }
198
+ }
199
+ /**
200
+ * Get current usage stats
201
+ */
202
+ getStats() {
203
+ const now = Date.now()
204
+ this.cleanup(now)
205
+ return {
206
+ lastMinute: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE).length,
207
+ lastHour: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR).length,
208
+ lastDay: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY).length,
209
+ limits: {
210
+ perMinute: this.config.maxPerMinute,
211
+ perHour: this.config.maxPerHour,
212
+ perDay: this.config.maxPerDay
213
+ },
214
+ knownChats: this.knownChats.size
215
+ }
216
+ }
217
+ /** Get the set of known chat JIDs (for state persistence) */
218
+ getKnownChats() {
219
+ return this.knownChats
220
+ }
221
+ /** Restore known chats from persisted state */
222
+ restoreKnownChats(chats) {
223
+ for (const jid of chats) {
224
+ this.knownChats.add(jid)
225
+ }
226
+ }
227
+ cleanup(now) {
228
+ this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY)
229
+ for (const [hash, tracker] of this.identicalCount.entries()) {
230
+ if (now - tracker.lastSeen > this.config.identicalMessageWindowMs) {
231
+ this.identicalCount.delete(hash)
232
+ }
233
+ }
234
+ }
235
+ /** Random delay between min and max (gaussian-ish distribution) */
236
+ jitter(min, max) {
237
+ const u1 = Math.random()
238
+ const u2 = Math.random()
239
+ const normal = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
240
+ const normalized = (normal + 3) / 6
241
+ const clamped = Math.max(0, Math.min(1, normalized))
242
+ return Math.round(min + clamped * (max - min))
243
+ }
244
+ /** Simple hash for content dedup */
245
+ hashContent(content) {
246
+ let hash = 0
247
+ for (let i = 0; i < content.length; i++) {
248
+ const char = content.charCodeAt(i)
249
+ hash = (hash << 5) - hash + char
250
+ hash |= 0
251
+ }
252
+ return hash.toString(36)
253
+ }
254
+ }
255
+
256
+ // warmup.js
257
+ var DEFAULT_CONFIG2 = {
258
+ warmUpDays: 7,
259
+ day1Limit: 20,
260
+ growthFactor: 1.8,
261
+ inactivityThresholdHours: 72
262
+ }
263
+ var WarmUp = class {
264
+ config
265
+ state
266
+ constructor(config = {}, existingState) {
267
+ this.config = { ...DEFAULT_CONFIG2, ...config }
268
+ this.state = existingState || this.freshState()
269
+ }
270
+ /**
271
+ * Get the current daily message limit based on warm-up phase
272
+ */
273
+ getDailyLimit() {
274
+ if (this.state.graduated) return Infinity
275
+ const day = this.getCurrentDay()
276
+ if (day >= this.config.warmUpDays) {
277
+ this.state.graduated = true
278
+ return Infinity
279
+ }
280
+ return Math.round(this.config.day1Limit * Math.pow(this.config.growthFactor, day))
281
+ }
282
+ /**
283
+ * Check if a message can be sent (within warm-up limits)
284
+ */
285
+ canSend() {
286
+ this.checkInactivity()
287
+ if (this.state.graduated) return true
288
+ const day = this.getCurrentDay()
289
+ const todayCount = this.state.dailyCounts[day] || 0
290
+ return todayCount < this.getDailyLimit()
291
+ }
292
+ /**
293
+ * Record a sent message
294
+ */
295
+ record() {
296
+ const now = Date.now()
297
+ const day = this.getCurrentDay()
298
+ while (this.state.dailyCounts.length <= day) {
299
+ this.state.dailyCounts.push(0)
300
+ }
301
+ this.state.dailyCounts[day]++
302
+ this.state.lastActiveAt = now
303
+ }
304
+ /**
305
+ * Get current warm-up status
306
+ */
307
+ getStatus() {
308
+ const day = this.getCurrentDay()
309
+ const todaySent = this.state.dailyCounts[day] || 0
310
+ const limit = this.getDailyLimit()
311
+ return {
312
+ phase: this.state.graduated ? 'graduated' : 'warming',
313
+ day: Math.min(day + 1, this.config.warmUpDays),
314
+ totalDays: this.config.warmUpDays,
315
+ todayLimit: limit === Infinity ? -1 : limit,
316
+ todaySent,
317
+ progress: this.state.graduated ? 100 : Math.round((day / this.config.warmUpDays) * 100)
318
+ }
319
+ }
320
+ /**
321
+ * Export state for persistence
322
+ */
323
+ exportState() {
324
+ return { ...this.state }
325
+ }
326
+ /**
327
+ * Reset warm-up (e.g., after detected ban risk)
328
+ */
329
+ reset() {
330
+ this.state = this.freshState()
331
+ }
332
+ getCurrentDay() {
333
+ return Math.floor((Date.now() - this.state.startedAt) / 864e5)
334
+ }
335
+ checkInactivity() {
336
+ const hoursSinceActive = (Date.now() - this.state.lastActiveAt) / 36e5
337
+ if (hoursSinceActive > this.config.inactivityThresholdHours && this.state.graduated) {
338
+ this.state = this.freshState()
339
+ this.state.graduated = false
340
+ }
341
+ }
342
+ freshState() {
343
+ const now = Date.now()
344
+ return {
345
+ startedAt: now,
346
+ lastActiveAt: now,
347
+ dailyCounts: [],
348
+ graduated: false
349
+ }
350
+ }
351
+ }
352
+
353
+ // health.js
354
+ var DEFAULT_CONFIG3 = {
355
+ disconnectWarningThreshold: 3,
356
+ disconnectCriticalThreshold: 5,
357
+ failedMessageThreshold: 5,
358
+ autoPauseAt: 'high'
359
+ }
360
+ var HealthMonitor = class {
361
+ config
362
+ events = []
363
+ startTime = Date.now()
364
+ paused = false
365
+ lastRisk = 'low'
366
+ lastBadEventTime = Date.now()
367
+ lastEventWasSevere = false
368
+ constructor(config = {}) {
369
+ this.config = { ...DEFAULT_CONFIG3, ...config }
370
+ }
371
+ /**
372
+ * Record a disconnection event
373
+ */
374
+ recordDisconnect(reason) {
375
+ const reasonStr = String(reason)
376
+ if (reasonStr === '403' || reasonStr === 'forbidden') {
377
+ this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr })
378
+ this.lastBadEventTime = Date.now()
379
+ this.lastEventWasSevere = true
380
+ } else if (reasonStr === '401' || reasonStr === 'loggedOut') {
381
+ this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr })
382
+ this.lastBadEventTime = Date.now()
383
+ this.lastEventWasSevere = true
384
+ } else if (reasonStr === '463') {
385
+ this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail: reasonStr })
386
+ this.lastBadEventTime = Date.now()
387
+ this.lastEventWasSevere = false
388
+ } else {
389
+ this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr })
390
+ this.lastBadEventTime = Date.now()
391
+ this.lastEventWasSevere = false
392
+ }
393
+ this.checkAndNotify()
394
+ }
395
+ /**
396
+ * Record a successful reconnection
397
+ */
398
+ recordReconnect() {
399
+ this.events.push({ type: 'reconnect', timestamp: Date.now() })
400
+ }
401
+ /**
402
+ * Record a failed message send
403
+ */
404
+ recordMessageFailed(error) {
405
+ this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error })
406
+ this.lastBadEventTime = Date.now()
407
+ this.lastEventWasSevere = false
408
+ this.checkAndNotify()
409
+ }
410
+ /**
411
+ * Record a 463 reachout timelock error
412
+ */
413
+ recordReachoutTimelock(detail) {
414
+ this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail })
415
+ this.lastBadEventTime = Date.now()
416
+ this.lastEventWasSevere = false
417
+ this.checkAndNotify()
418
+ }
419
+ /**
420
+ * Get current health status
421
+ */
422
+ getStatus() {
423
+ const now = Date.now()
424
+ this.cleanup(now)
425
+ const hourEvents = this.events.filter(e => now - e.timestamp < 36e5)
426
+ const disconnects = hourEvents.filter(e => e.type === 'disconnect').length
427
+ const forbidden = hourEvents.filter(e => e.type === 'forbidden').length
428
+ const loggedOut = hourEvents.filter(e => e.type === 'loggedOut').length
429
+ const failedMessages = hourEvents.filter(e => e.type === 'messageFailed').length
430
+ let score = 0
431
+ const reasons = []
432
+ if (forbidden > 0) {
433
+ score += 40 * forbidden
434
+ reasons.push(`${forbidden} forbidden (403) error${forbidden > 1 ? 's' : ''} in last hour`)
435
+ }
436
+ if (loggedOut > 0) {
437
+ score += 60
438
+ reasons.push('Logged out by WhatsApp \u2014 possible temporary ban')
439
+ }
440
+ const timelocked = hourEvents.filter(e => e.type === 'reachoutTimelocked').length
441
+ if (timelocked > 0) {
442
+ score += 25
443
+ reasons.push(`${timelocked} reachout timelock (463) error${timelocked > 1 ? 's' : ''} in last hour`)
444
+ }
445
+ if (disconnects >= this.config.disconnectCriticalThreshold) {
446
+ score += 30
447
+ reasons.push(`${disconnects} disconnects in last hour (critical threshold)`)
448
+ } else if (disconnects >= this.config.disconnectWarningThreshold) {
449
+ score += 30
450
+ reasons.push(`${disconnects} disconnects in last hour`)
451
+ }
452
+ if (failedMessages >= this.config.failedMessageThreshold) {
453
+ score += 20
454
+ reasons.push(`${failedMessages} failed messages in last hour`)
455
+ }
456
+ score = Math.min(100, score)
457
+ const minutesSinceLastBad = (now - this.lastBadEventTime) / 6e4
458
+ const decayRate = this.lastEventWasSevere ? 2 : 5
459
+ score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate))
460
+ let risk
461
+ if (score >= 80) risk = 'critical'
462
+ else if (score >= 40) risk = 'high'
463
+ else if (score >= 15) risk = 'medium'
464
+ else risk = 'low'
465
+ let recommendation
466
+ switch (risk) {
467
+ case 'critical':
468
+ recommendation = 'STOP ALL MESSAGING IMMEDIATELY. Disconnect and wait 24-48 hours before reconnecting.'
469
+ break
470
+ case 'high':
471
+ recommendation = 'Reduce messaging rate by 80%. Consider pausing for 1-2 hours.'
472
+ break
473
+ case 'medium':
474
+ recommendation = 'Reduce messaging rate by 50%. Increase delays between messages.'
475
+ break
476
+ default:
477
+ recommendation = 'Operating normally. Continue monitoring.'
478
+ }
479
+ const lastDisconnect = [...this.events]
480
+ .reverse()
481
+ .find(e => e.type === 'disconnect' || e.type === 'forbidden' || e.type === 'loggedOut')
482
+ return {
483
+ risk,
484
+ score,
485
+ reasons: reasons.length ? reasons : ['No issues detected'],
486
+ recommendation,
487
+ stats: {
488
+ disconnectsLastHour: disconnects,
489
+ failedMessagesLastHour: failedMessages,
490
+ forbiddenErrors: forbidden,
491
+ timelockErrors: timelocked,
492
+ uptimeMs: now - this.startTime,
493
+ lastDisconnectReason: lastDisconnect?.detail
494
+ }
495
+ }
496
+ }
497
+ /**
498
+ * Check if sending should be paused
499
+ */
500
+ isPaused() {
501
+ if (this.paused) return true
502
+ const status = this.getStatus()
503
+ const riskOrder = ['low', 'medium', 'high', 'critical']
504
+ return riskOrder.indexOf(status.risk) >= riskOrder.indexOf(this.config.autoPauseAt)
505
+ }
506
+ /**
507
+ * Manually pause/resume
508
+ */
509
+ setPaused(paused) {
510
+ this.paused = paused
511
+ }
512
+ /**
513
+ * Reset all tracked events
514
+ */
515
+ reset() {
516
+ this.events = []
517
+ this.startTime = Date.now()
518
+ this.paused = false
519
+ this.lastRisk = 'low'
520
+ this.lastBadEventTime = Date.now()
521
+ this.lastEventWasSevere = false
522
+ }
523
+ cleanup(now) {
524
+ this.events = this.events.filter(e => now - e.timestamp < 216e5)
525
+ }
526
+ checkAndNotify() {
527
+ const status = this.getStatus()
528
+ if (status.risk !== this.lastRisk) {
529
+ this.lastRisk = status.risk
530
+ this.config.onRiskChange?.(status)
531
+ }
532
+ }
533
+ }
534
+
535
+ // timelockGuard.js
536
+ var DEFAULT_CONFIG4 = {
537
+ resumeBufferMs: 1e4
538
+ }
539
+ var TimelockGuard = class {
540
+ config
541
+ state = {
542
+ isActive: false,
543
+ errorCount: 0
544
+ }
545
+ knownChats = /* @__PURE__ */ new Set()
546
+ resumeTimer = null
547
+ timerGeneration = 0
548
+ // BUG FIX 4: Track timer validity to prevent race conditions
549
+ constructor(config = {}) {
550
+ this.config = { ...DEFAULT_CONFIG4, ...config }
551
+ }
552
+ /**
553
+ * Update timelock state from Baileys connection.update event
554
+ */
555
+ onTimelockUpdate(data) {
556
+ const wasActive = this.state.isActive
557
+ this.state.isActive = !!data.isActive
558
+ this.state.enforcementType = data.enforcementType
559
+ this.state.expiresAt = data.timeEnforcementEnds
560
+ if (this.state.isActive && !wasActive) {
561
+ this.state.detectedAt = /* @__PURE__ */ new Date()
562
+ this.state.errorCount = 0
563
+ this.config.onTimelockDetected?.(this.getState())
564
+ this.scheduleResume()
565
+ } else if (this.state.isActive && wasActive) {
566
+ this.scheduleResume()
567
+ }
568
+ if (!this.state.isActive && wasActive) {
569
+ this.clearResumeTimer()
570
+ this.config.onTimelockLifted?.(this.getState())
571
+ }
572
+ }
573
+ /**
574
+ * Record a 463 error from a failed send
575
+ */
576
+ record463Error() {
577
+ this.state.errorCount++
578
+ if (!this.state.isActive) {
579
+ this.state.isActive = true
580
+ this.state.detectedAt = /* @__PURE__ */ new Date()
581
+ this.state.expiresAt = new Date(Date.now() + 6e4)
582
+ this.config.onTimelockDetected?.(this.getState())
583
+ this.scheduleResume()
584
+ }
585
+ }
586
+ /**
587
+ * Register a JID as a known/existing chat (has tctoken / prior history)
588
+ */
589
+ registerKnownChat(jid) {
590
+ this.knownChats.add(jid)
591
+ }
592
+ /**
593
+ * Register multiple known chats at once (e.g. from chat list on connect)
594
+ */
595
+ registerKnownChats(jids) {
596
+ for (const jid of jids) {
597
+ this.knownChats.add(jid)
598
+ }
599
+ }
600
+ /**
601
+ * Check if a message to this recipient should be allowed
602
+ */
603
+ canSend(jid) {
604
+ if (!this.state.isActive) {
605
+ return { allowed: true }
606
+ }
607
+ if (this.state.expiresAt) {
608
+ const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs
609
+ if (Date.now() >= expiryWithBuffer) {
610
+ this.lift()
611
+ return { allowed: true }
612
+ }
613
+ }
614
+ if (jid.endsWith('@g.us') || jid.endsWith('@newsletter')) {
615
+ return { allowed: true }
616
+ }
617
+ if (this.knownChats.has(jid)) {
618
+ return { allowed: true }
619
+ }
620
+ const expiresIn = this.state.expiresAt ? Math.max(0, this.state.expiresAt.getTime() - Date.now()) : 6e4
621
+ return {
622
+ allowed: false,
623
+ reason: `Reachout timelocked (${this.state.enforcementType || 'unknown'}). New contacts blocked. Expires in ${Math.ceil(expiresIn / 1e3)}s.`
624
+ }
625
+ }
626
+ /**
627
+ * Get current timelock state
628
+ */
629
+ getState() {
630
+ return { ...this.state }
631
+ }
632
+ /**
633
+ * Check if currently timelocked
634
+ */
635
+ isTimelocked() {
636
+ if (!this.state.isActive) return false
637
+ if (this.state.expiresAt) {
638
+ const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs
639
+ if (Date.now() >= expiryWithBuffer) {
640
+ this.lift()
641
+ return false
642
+ }
643
+ }
644
+ return true
645
+ }
646
+ /**
647
+ * Get the set of known chat JIDs
648
+ */
649
+ getKnownChats() {
650
+ return new Set(this.knownChats)
651
+ }
652
+ /**
653
+ * Manually lift the timelock
654
+ */
655
+ lift() {
656
+ if (this.state.isActive) {
657
+ this.state.isActive = false
658
+ this.clearResumeTimer()
659
+ this.config.onTimelockLifted?.(this.getState())
660
+ }
661
+ }
662
+ /**
663
+ * Reset all state
664
+ */
665
+ reset() {
666
+ this.state = { isActive: false, errorCount: 0 }
667
+ this.knownChats.clear()
668
+ this.clearResumeTimer()
669
+ }
670
+ scheduleResume() {
671
+ this.clearResumeTimer()
672
+ if (this.state.expiresAt) {
673
+ const delay = this.state.expiresAt.getTime() - Date.now() + this.config.resumeBufferMs
674
+ if (delay > 0) {
675
+ this.timerGeneration++
676
+ const currentGeneration = this.timerGeneration
677
+ this.resumeTimer = setTimeout(() => {
678
+ if (currentGeneration === this.timerGeneration) {
679
+ this.lift()
680
+ }
681
+ }, delay)
682
+ }
683
+ }
684
+ }
685
+ clearResumeTimer() {
686
+ if (this.resumeTimer) {
687
+ clearTimeout(this.resumeTimer)
688
+ this.resumeTimer = null
689
+ this.timerGeneration++
690
+ }
691
+ }
692
+ }
693
+
694
+ // replyRatio.js
695
+ var DEFAULT_CONFIG5 = {
696
+ enabled: false,
697
+ minRatio: 0.1,
698
+ minMessagesBeforeEnforce: 5,
699
+ inboundAutoReplyProbability: 0.25,
700
+ autoReplyTemplates: ['\u{1F44D}', '\u{1F44C}', 'ok', 'noted', 'thanks', '\u{1F64F}', 'got it'],
701
+ cooldownHoursOnViolation: 24,
702
+ scope: 'individual'
703
+ }
704
+ var ReplyRatioGuard = class {
705
+ config
706
+ contacts = /* @__PURE__ */ new Map()
707
+ constructor(config = {}) {
708
+ this.config = { ...DEFAULT_CONFIG5, ...config }
709
+ }
710
+ /**
711
+ * Check if message can be sent to this contact based on reply ratio.
712
+ * Call before sending.
713
+ */
714
+ beforeSend(jid) {
715
+ if (!this.config.enabled) {
716
+ return { allowed: true }
717
+ }
718
+ if (this.isGroup(jid) && this.config.scope === 'individual') {
719
+ return { allowed: true }
720
+ }
721
+ const record = this.contacts.get(jid)
722
+ if (!record) {
723
+ return { allowed: true }
724
+ }
725
+ if (record.cooledUntil && Date.now() < record.cooledUntil) {
726
+ const hoursLeft = Math.ceil((record.cooledUntil - Date.now()) / 36e5)
727
+ return {
728
+ allowed: false,
729
+ reason: `Reply ratio cooldown \u2014 ${record.sent} sent, ${record.received} received. Retry in ${hoursLeft}h`
730
+ }
731
+ }
732
+ if (record.sent >= this.config.minMessagesBeforeEnforce) {
733
+ const ratio = record.sent === 0 ? 1 : record.received / record.sent
734
+ if (ratio < this.config.minRatio) {
735
+ record.cooledUntil = Date.now() + this.config.cooldownHoursOnViolation * 36e5
736
+ return {
737
+ allowed: false,
738
+ reason: `Reply ratio too low (${(ratio * 100).toFixed(1)}% < ${(this.config.minRatio * 100).toFixed(1)}%). Cooldown ${this.config.cooldownHoursOnViolation}h`
739
+ }
740
+ }
741
+ }
742
+ return { allowed: true }
743
+ }
744
+ /**
745
+ * Record an outbound message sent to this contact.
746
+ */
747
+ recordSent(jid) {
748
+ if (!this.config.enabled) return
749
+ const record = this.contacts.get(jid) || { sent: 0, received: 0 }
750
+ record.sent++
751
+ this.contacts.set(jid, record)
752
+ }
753
+ /**
754
+ * Record an inbound message received from this contact.
755
+ */
756
+ recordReceived(jid) {
757
+ if (!this.config.enabled) return
758
+ const record = this.contacts.get(jid) || { sent: 0, received: 0 }
759
+ record.received++
760
+ delete record.cooledUntil
761
+ this.contacts.set(jid, record)
762
+ }
763
+ /**
764
+ * Suggest whether to send an auto-reply to this incoming message.
765
+ * Returns { shouldReply: true, suggestedText: '👍' } if probability check passes.
766
+ * Caller is responsible for actually sending the message.
767
+ */
768
+ suggestReply(jid, _msgText) {
769
+ if (!this.config.enabled) {
770
+ return { shouldReply: false }
771
+ }
772
+ if (this.isGroup(jid) && this.config.scope === 'individual') {
773
+ return { shouldReply: false }
774
+ }
775
+ if (Math.random() < this.config.inboundAutoReplyProbability) {
776
+ const templates = this.config.autoReplyTemplates
777
+ const suggestedText = templates[Math.floor(Math.random() * templates.length)]
778
+ return { shouldReply: true, suggestedText }
779
+ }
780
+ return { shouldReply: false }
781
+ }
782
+ /**
783
+ * Get statistics for all contacts and global metrics.
784
+ */
785
+ getStats() {
786
+ const perContact = Array.from(this.contacts.entries()).map(([jid, record]) => ({
787
+ jid,
788
+ sent: record.sent,
789
+ received: record.received,
790
+ ratio: record.sent === 0 ? 0 : record.received / record.sent,
791
+ cooledUntil: record.cooledUntil
792
+ }))
793
+ const globalSent = perContact.reduce((sum, c) => sum + c.sent, 0)
794
+ const globalReceived = perContact.reduce((sum, c) => sum + c.received, 0)
795
+ const globalRatio = globalSent === 0 ? 0 : globalReceived / globalSent
796
+ const contactsOnCooldown = perContact.filter(c => c.cooledUntil && Date.now() < c.cooledUntil).length
797
+ return {
798
+ perContact,
799
+ globalSent,
800
+ globalReceived,
801
+ globalRatio,
802
+ contactsOnCooldown
803
+ }
804
+ }
805
+ /**
806
+ * Reset all counters.
807
+ */
808
+ reset() {
809
+ this.contacts.clear()
810
+ }
811
+ /**
812
+ * Export state for persistence.
813
+ */
814
+ exportState() {
815
+ return {
816
+ contacts: Array.from(this.contacts.entries())
817
+ }
818
+ }
819
+ /**
820
+ * Restore state from persistence.
821
+ */
822
+ restoreState(state) {
823
+ if (state?.contacts && Array.isArray(state.contacts)) {
824
+ this.contacts = new Map(state.contacts)
825
+ }
826
+ }
827
+ /**
828
+ * Check if JID is a group.
829
+ */
830
+ isGroup(jid) {
831
+ return jid.endsWith('@g.us')
832
+ }
833
+ }
834
+
835
+ // contactGraph.js
836
+ var DEFAULT_CONFIG6 = {
837
+ enabled: false,
838
+ requireHandshakeBeforeGroupSend: true,
839
+ handshakeMinDelayMs: 36e5,
840
+ // 1 hour
841
+ groupLurkPeriodMs: 432e5,
842
+ // 12 hours
843
+ maxStrangerMessagesPerDay: 5,
844
+ autoRegisterOnIncoming: true
845
+ }
846
+ var ContactGraphWarmer = class {
847
+ config
848
+ contacts = /* @__PURE__ */ new Map()
849
+ groups = /* @__PURE__ */ new Map()
850
+ strangerMessagesToday = 0
851
+ lastStrangerResetDay = this.getCurrentDay()
852
+ constructor(config = {}) {
853
+ this.config = { ...DEFAULT_CONFIG6, ...config }
854
+ }
855
+ /**
856
+ * Check if message can be sent to this contact/group.
857
+ * Returns { allowed: false, needsHandshake: true } if handshake required.
858
+ */
859
+ canMessage(jid) {
860
+ if (!this.config.enabled) {
861
+ return { allowed: true }
862
+ }
863
+ const currentDay = this.getCurrentDay()
864
+ if (currentDay !== this.lastStrangerResetDay) {
865
+ this.strangerMessagesToday = 0
866
+ this.lastStrangerResetDay = currentDay
867
+ }
868
+ if (this.isGroup(jid)) {
869
+ return this.checkGroupMessage(jid)
870
+ }
871
+ return this.checkIndividualMessage(jid)
872
+ }
873
+ /**
874
+ * Mark handshake as sent to this contact.
875
+ */
876
+ markHandshakeSent(jid) {
877
+ if (!this.config.enabled) return
878
+ if (this.isGroup(jid)) return
879
+ const record = this.contacts.get(jid) || { state: 'stranger' }
880
+ record.state = 'handshake_sent'
881
+ record.handshakeSentAt = Date.now()
882
+ this.contacts.set(jid, record)
883
+ }
884
+ /**
885
+ * Mark handshake as complete with this contact.
886
+ */
887
+ markHandshakeComplete(jid) {
888
+ if (!this.config.enabled) return
889
+ if (this.isGroup(jid)) return
890
+ const record = this.contacts.get(jid) || { state: 'stranger' }
891
+ record.state = 'handshake_complete'
892
+ this.contacts.set(jid, record)
893
+ }
894
+ /**
895
+ * Register a contact as known (skip handshake requirement).
896
+ */
897
+ registerKnownContact(jid) {
898
+ if (!this.config.enabled) return
899
+ if (this.isGroup(jid)) return
900
+ const record = this.contacts.get(jid) || { state: 'stranger' }
901
+ record.state = 'known'
902
+ this.contacts.set(jid, record)
903
+ }
904
+ /**
905
+ * Register a group join event.
906
+ */
907
+ registerGroupJoin(groupJid) {
908
+ if (!this.config.enabled) return
909
+ if (!this.isGroup(groupJid)) return
910
+ this.groups.set(groupJid, { joinedAt: Date.now() })
911
+ }
912
+ /**
913
+ * Get contact state.
914
+ */
915
+ getContactState(jid) {
916
+ if (this.isGroup(jid)) return 'known'
917
+ return this.contacts.get(jid)?.state || 'stranger'
918
+ }
919
+ /**
920
+ * Handle incoming message — auto-register if enabled.
921
+ */
922
+ onIncomingMessage(jid) {
923
+ if (!this.config.enabled) return
924
+ if (this.isGroup(jid)) return
925
+ if (this.config.autoRegisterOnIncoming) {
926
+ this.registerKnownContact(jid)
927
+ }
928
+ }
929
+ /**
930
+ * Get statistics.
931
+ */
932
+ getStats() {
933
+ const knownContacts = Array.from(this.contacts.values()).filter(c => c.state === 'known').length
934
+ const pendingHandshakes = Array.from(this.contacts.values()).filter(c => c.state === 'handshake_sent').length
935
+ const groupsJoined = Array.from(this.groups.entries()).map(([groupJid, record]) => ({
936
+ groupJid,
937
+ joinedAt: record.joinedAt,
938
+ firstSendUnlocksAt: record.joinedAt + this.config.groupLurkPeriodMs
939
+ }))
940
+ return {
941
+ knownContacts,
942
+ pendingHandshakes,
943
+ strangersToday: this.strangerMessagesToday,
944
+ groupsJoined
945
+ }
946
+ }
947
+ /**
948
+ * Reset all state.
949
+ */
950
+ reset() {
951
+ this.contacts.clear()
952
+ this.groups.clear()
953
+ this.strangerMessagesToday = 0
954
+ this.lastStrangerResetDay = this.getCurrentDay()
955
+ }
956
+ /**
957
+ * Export state for persistence.
958
+ */
959
+ exportState() {
960
+ return {
961
+ contacts: Array.from(this.contacts.entries()),
962
+ groups: Array.from(this.groups.entries()),
963
+ strangerMessagesToday: this.strangerMessagesToday,
964
+ lastStrangerResetDay: this.lastStrangerResetDay
965
+ }
966
+ }
967
+ /**
968
+ * Restore state from persistence.
969
+ */
970
+ restoreState(state) {
971
+ if (state?.contacts && Array.isArray(state.contacts)) {
972
+ this.contacts = new Map(state.contacts)
973
+ }
974
+ if (state?.groups && Array.isArray(state.groups)) {
975
+ this.groups = new Map(state.groups)
976
+ }
977
+ if (typeof state?.strangerMessagesToday === 'number') {
978
+ this.strangerMessagesToday = state.strangerMessagesToday
979
+ }
980
+ if (typeof state?.lastStrangerResetDay === 'number') {
981
+ this.lastStrangerResetDay = state.lastStrangerResetDay
982
+ }
983
+ }
984
+ // Private helpers
985
+ isGroup(jid) {
986
+ return jid.endsWith('@g.us')
987
+ }
988
+ getCurrentDay() {
989
+ return Math.floor(Date.now() / 864e5)
990
+ }
991
+ checkGroupMessage(groupJid) {
992
+ const record = this.groups.get(groupJid)
993
+ if (!record) {
994
+ return { allowed: true }
995
+ }
996
+ const lurkEndsAt = record.joinedAt + this.config.groupLurkPeriodMs
997
+ if (Date.now() < lurkEndsAt) {
998
+ const minutesLeft = Math.ceil((lurkEndsAt - Date.now()) / 6e4)
999
+ return {
1000
+ allowed: false,
1001
+ reason: `Group lurk period not elapsed \u2014 wait ${minutesLeft} minutes`
1002
+ }
1003
+ }
1004
+ return { allowed: true }
1005
+ }
1006
+ checkIndividualMessage(jid) {
1007
+ const record = this.contacts.get(jid)
1008
+ if (!record || record.state === 'stranger') {
1009
+ if (this.config.requireHandshakeBeforeGroupSend) {
1010
+ if (this.strangerMessagesToday >= this.config.maxStrangerMessagesPerDay) {
1011
+ return {
1012
+ allowed: false,
1013
+ reason: `Daily new-contact limit reached (${this.config.maxStrangerMessagesPerDay})`,
1014
+ needsHandshake: true
1015
+ }
1016
+ }
1017
+ this.strangerMessagesToday++
1018
+ }
1019
+ return { allowed: true, needsHandshake: true }
1020
+ }
1021
+ if (record.state === 'handshake_sent') {
1022
+ if (!record.handshakeSentAt) {
1023
+ return { allowed: true }
1024
+ }
1025
+ const elapsed = Date.now() - record.handshakeSentAt
1026
+ if (elapsed < this.config.handshakeMinDelayMs) {
1027
+ const minutesLeft = Math.ceil((this.config.handshakeMinDelayMs - elapsed) / 6e4)
1028
+ return {
1029
+ allowed: false,
1030
+ reason: `Handshake too recent \u2014 wait ${minutesLeft} minutes`
1031
+ }
1032
+ }
1033
+ }
1034
+ return { allowed: true }
1035
+ }
1036
+ }
1037
+
1038
+ // presenceChoreographer.js
1039
+ var DEFAULT_CONFIG7 = {
1040
+ enabled: false,
1041
+ enableCircadianRhythm: true,
1042
+ timezone: 'UTC',
1043
+ activityCurve: 'office',
1044
+ circadian: {
1045
+ enabled: true,
1046
+ profile: 'default',
1047
+ timezone: 'UTC'
1048
+ },
1049
+ distractionPauseProbability: 0.05,
1050
+ distractionPauseMinMs: 3e5,
1051
+ distractionPauseMaxMs: 12e5,
1052
+ readReceiptDelayMinMs: 3e3,
1053
+ readReceiptDelayMaxMs: 45e3,
1054
+ readReceiptSkipProbability: 0.15,
1055
+ offlineGapProbability: 0.03,
1056
+ offlineGapMinMs: 3e5,
1057
+ offlineGapMaxMs: 9e5,
1058
+ enableTypingModel: true,
1059
+ typingWPM: 45,
1060
+ typingWPMStdDev: 15,
1061
+ thinkPauseProbability: 0.08,
1062
+ thinkPauseMinMs: 800,
1063
+ thinkPauseMaxMs: 3500,
1064
+ intermittentPausedProbability: 0.4,
1065
+ typingMaxMs: 9e4,
1066
+ typingMinMs: 600
1067
+ }
1068
+ var ACTIVITY_CURVES = {
1069
+ office: [
1070
+ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
1071
+ // 0-7: night quiet
1072
+ 0.5, 0.5,
1073
+ // 8-9: morning ramp
1074
+ 0.95, 0.95,
1075
+ // 10-11: morning peak
1076
+ 0.6,
1077
+ // 12: lunch dip
1078
+ 0.9, 0.9, 0.9, 0.9,
1079
+ // 13-16: afternoon
1080
+ 0.6, 0.6,
1081
+ // 17-18: wind-down
1082
+ 0.4, 0.4,
1083
+ // 19-20: evening
1084
+ 0.2, 0.2, 0.2, 0.2
1085
+ // 21-24: taper
1086
+ ],
1087
+ social: [
1088
+ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
1089
+ // 0-7: night quiet
1090
+ 0.3, 0.4,
1091
+ // 8-9: slow start
1092
+ 0.7, 0.8,
1093
+ // 10-11: ramp up
1094
+ 0.5,
1095
+ // 12: lunch
1096
+ 0.7, 0.7,
1097
+ // 13-14: afternoon
1098
+ 0.4,
1099
+ // 15: tea time dip
1100
+ 0.8, 0.9, 0.9,
1101
+ // 16-18: active
1102
+ 0.6,
1103
+ // 19: dinner dip
1104
+ 0.8, 0.85, 0.9, 0.95, 1
1105
+ // 20-24: evening peak
1106
+ ],
1107
+ global: [
1108
+ 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
1109
+ // 0-5: night
1110
+ 0.4, 0.4,
1111
+ // 6-7: dawn dip
1112
+ 0.6, 0.7, 0.8, 0.8,
1113
+ // 8-11: morning
1114
+ 0.6,
1115
+ // 12: lunch
1116
+ 0.8, 0.8, 0.8, 0.8,
1117
+ // 13-16: afternoon
1118
+ 0.7, 0.7,
1119
+ // 17-18: evening
1120
+ 0.6, 0.5, 0.5, 0.5, 0.5, 0.5
1121
+ // 19-24: night taper
1122
+ ]
1123
+ }
1124
+ function getCircadianMultiplier(date = /* @__PURE__ */ new Date(), profile = 'default', timezone) {
1125
+ if (profile === 'always_on') {
1126
+ return 1
1127
+ }
1128
+ let hour
1129
+ if (timezone) {
1130
+ try {
1131
+ const formatter = new Intl.DateTimeFormat('en-US', {
1132
+ timeZone: timezone,
1133
+ hour: 'numeric',
1134
+ hour12: false
1135
+ })
1136
+ const parts = formatter.formatToParts(date)
1137
+ const hourPart = parts.find(p => p.type === 'hour')
1138
+ hour = hourPart ? parseInt(hourPart.value, 10) : date.getHours()
1139
+ } catch {
1140
+ hour = date.getHours()
1141
+ }
1142
+ } else {
1143
+ hour = date.getHours()
1144
+ }
1145
+ let shiftedHour = hour
1146
+ if (profile === 'nightOwl') {
1147
+ shiftedHour = (hour - 3 + 24) % 24
1148
+ } else if (profile === 'earlyBird') {
1149
+ shiftedHour = (hour + 2) % 24
1150
+ }
1151
+ if (shiftedHour >= 9 && shiftedHour < 22) {
1152
+ const t = (shiftedHour - 9) / 13
1153
+ return 1 + 0.2 * Math.cos(2 * Math.PI * t)
1154
+ } else if (shiftedHour >= 22 && shiftedHour < 24) {
1155
+ const t = (shiftedHour - 22) / 2
1156
+ return 1.2 + 1.3 * t
1157
+ } else if (shiftedHour >= 0 && shiftedHour < 2) {
1158
+ const t = shiftedHour / 2
1159
+ return 2.5 + 1.5 * t
1160
+ } else if (shiftedHour >= 2 && shiftedHour < 6) {
1161
+ const t = (shiftedHour - 2) / 4
1162
+ return 5 + 1 * Math.cos(Math.PI * t)
1163
+ } else {
1164
+ const t = (shiftedHour - 6) / 3
1165
+ return 4 - 3 * t
1166
+ }
1167
+ }
1168
+ var PresenceChoreographer = class {
1169
+ config
1170
+ stats = {
1171
+ distractionPausesInjected: 0,
1172
+ offlineGapsInjected: 0,
1173
+ readReceiptsDelayed: 0,
1174
+ readReceiptsSkipped: 0,
1175
+ typingPlansComputed: 0,
1176
+ typingPlansExecuted: 0,
1177
+ totalTypingTimeMs: 0
1178
+ }
1179
+ constructor(config = {}) {
1180
+ this.config = {
1181
+ ...DEFAULT_CONFIG7,
1182
+ ...config,
1183
+ circadian: {
1184
+ ...DEFAULT_CONFIG7.circadian,
1185
+ ...(config.circadian || {})
1186
+ }
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Get current activity factor (0.1 to 1.0).
1191
+ * Higher = more active = shorter delays.
1192
+ * If circadian disabled, returns 1.0.
1193
+ */
1194
+ getCurrentActivityFactor() {
1195
+ if (!this.config.enabled || !this.config.enableCircadianRhythm) {
1196
+ return 1
1197
+ }
1198
+ const hour = this.getLocalHour()
1199
+ const curve = ACTIVITY_CURVES[this.config.activityCurve]
1200
+ return curve[hour] || 0.5
1201
+ }
1202
+ /**
1203
+ * Check if should pause for distraction.
1204
+ * Returns { pause: true, durationMs: 600000 } if probability check passes.
1205
+ */
1206
+ shouldPauseForDistraction() {
1207
+ if (!this.config.enabled) {
1208
+ return { pause: false, durationMs: 0 }
1209
+ }
1210
+ if (Math.random() < this.config.distractionPauseProbability) {
1211
+ const durationMs = this.randomBetween(this.config.distractionPauseMinMs, this.config.distractionPauseMaxMs)
1212
+ this.stats.distractionPausesInjected++
1213
+ return { pause: true, durationMs }
1214
+ }
1215
+ return { pause: false, durationMs: 0 }
1216
+ }
1217
+ /**
1218
+ * Check if should take offline gap.
1219
+ * Returns { offline: true, durationMs: 600000 } if probability check passes.
1220
+ */
1221
+ shouldTakeOfflineGap() {
1222
+ if (!this.config.enabled) {
1223
+ return { offline: false, durationMs: 0 }
1224
+ }
1225
+ if (Math.random() < this.config.offlineGapProbability) {
1226
+ const durationMs = this.randomBetween(this.config.offlineGapMinMs, this.config.offlineGapMaxMs)
1227
+ this.stats.offlineGapsInjected++
1228
+ return { offline: true, durationMs }
1229
+ }
1230
+ return { offline: false, durationMs: 0 }
1231
+ }
1232
+ /**
1233
+ * Check if should mark message as read.
1234
+ * Returns { mark: false } if skip probability hit.
1235
+ * Returns { mark: true, delayMs: 5000 } otherwise.
1236
+ * Applies circadian multiplier to delay.
1237
+ */
1238
+ shouldMarkRead() {
1239
+ if (!this.config.enabled) {
1240
+ return { mark: true, delayMs: 0 }
1241
+ }
1242
+ if (Math.random() < this.config.readReceiptSkipProbability) {
1243
+ this.stats.readReceiptsSkipped++
1244
+ return { mark: false, delayMs: 0 }
1245
+ }
1246
+ const baseDelayMs = this.randomBetween(this.config.readReceiptDelayMinMs, this.config.readReceiptDelayMaxMs)
1247
+ let delayMs = baseDelayMs
1248
+ if (this.config.circadian.enabled) {
1249
+ const circadianMultiplier = getCircadianMultiplier(
1250
+ /* @__PURE__ */ new Date(),
1251
+ this.config.circadian.profile,
1252
+ this.config.circadian.timezone
1253
+ )
1254
+ delayMs = Math.floor(baseDelayMs * circadianMultiplier)
1255
+ }
1256
+ this.stats.readReceiptsDelayed++
1257
+ return { mark: true, delayMs }
1258
+ }
1259
+ /**
1260
+ * Compute realistic typing duration for a message of given length.
1261
+ * Includes Gaussian WPM variance + think-pause injection + circadian timing multiplier.
1262
+ * Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
1263
+ *
1264
+ * plan = [
1265
+ * { state: 'composing', durationMs: 4200 },
1266
+ * { state: 'paused', durationMs: 950 }, // think pause
1267
+ * { state: 'composing', durationMs: 6800 },
1268
+ * { state: 'paused', durationMs: 600 }, // brief stop before send
1269
+ * ]
1270
+ */
1271
+ computeTypingPlan(messageLength) {
1272
+ if (!this.config.enabled || !this.config.enableTypingModel) {
1273
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }]
1274
+ }
1275
+ this.stats.typingPlansComputed++
1276
+ if (messageLength === 0) {
1277
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }]
1278
+ }
1279
+ const wpmSample = this.clamp(this.gaussianSample(this.config.typingWPM, this.config.typingWPMStdDev), 10, 120)
1280
+ const cps = (wpmSample * 5) / 60
1281
+ const baseMs = (messageLength / cps) * 1e3
1282
+ let circadianMultiplier = 1
1283
+ if (this.config.circadian.enabled) {
1284
+ circadianMultiplier = getCircadianMultiplier(
1285
+ /* @__PURE__ */ new Date(),
1286
+ this.config.circadian.profile,
1287
+ this.config.circadian.timezone
1288
+ )
1289
+ }
1290
+ const targetMs = this.clamp(baseMs * circadianMultiplier, this.config.typingMinMs, this.config.typingMaxMs)
1291
+ const plan = []
1292
+ let remainingBudget = targetMs
1293
+ let position = 0
1294
+ const chunkSize = 10
1295
+ const numChunks = Math.max(1, Math.ceil(messageLength / chunkSize))
1296
+ for (let i = 0; i < numChunks && remainingBudget > 0; i++) {
1297
+ const charsInChunk = Math.min(chunkSize, messageLength - position)
1298
+ const remainingChunks = numChunks - i
1299
+ const chunkBudget = remainingBudget / remainingChunks
1300
+ const chunkTypingMs = Math.floor(Math.min(chunkBudget, remainingBudget))
1301
+ if (chunkTypingMs <= 0) break
1302
+ if (i > 0 && i < numChunks - 1 && Math.random() < this.config.thinkPauseProbability) {
1303
+ plan.push({ state: 'composing', durationMs: chunkTypingMs })
1304
+ remainingBudget -= chunkTypingMs
1305
+ const basePauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs)
1306
+ const pauseMs = Math.floor(basePauseMs * circadianMultiplier)
1307
+ plan.push({ state: 'paused', durationMs: pauseMs })
1308
+ } else {
1309
+ if (plan.length === 0 || plan[plan.length - 1].state === 'paused') {
1310
+ plan.push({ state: 'composing', durationMs: chunkTypingMs })
1311
+ } else {
1312
+ plan[plan.length - 1].durationMs += chunkTypingMs
1313
+ }
1314
+ remainingBudget -= chunkTypingMs
1315
+ }
1316
+ position += charsInChunk
1317
+ }
1318
+ if (Math.random() < this.config.intermittentPausedProbability) {
1319
+ const baseFinalPauseMs = this.randomBetween(200, 800)
1320
+ const finalPauseMs = Math.floor(baseFinalPauseMs * circadianMultiplier)
1321
+ plan.push({ state: 'paused', durationMs: finalPauseMs })
1322
+ }
1323
+ if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
1324
+ return [{ state: 'composing', durationMs: this.config.typingMinMs }]
1325
+ }
1326
+ return plan
1327
+ }
1328
+ /**
1329
+ * Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
1330
+ * Awaits each step's duration. Updates stats.
1331
+ *
1332
+ * await choreo.executeTypingPlan(sock, jid, plan);
1333
+ * await sock.sendMessage(jid, content);
1334
+ */
1335
+ async executeTypingPlan(sock, jid, plan, options) {
1336
+ this.stats.typingPlansExecuted++
1337
+ for (const step of plan) {
1338
+ if (options?.signal?.aborted) {
1339
+ await Promise.resolve(sock.sendPresenceUpdate('paused', jid))
1340
+ throw new Error('Typing plan aborted')
1341
+ }
1342
+ await Promise.resolve(sock.sendPresenceUpdate(step.state, jid))
1343
+ await this.sleep(step.durationMs)
1344
+ this.stats.totalTypingTimeMs += step.durationMs
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Get statistics.
1349
+ */
1350
+ getStats() {
1351
+ return {
1352
+ currentActivityFactor: this.getCurrentActivityFactor(),
1353
+ distractionPausesInjected: this.stats.distractionPausesInjected,
1354
+ offlineGapsInjected: this.stats.offlineGapsInjected,
1355
+ readReceiptsDelayed: this.stats.readReceiptsDelayed,
1356
+ readReceiptsSkipped: this.stats.readReceiptsSkipped,
1357
+ currentHourLocal: this.getLocalHour(),
1358
+ typingPlansComputed: this.stats.typingPlansComputed,
1359
+ typingPlansExecuted: this.stats.typingPlansExecuted,
1360
+ totalTypingTimeMs: this.stats.totalTypingTimeMs
1361
+ }
1362
+ }
1363
+ /**
1364
+ * Reset statistics.
1365
+ */
1366
+ reset() {
1367
+ this.stats = {
1368
+ distractionPausesInjected: 0,
1369
+ offlineGapsInjected: 0,
1370
+ readReceiptsDelayed: 0,
1371
+ readReceiptsSkipped: 0,
1372
+ typingPlansComputed: 0,
1373
+ typingPlansExecuted: 0,
1374
+ totalTypingTimeMs: 0
1375
+ }
1376
+ }
1377
+ // Private helpers
1378
+ getLocalHour() {
1379
+ try {
1380
+ const formatter = new Intl.DateTimeFormat('en-US', {
1381
+ timeZone: this.config.timezone,
1382
+ hour: 'numeric',
1383
+ hour12: false
1384
+ })
1385
+ const parts = formatter.formatToParts(/* @__PURE__ */ new Date())
1386
+ const hourPart = parts.find(p => p.type === 'hour')
1387
+ if (hourPart) {
1388
+ return parseInt(hourPart.value, 10)
1389
+ }
1390
+ } catch (error) {}
1391
+ return /* @__PURE__ */ new Date().getUTCHours()
1392
+ }
1393
+ randomBetween(min, max) {
1394
+ return Math.floor(Math.random() * (max - min + 1)) + min
1395
+ }
1396
+ clamp(value, min, max) {
1397
+ return Math.max(min, Math.min(max, value))
1398
+ }
1399
+ /**
1400
+ * Generate Gaussian sample using Box-Muller transform.
1401
+ * Returns a sample from N(mean, stdDev).
1402
+ */
1403
+ gaussianSample(mean, stdDev) {
1404
+ const u1 = Math.random()
1405
+ const u2 = Math.random()
1406
+ const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
1407
+ return mean + z0 * stdDev
1408
+ }
1409
+ sleep(ms) {
1410
+ return new Promise(resolve => setTimeout(resolve, ms))
1411
+ }
1412
+ }
1413
+
1414
+ // retryTracker.js
1415
+ var DEFAULT_CONFIG8 = {
1416
+ enabled: false,
1417
+ maxRetries: 5,
1418
+ spiralThreshold: 3,
1419
+ onSpiral: () => {}
1420
+ }
1421
+ var RetryReasonTracker = class {
1422
+ config
1423
+ retries = /* @__PURE__ */ new Map()
1424
+ totalRetries = 0
1425
+ reasonCounts = {
1426
+ no_session: 0,
1427
+ invalid_key: 0,
1428
+ bad_mac: 0,
1429
+ decryption_failure: 0,
1430
+ server_error_463: 0,
1431
+ server_error_429: 0,
1432
+ timeout: 0,
1433
+ no_route: 0,
1434
+ node_malformed: 0,
1435
+ unknown: 0
1436
+ }
1437
+ spiralsDetected = 0
1438
+ constructor(config) {
1439
+ this.config = { ...DEFAULT_CONFIG8, ...config }
1440
+ }
1441
+ /**
1442
+ * Call when a messages.update event arrives with a status/error.
1443
+ * Classifies and records the retry.
1444
+ */
1445
+ onMessageUpdate(update) {
1446
+ if (!this.config.enabled) return
1447
+ const msgId = update.key?.id
1448
+ if (!msgId) return
1449
+ if (update.status !== 0 && !update.error) return
1450
+ const reason = this.classify(update.error || update)
1451
+ this.recordRetry(msgId, reason)
1452
+ }
1453
+ /**
1454
+ * Classify an arbitrary error object into a RetryReason
1455
+ */
1456
+ classify(err) {
1457
+ if (!err) return 'unknown'
1458
+ const statusCode = err.output?.statusCode || err.statusCode || err.status
1459
+ if (statusCode === 463) return 'server_error_463'
1460
+ if (statusCode === 429) return 'server_error_429'
1461
+ const errorMsg = (err.message || err.text || String(err)).toLowerCase()
1462
+ if (errorMsg.includes('bad mac')) return 'bad_mac'
1463
+ if (errorMsg.includes('no session') || errorMsg.includes('session not found')) return 'no_session'
1464
+ if (errorMsg.includes('invalid key') || errorMsg.includes('key error')) return 'invalid_key'
1465
+ if (errorMsg.includes('decryption') || errorMsg.includes('decrypt')) return 'decryption_failure'
1466
+ if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) return 'timeout'
1467
+ if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
1468
+ return 'no_route'
1469
+ if (errorMsg.includes('malformed') || errorMsg.includes('invalid node')) return 'node_malformed'
1470
+ return 'unknown'
1471
+ }
1472
+ /**
1473
+ * Record a retry for a message
1474
+ */
1475
+ recordRetry(msgId, reason) {
1476
+ const now = Date.now()
1477
+ let record = this.retries.get(msgId)
1478
+ if (!record) {
1479
+ record = {
1480
+ msgId,
1481
+ count: 0,
1482
+ reasons: [],
1483
+ firstRetry: now,
1484
+ lastRetry: now
1485
+ }
1486
+ this.retries.set(msgId, record)
1487
+ }
1488
+ record.count++
1489
+ record.reasons.push(reason)
1490
+ record.lastRetry = now
1491
+ this.totalRetries++
1492
+ this.reasonCounts[reason]++
1493
+ if (record.count >= this.config.spiralThreshold) {
1494
+ this.spiralsDetected++
1495
+ this.config.onSpiral(msgId, reason)
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Should we warn the user this message is spiraling?
1500
+ */
1501
+ isSpiraling(msgId) {
1502
+ const record = this.retries.get(msgId)
1503
+ return record ? record.count >= this.config.spiralThreshold : false
1504
+ }
1505
+ /**
1506
+ * Reset counters for a specific message (call on successful delivery)
1507
+ */
1508
+ clear(msgId) {
1509
+ this.retries.delete(msgId)
1510
+ }
1511
+ /**
1512
+ * Get current stats
1513
+ */
1514
+ getStats() {
1515
+ return {
1516
+ totalRetries: this.totalRetries,
1517
+ byReason: { ...this.reasonCounts },
1518
+ spiralsDetected: this.spiralsDetected,
1519
+ activeRetries: this.retries.size
1520
+ }
1521
+ }
1522
+ /**
1523
+ * Clean up old retry records (>5 minutes old)
1524
+ */
1525
+ cleanup() {
1526
+ const now = Date.now()
1527
+ const maxAge = 5 * 60 * 1e3
1528
+ for (const [msgId, record] of this.retries.entries()) {
1529
+ if (now - record.lastRetry > maxAge) {
1530
+ this.retries.delete(msgId)
1531
+ }
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Destroy and clean up
1536
+ */
1537
+ destroy() {
1538
+ this.retries.clear()
1539
+ this.cleanup()
1540
+ }
1541
+ }
1542
+
1543
+ // reconnectThrottle.js
1544
+ var DEFAULT_CONFIG9 = {
1545
+ enabled: false,
1546
+ rampDurationMs: 6e4,
1547
+ initialRateMultiplier: 0.1,
1548
+ rampSteps: 6,
1549
+ baselineRatePerMinute: null
1550
+ }
1551
+ var PostReconnectThrottle = class {
1552
+ config
1553
+ throttledSince = null
1554
+ throttledSendCount = 0
1555
+ lifetimeReconnects = 0
1556
+ rampTimer = null
1557
+ currentStep = 0
1558
+ // Tracking sends in current window
1559
+ sendsInCurrentWindow = 0
1560
+ currentWindowStart = 0
1561
+ WINDOW_DURATION_MS = 6e4
1562
+ // 1 minute window
1563
+ constructor(config) {
1564
+ this.config = {
1565
+ ...DEFAULT_CONFIG9,
1566
+ ...config,
1567
+ baselineRatePerMinute: config?.baselineRatePerMinute || null
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Call when connection is re-established. Starts throttle window.
1572
+ */
1573
+ onReconnect() {
1574
+ if (!this.config.enabled) return
1575
+ this.throttledSince = Date.now()
1576
+ this.currentStep = 0
1577
+ this.throttledSendCount = 0
1578
+ this.lifetimeReconnects++
1579
+ this.sendsInCurrentWindow = 0
1580
+ this.currentWindowStart = Date.now()
1581
+ if (this.rampTimer) {
1582
+ clearTimeout(this.rampTimer)
1583
+ }
1584
+ this.scheduleNextRampStep()
1585
+ }
1586
+ /**
1587
+ * Call when connection drops (optional — reset state).
1588
+ */
1589
+ onDisconnect() {}
1590
+ /**
1591
+ * Schedule the next ramp step
1592
+ */
1593
+ scheduleNextRampStep() {
1594
+ if (this.currentStep >= this.config.rampSteps) {
1595
+ this.throttledSince = null
1596
+ this.rampTimer = null
1597
+ return
1598
+ }
1599
+ const stepDuration = this.config.rampDurationMs / this.config.rampSteps
1600
+ this.rampTimer = setTimeout(() => {
1601
+ this.currentStep++
1602
+ this.scheduleNextRampStep()
1603
+ }, stepDuration)
1604
+ }
1605
+ /**
1606
+ * Returns current rate multiplier (1.0 = no throttle)
1607
+ */
1608
+ getCurrentMultiplier() {
1609
+ if (!this.config.enabled || !this.throttledSince) {
1610
+ return 1
1611
+ }
1612
+ const elapsed = Date.now() - this.throttledSince
1613
+ if (elapsed >= this.config.rampDurationMs) {
1614
+ return 1
1615
+ }
1616
+ const progress = this.currentStep / this.config.rampSteps
1617
+ const multiplier = this.config.initialRateMultiplier + (1 - this.config.initialRateMultiplier) * progress
1618
+ return Math.min(1, multiplier)
1619
+ }
1620
+ /**
1621
+ * Checks if a send should be gated. Returns {allowed, reason, retryAfterMs?}
1622
+ */
1623
+ beforeSend() {
1624
+ if (!this.config.enabled || !this.throttledSince) {
1625
+ return { allowed: true }
1626
+ }
1627
+ const now = Date.now()
1628
+ const multiplier = this.getCurrentMultiplier()
1629
+ if (multiplier >= 1) {
1630
+ this.throttledSince = null
1631
+ return { allowed: true }
1632
+ }
1633
+ if (now - this.currentWindowStart >= this.WINDOW_DURATION_MS) {
1634
+ this.sendsInCurrentWindow = 0
1635
+ this.currentWindowStart = now
1636
+ }
1637
+ const baselineRate = this.config.baselineRatePerMinute ? this.config.baselineRatePerMinute() : 8
1638
+ const allowedInWindow = Math.max(1, Math.floor(baselineRate * multiplier))
1639
+ if (this.sendsInCurrentWindow >= allowedInWindow) {
1640
+ const windowRemaining = this.WINDOW_DURATION_MS - (now - this.currentWindowStart)
1641
+ return {
1642
+ allowed: false,
1643
+ reason: `Post-reconnect throttle: ${Math.floor(multiplier * 100)}% rate (${this.sendsInCurrentWindow}/${allowedInWindow} sends in window)`,
1644
+ retryAfterMs: windowRemaining
1645
+ }
1646
+ }
1647
+ this.sendsInCurrentWindow++
1648
+ this.throttledSendCount++
1649
+ return { allowed: true }
1650
+ }
1651
+ /**
1652
+ * Get current stats
1653
+ */
1654
+ getStats() {
1655
+ const multiplier = this.getCurrentMultiplier()
1656
+ const isThrottled = this.throttledSince !== null && multiplier < 1
1657
+ const remainingMs =
1658
+ isThrottled && this.throttledSince
1659
+ ? Math.max(0, this.config.rampDurationMs - (Date.now() - this.throttledSince))
1660
+ : 0
1661
+ return {
1662
+ isThrottled,
1663
+ currentMultiplier: multiplier,
1664
+ throttledSinceMs: this.throttledSince,
1665
+ remainingMs,
1666
+ throttledSendCount: this.throttledSendCount,
1667
+ lifetimeReconnects: this.lifetimeReconnects
1668
+ }
1669
+ }
1670
+ /**
1671
+ * Destroy and clean up timers
1672
+ */
1673
+ destroy() {
1674
+ if (this.rampTimer) {
1675
+ clearTimeout(this.rampTimer)
1676
+ this.rampTimer = null
1677
+ }
1678
+ this.throttledSince = null
1679
+ }
1680
+ }
1681
+
1682
+ // lidResolver.js
1683
+ var DEFAULT_CONFIG10 = {
1684
+ canonical: 'pn',
1685
+ maxEntries: 1e4
1686
+ }
1687
+ var LidResolver = class {
1688
+ config
1689
+ persistence
1690
+ // Bidirectional maps: lid→pn and pn→lid
1691
+ lidToPn = /* @__PURE__ */ new Map()
1692
+ pnToLid = /* @__PURE__ */ new Map()
1693
+ // pn → lid (for quick reverse lookup)
1694
+ stats = {
1695
+ learnedFromEvents: 0,
1696
+ lookupsServed: 0,
1697
+ lookupMisses: 0
1698
+ }
1699
+ constructor(config = {}) {
1700
+ this.config = { ...DEFAULT_CONFIG10, ...config }
1701
+ this.persistence = config.persistence
1702
+ if (this.persistence?.load) {
1703
+ void this.hydrate()
1704
+ }
1705
+ }
1706
+ /**
1707
+ * Learn from a message event. Idempotent.
1708
+ * Accepts partial mappings — will use whatever fields are available.
1709
+ */
1710
+ learn(mapping) {
1711
+ let lid = mapping.lid ? this.normalizeJid(mapping.lid) : void 0
1712
+ let pn = mapping.pn ? this.normalizeJid(mapping.pn) : void 0
1713
+ const phone = mapping.phone
1714
+ if (!lid || (!pn && !phone)) {
1715
+ return
1716
+ }
1717
+ if (!pn && phone) {
1718
+ pn = `${phone}@s.whatsapp.net`
1719
+ }
1720
+ if (!lid || !pn) return
1721
+ if (!lid.endsWith('@lid')) return
1722
+ if (!pn.endsWith('@s.whatsapp.net')) return
1723
+ const existing = this.lidToPn.get(lid)
1724
+ if (existing) {
1725
+ existing.seenCount++
1726
+ existing.learnedAt = Date.now()
1727
+ return
1728
+ }
1729
+ const extractedPhone = phone || pn.split('@')[0]
1730
+ const newMapping = {
1731
+ lid,
1732
+ pn,
1733
+ phone: extractedPhone,
1734
+ learnedAt: Date.now(),
1735
+ seenCount: 1
1736
+ }
1737
+ if (this.lidToPn.size >= this.config.maxEntries) {
1738
+ this.evictLRU()
1739
+ }
1740
+ this.lidToPn.set(lid, newMapping)
1741
+ this.pnToLid.set(pn, lid)
1742
+ this.stats.learnedFromEvents++
1743
+ if (this.persistence?.save) {
1744
+ void this.flush()
1745
+ }
1746
+ }
1747
+ /**
1748
+ * Given any form (LID or PN), return the canonical form.
1749
+ * Falls back to input if unknown (no throw).
1750
+ */
1751
+ resolveCanonical(jid) {
1752
+ const normalized = this.normalizeJid(jid)
1753
+ if (this.config.canonical === 'pn') {
1754
+ if (normalized.endsWith('@lid')) {
1755
+ const mapping = this.lidToPn.get(normalized)
1756
+ if (mapping) {
1757
+ this.stats.lookupsServed++
1758
+ mapping.learnedAt = Date.now()
1759
+ return mapping.pn
1760
+ }
1761
+ this.stats.lookupMisses++
1762
+ return jid
1763
+ }
1764
+ this.stats.lookupsServed++
1765
+ return normalized
1766
+ } else {
1767
+ if (normalized.endsWith('@s.whatsapp.net')) {
1768
+ const lid = this.pnToLid.get(normalized)
1769
+ if (lid) {
1770
+ this.stats.lookupsServed++
1771
+ const mapping = this.lidToPn.get(lid)
1772
+ if (mapping) {
1773
+ mapping.learnedAt = Date.now()
1774
+ }
1775
+ return lid
1776
+ }
1777
+ this.stats.lookupMisses++
1778
+ return jid
1779
+ }
1780
+ this.stats.lookupsServed++
1781
+ return normalized
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Lookup partner form. Returns null if unknown.
1786
+ */
1787
+ getLid(pn) {
1788
+ const normalized = this.normalizeJid(pn)
1789
+ const lid = this.pnToLid.get(normalized)
1790
+ if (lid) {
1791
+ const mapping = this.lidToPn.get(lid)
1792
+ if (mapping) {
1793
+ mapping.learnedAt = Date.now()
1794
+ }
1795
+ }
1796
+ return lid || null
1797
+ }
1798
+ getPn(lid) {
1799
+ const normalized = this.normalizeJid(lid)
1800
+ const mapping = this.lidToPn.get(normalized)
1801
+ if (mapping) {
1802
+ mapping.learnedAt = Date.now()
1803
+ return mapping.pn
1804
+ }
1805
+ return null
1806
+ }
1807
+ /**
1808
+ * Full mapping for inspection
1809
+ */
1810
+ getMapping(jid) {
1811
+ const normalized = this.normalizeJid(jid)
1812
+ const byLid = this.lidToPn.get(normalized)
1813
+ if (byLid) {
1814
+ byLid.learnedAt = Date.now()
1815
+ return byLid
1816
+ }
1817
+ const lid = this.pnToLid.get(normalized)
1818
+ if (lid) {
1819
+ const mapping = this.lidToPn.get(lid)
1820
+ if (mapping) {
1821
+ mapping.learnedAt = Date.now()
1822
+ return mapping
1823
+ }
1824
+ }
1825
+ return null
1826
+ }
1827
+ /**
1828
+ * Seed from persistence (called automatically in constructor if persistence provided)
1829
+ */
1830
+ async hydrate() {
1831
+ if (!this.persistence?.load) return
1832
+ try {
1833
+ const stored = await this.persistence.load()
1834
+ if (!stored || typeof stored !== 'object') return
1835
+ for (const [lid, serialized] of Object.entries(stored)) {
1836
+ if (typeof serialized === 'string') {
1837
+ const pn = serialized
1838
+ const phone = pn.split('@')[0]
1839
+ const mapping = {
1840
+ lid,
1841
+ pn,
1842
+ phone,
1843
+ learnedAt: Date.now(),
1844
+ seenCount: 1
1845
+ }
1846
+ this.lidToPn.set(lid, mapping)
1847
+ this.pnToLid.set(pn, lid)
1848
+ } else if (typeof serialized === 'object' && serialized !== null) {
1849
+ const mapping = serialized
1850
+ this.lidToPn.set(lid, mapping)
1851
+ this.pnToLid.set(mapping.pn, lid)
1852
+ }
1853
+ }
1854
+ } catch (error) {}
1855
+ }
1856
+ /**
1857
+ * Flush current map to persistence
1858
+ */
1859
+ async flush() {
1860
+ if (!this.persistence?.save) return
1861
+ try {
1862
+ const toStore = {}
1863
+ for (const [lid, mapping] of this.lidToPn.entries()) {
1864
+ toStore[lid] = mapping
1865
+ }
1866
+ await this.persistence.save(toStore)
1867
+ } catch (error) {}
1868
+ }
1869
+ getStats() {
1870
+ return {
1871
+ totalMappings: this.lidToPn.size,
1872
+ learnedFromEvents: this.stats.learnedFromEvents,
1873
+ lookupsServed: this.stats.lookupsServed,
1874
+ lookupMisses: this.stats.lookupMisses,
1875
+ canonicalForm: this.config.canonical
1876
+ }
1877
+ }
1878
+ /**
1879
+ * Clear everything
1880
+ */
1881
+ reset() {
1882
+ this.lidToPn.clear()
1883
+ this.pnToLid.clear()
1884
+ this.stats = {
1885
+ learnedFromEvents: 0,
1886
+ lookupsServed: 0,
1887
+ lookupMisses: 0
1888
+ }
1889
+ }
1890
+ destroy() {
1891
+ this.reset()
1892
+ if (this.persistence?.save) {
1893
+ void this.flush()
1894
+ }
1895
+ }
1896
+ // Private helpers
1897
+ /**
1898
+ * Normalize JID: strip device suffix `:N`
1899
+ */
1900
+ normalizeJid(jid) {
1901
+ return jid.replace(/:\d+@/, '@')
1902
+ }
1903
+ /**
1904
+ * Evict least recently accessed mapping (LRU)
1905
+ */
1906
+ evictLRU() {
1907
+ let oldestLid = null
1908
+ let oldestTime = Infinity
1909
+ for (const [lid, mapping] of this.lidToPn.entries()) {
1910
+ if (mapping.learnedAt < oldestTime) {
1911
+ oldestTime = mapping.learnedAt
1912
+ oldestLid = lid
1913
+ }
1914
+ }
1915
+ if (oldestLid) {
1916
+ const mapping = this.lidToPn.get(oldestLid)
1917
+ if (mapping) {
1918
+ this.pnToLid.delete(mapping.pn)
1919
+ }
1920
+ this.lidToPn.delete(oldestLid)
1921
+ }
1922
+ }
1923
+ }
1924
+
1925
+ // jidCanonicalizer.js
1926
+ var DEFAULT_CONFIG11 = {
1927
+ enabled: false,
1928
+ canonicalizeOutbound: true,
1929
+ learnFromEvents: true
1930
+ }
1931
+ var JidCanonicalizer = class {
1932
+ config
1933
+ lidResolver
1934
+ ownsResolver
1935
+ // Track if we created the resolver (for destroy)
1936
+ stats = {
1937
+ outboundCanonicalized: 0,
1938
+ outboundPassthrough: 0,
1939
+ inboundLearned: 0,
1940
+ canonicalKeyHits: 0,
1941
+ canonicalKeyMisses: 0
1942
+ }
1943
+ constructor(config = {}) {
1944
+ this.config = { ...DEFAULT_CONFIG11, ...config }
1945
+ if (config.resolver) {
1946
+ this.lidResolver = config.resolver
1947
+ this.ownsResolver = false
1948
+ } else {
1949
+ this.lidResolver = new LidResolver(config.resolverConfig)
1950
+ this.ownsResolver = true
1951
+ }
1952
+ }
1953
+ /**
1954
+ * Access the underlying resolver (for cross-module sharing)
1955
+ */
1956
+ get resolver() {
1957
+ return this.lidResolver
1958
+ }
1959
+ /**
1960
+ * Called by wrapper on every outbound send. Returns canonical JID.
1961
+ */
1962
+ canonicalizeTarget(jid) {
1963
+ if (!this.config.enabled || !this.config.canonicalizeOutbound) {
1964
+ return jid
1965
+ }
1966
+ const canonical = this.lidResolver.resolveCanonical(jid)
1967
+ if (canonical !== jid) {
1968
+ this.stats.outboundCanonicalized++
1969
+ } else {
1970
+ this.stats.outboundPassthrough++
1971
+ }
1972
+ return canonical
1973
+ }
1974
+ /**
1975
+ * Returns a stable, canonical thread key for storage / DB indexing.
1976
+ *
1977
+ * Different from `canonicalizeTarget()` (which picks the right send target):
1978
+ * - canonicalizeTarget('1234@lid') → '+27...@s.whatsapp.net' (best send target)
1979
+ * - canonicalKey('1234@lid') → 'thread:27...' (stable thread identifier)
1980
+ *
1981
+ * If LID has known PN mapping → use phone-number form
1982
+ * If only LID known → use LID stripped of suffix
1983
+ * Always lowercase, no @-suffix, prefixed with `thread:`
1984
+ *
1985
+ * Apps using this as their DB key won't double-thread on LID/PN drift.
1986
+ *
1987
+ * @param jid - WhatsApp JID (can be PN, LID, group, or broadcast)
1988
+ * @returns Stable thread key for DB indexing
1989
+ */
1990
+ canonicalKey(jid) {
1991
+ if (!jid || typeof jid !== 'string' || jid.trim() === '') {
1992
+ return 'thread:invalid'
1993
+ }
1994
+ const normalized = jid.trim().toLowerCase()
1995
+ const atIndex = normalized.indexOf('@')
1996
+ if (atIndex === -1) {
1997
+ return 'thread:invalid'
1998
+ }
1999
+ const user = normalized.substring(0, atIndex)
2000
+ const domain = normalized.substring(atIndex + 1)
2001
+ if (domain === 'g.us') {
2002
+ return `thread:group:${user}`
2003
+ }
2004
+ if (domain === 'broadcast') {
2005
+ return `thread:broadcast:${user}`
2006
+ }
2007
+ if (domain === 'newsletter') {
2008
+ return `thread:newsletter:${user}`
2009
+ }
2010
+ if (domain === 's.whatsapp.net') {
2011
+ this.stats.canonicalKeyHits++
2012
+ return `thread:${user}`
2013
+ }
2014
+ if (domain === 'lid') {
2015
+ const mapping = this.lidResolver.getMapping(normalized)
2016
+ if (mapping?.pn) {
2017
+ const pnUser = mapping.pn.split('@')[0]
2018
+ this.stats.canonicalKeyHits++
2019
+ return `thread:${pnUser}`
2020
+ } else {
2021
+ this.stats.canonicalKeyMisses++
2022
+ return `thread:lid:${user}`
2023
+ }
2024
+ }
2025
+ return `thread:${domain}:${user}`
2026
+ }
2027
+ /**
2028
+ * Called by wrapper on messages.upsert event. Learns mappings.
2029
+ */
2030
+ onIncomingEvent(upsert) {
2031
+ if (!this.config.enabled || !this.config.learnFromEvents) {
2032
+ return
2033
+ }
2034
+ for (const msg of upsert.messages || []) {
2035
+ this.learnFromMessage(msg)
2036
+ }
2037
+ }
2038
+ /**
2039
+ * Called by wrapper on messages.update event. Learns from sent-message refs.
2040
+ */
2041
+ onMessageUpdate(updates) {
2042
+ if (!this.config.enabled || !this.config.learnFromEvents) {
2043
+ return
2044
+ }
2045
+ for (const update of updates) {
2046
+ if (update.key) {
2047
+ this.learnFromMessageKey(update.key)
2048
+ }
2049
+ }
2050
+ }
2051
+ getStats() {
2052
+ return {
2053
+ resolver: this.lidResolver.getStats(),
2054
+ outboundCanonicalized: this.stats.outboundCanonicalized,
2055
+ outboundPassthrough: this.stats.outboundPassthrough,
2056
+ inboundLearned: this.stats.inboundLearned,
2057
+ canonicalKeyHits: this.stats.canonicalKeyHits,
2058
+ canonicalKeyMisses: this.stats.canonicalKeyMisses
2059
+ }
2060
+ }
2061
+ destroy() {
2062
+ if (this.ownsResolver) {
2063
+ this.lidResolver.destroy()
2064
+ }
2065
+ }
2066
+ // Private helpers
2067
+ /**
2068
+ * Extract LID↔PN mappings from a message object
2069
+ */
2070
+ learnFromMessage(msg) {
2071
+ if (!msg.key) return
2072
+ this.learnFromMessageKey(msg.key)
2073
+ if (msg.participantPn && msg.key.participant) {
2074
+ this.lidResolver.learn({
2075
+ lid: msg.key.participant.endsWith('@lid') ? msg.key.participant : void 0,
2076
+ pn: msg.participantPn
2077
+ })
2078
+ this.stats.inboundLearned++
2079
+ }
2080
+ }
2081
+ /**
2082
+ * Extract mappings from message.key
2083
+ */
2084
+ learnFromMessageKey(key) {
2085
+ if (!key) return
2086
+ if (key.participant && key.participantPn) {
2087
+ if (key.participant.endsWith('@lid')) {
2088
+ this.lidResolver.learn({
2089
+ lid: key.participant,
2090
+ pn: key.participantPn
2091
+ })
2092
+ this.stats.inboundLearned++
2093
+ }
2094
+ }
2095
+ if (key.remoteJid && key.senderPn) {
2096
+ if (key.remoteJid.endsWith('@lid')) {
2097
+ this.lidResolver.learn({
2098
+ lid: key.remoteJid,
2099
+ pn: key.senderPn
2100
+ })
2101
+ this.stats.inboundLearned++
2102
+ }
2103
+ }
2104
+ if (key.participant && key.remoteJid) {
2105
+ if (key.participant.endsWith('@s.whatsapp.net') && key.remoteJid.endsWith('@lid')) {
2106
+ this.lidResolver.learn({
2107
+ lid: key.remoteJid,
2108
+ pn: key.participant
2109
+ })
2110
+ this.stats.inboundLearned++
2111
+ }
2112
+ }
2113
+ }
2114
+ }
2115
+
2116
+ // sessionStability.js
2117
+ function classifyDisconnect(statusCode) {
2118
+ if (statusCode === 401 || statusCode === 440) {
2119
+ return {
2120
+ category: 'fatal',
2121
+ shouldReconnect: false,
2122
+ message: 'Logged out \u2014 restart with QR code required',
2123
+ code: statusCode
2124
+ }
2125
+ }
2126
+ if (statusCode === 515) {
2127
+ return {
2128
+ category: 'fatal',
2129
+ shouldReconnect: false,
2130
+ message: 'Restart required by WhatsApp \u2014 client too old or protocol mismatch',
2131
+ code: statusCode
2132
+ }
2133
+ }
2134
+ if (statusCode === 405) {
2135
+ return {
2136
+ category: 'fatal',
2137
+ shouldReconnect: false,
2138
+ message: 'Method not allowed \u2014 server rejected connection method',
2139
+ code: statusCode
2140
+ }
2141
+ }
2142
+ if (statusCode === 409 || statusCode === 428) {
2143
+ return {
2144
+ category: 'fatal',
2145
+ shouldReconnect: false,
2146
+ message: 'Connection replaced \u2014 another device took over',
2147
+ code: statusCode
2148
+ }
2149
+ }
2150
+ if (statusCode === 412) {
2151
+ return {
2152
+ category: 'recoverable',
2153
+ shouldReconnect: true,
2154
+ backoffMs: 3e4,
2155
+ // 30 seconds
2156
+ message: 'Precondition failed \u2014 auth state mismatch, retry after delay',
2157
+ code: statusCode
2158
+ }
2159
+ }
2160
+ if (statusCode === 429) {
2161
+ return {
2162
+ category: 'rate-limited',
2163
+ shouldReconnect: true,
2164
+ backoffMs: 3e5,
2165
+ // 5 minutes
2166
+ message: 'Rate limited by WhatsApp \u2014 cool-off period required',
2167
+ code: statusCode
2168
+ }
2169
+ }
2170
+ if (statusCode === 503) {
2171
+ return {
2172
+ category: 'rate-limited',
2173
+ shouldReconnect: true,
2174
+ backoffMs: 6e4,
2175
+ // 1 minute
2176
+ message: 'WhatsApp service unavailable \u2014 temporary outage',
2177
+ code: statusCode
2178
+ }
2179
+ }
2180
+ if (statusCode === 408) {
2181
+ return {
2182
+ category: 'recoverable',
2183
+ shouldReconnect: true,
2184
+ backoffMs: 5e3,
2185
+ // 5 seconds
2186
+ message: 'Connection timeout \u2014 network issue, safe to retry',
2187
+ code: statusCode
2188
+ }
2189
+ }
2190
+ if (statusCode === 500) {
2191
+ return {
2192
+ category: 'recoverable',
2193
+ shouldReconnect: true,
2194
+ backoffMs: 1e4,
2195
+ // 10 seconds
2196
+ message: 'WhatsApp internal error \u2014 temporary server issue',
2197
+ code: statusCode
2198
+ }
2199
+ }
2200
+ if (statusCode === 1e3) {
2201
+ return {
2202
+ category: 'recoverable',
2203
+ shouldReconnect: true,
2204
+ backoffMs: 2e3,
2205
+ // 2 seconds
2206
+ message: 'Connection closed gracefully \u2014 safe to reconnect',
2207
+ code: statusCode
2208
+ }
2209
+ }
2210
+ return {
2211
+ category: 'unknown',
2212
+ shouldReconnect: true,
2213
+ backoffMs: 15e3,
2214
+ // 15 seconds
2215
+ message: `Unknown disconnect reason (code ${statusCode}) \u2014 reconnect with caution`,
2216
+ code: statusCode
2217
+ }
2218
+ }
2219
+ var DEFAULT_HEALTH_CONFIG = {
2220
+ badMacThreshold: 3,
2221
+ badMacWindowMs: 6e4
2222
+ }
2223
+ var SessionHealthMonitor = class {
2224
+ config
2225
+ onDegraded
2226
+ onRecovered
2227
+ stats = {
2228
+ decryptSuccess: 0,
2229
+ decryptFail: 0,
2230
+ badMacCount: 0,
2231
+ isDegraded: false
2232
+ }
2233
+ badMacTimestamps = []
2234
+ constructor(config = {}) {
2235
+ this.config = { ...DEFAULT_HEALTH_CONFIG, ...config }
2236
+ this.onDegraded = config.onDegraded
2237
+ this.onRecovered = config.onRecovered
2238
+ }
2239
+ /**
2240
+ * Record successful decrypt
2241
+ */
2242
+ recordDecryptSuccess() {
2243
+ this.stats.decryptSuccess++
2244
+ this.checkRecovery()
2245
+ }
2246
+ /**
2247
+ * Record failed decrypt (Bad MAC or similar)
2248
+ */
2249
+ recordDecryptFail(isBadMac = false) {
2250
+ this.stats.decryptFail++
2251
+ if (isBadMac) {
2252
+ const now = Date.now()
2253
+ this.stats.badMacCount++
2254
+ this.stats.lastBadMac = new Date(now)
2255
+ this.badMacTimestamps.push(now)
2256
+ const cutoff = now - this.config.badMacWindowMs
2257
+ this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff)
2258
+ if (!this.stats.isDegraded && this.badMacTimestamps.length >= this.config.badMacThreshold) {
2259
+ this.stats.isDegraded = true
2260
+ this.stats.degradedSince = new Date(now)
2261
+ this.onDegraded?.(this.getStats())
2262
+ }
2263
+ }
2264
+ }
2265
+ /**
2266
+ * Check if session has recovered from degraded state
2267
+ */
2268
+ checkRecovery() {
2269
+ if (!this.stats.isDegraded) return
2270
+ const now = Date.now()
2271
+ const cutoff = now - this.config.badMacWindowMs
2272
+ this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff)
2273
+ if (this.badMacTimestamps.length < this.config.badMacThreshold) {
2274
+ this.stats.isDegraded = false
2275
+ this.stats.degradedSince = void 0
2276
+ this.onRecovered?.(this.getStats())
2277
+ }
2278
+ }
2279
+ /**
2280
+ * Get current health stats
2281
+ */
2282
+ getStats() {
2283
+ return { ...this.stats }
2284
+ }
2285
+ /**
2286
+ * Reset all counters
2287
+ */
2288
+ reset() {
2289
+ this.stats = {
2290
+ decryptSuccess: 0,
2291
+ decryptFail: 0,
2292
+ badMacCount: 0,
2293
+ isDegraded: false
2294
+ }
2295
+ this.badMacTimestamps = []
2296
+ }
2297
+ }
2298
+ function wrapWithSessionStability(sock, config = {}) {
2299
+ const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver } = config
2300
+ const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null
2301
+ return new Proxy(sock, {
2302
+ get(target, prop) {
2303
+ if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
2304
+ return async (jid, content, options) => {
2305
+ const canonical = lidResolver.resolveCanonical(jid)
2306
+ return target.sendMessage(canonical, content, options)
2307
+ }
2308
+ }
2309
+ if (prop === 'sessionHealthStats' && healthMonitor) {
2310
+ return healthMonitor.getStats()
2311
+ }
2312
+ if (prop === 'sessionHealthMonitor' && healthMonitor) {
2313
+ return healthMonitor
2314
+ }
2315
+ return target[prop]
2316
+ }
2317
+ })
2318
+ }
2319
+
2320
+ // presets.js
2321
+ var PRESETS = {
2322
+ conservative: {
2323
+ maxPerMinute: 5,
2324
+ maxPerHour: 100,
2325
+ maxPerDay: 800,
2326
+ minDelayMs: 2500,
2327
+ maxDelayMs: 7e3,
2328
+ newChatDelayMs: 4e3,
2329
+ warmupDays: 10,
2330
+ day1Limit: 15,
2331
+ growthFactor: 1.8,
2332
+ inactivityThresholdHours: 72,
2333
+ autoPauseAt: 'medium',
2334
+ groupMultiplier: 0.5,
2335
+ groupProfiles: false,
2336
+ logging: true
2337
+ },
2338
+ moderate: {
2339
+ maxPerMinute: 10,
2340
+ maxPerHour: 300,
2341
+ maxPerDay: 1500,
2342
+ minDelayMs: 1500,
2343
+ maxDelayMs: 5e3,
2344
+ newChatDelayMs: 3e3,
2345
+ warmupDays: 7,
2346
+ day1Limit: 20,
2347
+ growthFactor: 1.8,
2348
+ inactivityThresholdHours: 72,
2349
+ autoPauseAt: 'high',
2350
+ groupMultiplier: 0.7,
2351
+ groupProfiles: false,
2352
+ logging: true
2353
+ },
2354
+ aggressive: {
2355
+ maxPerMinute: 20,
2356
+ maxPerHour: 800,
2357
+ maxPerDay: 4e3,
2358
+ minDelayMs: 800,
2359
+ maxDelayMs: 3e3,
2360
+ newChatDelayMs: 2e3,
2361
+ warmupDays: 4,
2362
+ day1Limit: 35,
2363
+ growthFactor: 2,
2364
+ inactivityThresholdHours: 48,
2365
+ autoPauseAt: 'critical',
2366
+ groupMultiplier: 0.9,
2367
+ groupProfiles: false,
2368
+ logging: true
2369
+ }
2370
+ }
2371
+ function resolveConfig(input) {
2372
+ if (input === void 0) {
2373
+ return { ...PRESETS.conservative }
2374
+ }
2375
+ if (typeof input === 'string') {
2376
+ if (!(input in PRESETS)) {
2377
+ throw new Error(`Unknown preset "${input}". Valid: ${Object.keys(PRESETS).join(', ')}`)
2378
+ }
2379
+ return { ...PRESETS[input] }
2380
+ }
2381
+ const { preset = 'conservative', ...overrides } = input
2382
+ if (!(preset in PRESETS)) {
2383
+ throw new Error(`Unknown preset "${preset}". Valid: ${Object.keys(PRESETS).join(', ')}`)
2384
+ }
2385
+ return { ...PRESETS[preset], ...overrides }
2386
+ }
2387
+
2388
+ // persist.js
2389
+ var fs = __toESM(require('fs'), 1)
2390
+ var KNOWN_CHATS_MAX = 1e3
2391
+ var DEBOUNCE_MS = 5e3
2392
+ var StateManager = class {
2393
+ path
2394
+ debounceTimer = null
2395
+ constructor(filePath) {
2396
+ this.path = filePath
2397
+ }
2398
+ load() {
2399
+ try {
2400
+ const raw = fs.readFileSync(this.path, 'utf-8')
2401
+ const parsed = JSON.parse(raw)
2402
+ if (parsed.version !== 3) {
2403
+ process.stderr.write('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh\n')
2404
+ return null
2405
+ }
2406
+ return parsed
2407
+ } catch {
2408
+ if (fs.existsSync(this.path)) {
2409
+ process.stderr.write('[baileys-antiban] WARN: corrupt state file, starting fresh\n')
2410
+ }
2411
+ return null
2412
+ }
2413
+ }
2414
+ /** Debounced save — called after every send (5s delay) */
2415
+ saveDebounced(state) {
2416
+ if (this.debounceTimer) {
2417
+ clearTimeout(this.debounceTimer)
2418
+ }
2419
+ this.debounceTimer = setTimeout(() => {
2420
+ this.writeFile(state)
2421
+ this.debounceTimer = null
2422
+ }, DEBOUNCE_MS)
2423
+ }
2424
+ /** Immediate save — called after health events (ban/restriction) */
2425
+ saveImmediate(state) {
2426
+ if (this.debounceTimer) {
2427
+ clearTimeout(this.debounceTimer)
2428
+ this.debounceTimer = null
2429
+ }
2430
+ this.writeFile(state)
2431
+ }
2432
+ /** Flush/cancel pending debounced write (for tests and process exit) */
2433
+ flush() {
2434
+ if (this.debounceTimer) {
2435
+ clearTimeout(this.debounceTimer)
2436
+ this.debounceTimer = null
2437
+ }
2438
+ }
2439
+ destroy() {
2440
+ this.flush()
2441
+ }
2442
+ writeFile(state) {
2443
+ const toSave = {
2444
+ ...state,
2445
+ savedAt: Date.now(),
2446
+ // LRU eviction: keep last KNOWN_CHATS_MAX entries
2447
+ knownChats:
2448
+ state.knownChats.length > KNOWN_CHATS_MAX ? state.knownChats.slice(-KNOWN_CHATS_MAX) : state.knownChats
2449
+ }
2450
+ try {
2451
+ fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8')
2452
+ } catch (err) {
2453
+ process.stderr.write(`[baileys-antiban] WARN: failed to write state to ${this.path}: ${err}\n`)
2454
+ }
2455
+ }
2456
+ }
2457
+
2458
+ // profiles.js
2459
+ function isGroup(jid) {
2460
+ return jid.endsWith('@g.us')
2461
+ }
2462
+ function isNewsletter(jid) {
2463
+ return jid.endsWith('@newsletter')
2464
+ }
2465
+ function isBroadcast(jid) {
2466
+ return jid === 'status@broadcast' || jid.endsWith('@broadcast')
2467
+ }
2468
+ function shouldUseGroupProfile(jid) {
2469
+ return isGroup(jid) || isNewsletter(jid)
2470
+ }
2471
+ function applyGroupMultiplier(limits, multiplier) {
2472
+ return {
2473
+ maxPerMinute: Math.max(1, Math.floor(limits.maxPerMinute * multiplier)),
2474
+ maxPerHour: Math.max(1, Math.floor(limits.maxPerHour * multiplier)),
2475
+ maxPerDay: Math.max(1, Math.floor(limits.maxPerDay * multiplier))
2476
+ }
2477
+ }
2478
+
2479
+ // antiban.js
2480
+ function isLegacyConfig(cfg) {
2481
+ if (typeof cfg !== 'object' || cfg === null) return false
2482
+ return (
2483
+ 'rateLimiter' in cfg ||
2484
+ 'warmUp' in cfg ||
2485
+ 'health' in cfg ||
2486
+ 'timelock' in cfg ||
2487
+ 'replyRatio' in cfg ||
2488
+ 'contactGraph' in cfg ||
2489
+ 'presence' in cfg ||
2490
+ 'retryTracker' in cfg ||
2491
+ 'reconnectThrottle' in cfg ||
2492
+ 'lidResolver' in cfg ||
2493
+ 'jidCanonicalizer' in cfg ||
2494
+ 'sessionStability' in cfg
2495
+ )
2496
+ }
2497
+ function mapLegacyToFlat(legacy) {
2498
+ process.stderr.write('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. Migrate to flat config: new AntiBan({ maxPerMinute: 8 }).\n')
2499
+ const flat = {}
2500
+ if (legacy.rateLimiter?.maxPerMinute !== void 0) flat.maxPerMinute = legacy.rateLimiter.maxPerMinute
2501
+ if (legacy.rateLimiter?.maxPerHour !== void 0) flat.maxPerHour = legacy.rateLimiter.maxPerHour
2502
+ if (legacy.rateLimiter?.maxPerDay !== void 0) flat.maxPerDay = legacy.rateLimiter.maxPerDay
2503
+ if (legacy.rateLimiter?.minDelayMs !== void 0) flat.minDelayMs = legacy.rateLimiter.minDelayMs
2504
+ if (legacy.rateLimiter?.maxDelayMs !== void 0) flat.maxDelayMs = legacy.rateLimiter.maxDelayMs
2505
+ if (legacy.rateLimiter?.newChatDelayMs !== void 0) flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs
2506
+ if (legacy.warmUp?.warmUpDays !== void 0) flat.warmupDays = legacy.warmUp.warmUpDays
2507
+ if (legacy.warmUp?.day1Limit !== void 0) flat.day1Limit = legacy.warmUp.day1Limit
2508
+ if (legacy.warmUp?.growthFactor !== void 0) flat.growthFactor = legacy.warmUp.growthFactor
2509
+ if (legacy.logging !== void 0) flat.logging = legacy.logging
2510
+ return flat
2511
+ }
2512
+ var AntiBan = class {
2513
+ rateLimiter
2514
+ warmUp
2515
+ health
2516
+ timelockGuard
2517
+ replyRatioGuard
2518
+ contactGraphWarmer
2519
+ presenceChoreographer
2520
+ retryTrackerModule
2521
+ reconnectThrottleModule
2522
+ lidResolverModule = null
2523
+ jidCanonicalizerModule = null
2524
+ sessionStabilityMonitor = null
2525
+ stateManager = null
2526
+ resolvedConfig
2527
+ logging
2528
+ stats = {
2529
+ messagesAllowed: 0,
2530
+ messagesBlocked: 0,
2531
+ totalDelayMs: 0
2532
+ }
2533
+ constructor(input, warmUpStateArg) {
2534
+ let flatConfig
2535
+ let legacyPassthrough = null
2536
+ let warmUpState = warmUpStateArg
2537
+ if (isLegacyConfig(input)) {
2538
+ legacyPassthrough = input
2539
+ flatConfig = mapLegacyToFlat(legacyPassthrough)
2540
+ } else {
2541
+ flatConfig = {}
2542
+ legacyPassthrough = null
2543
+ }
2544
+ const cfg = isLegacyConfig(input) ? resolveConfig(flatConfig) : resolveConfig(input)
2545
+ this.resolvedConfig = cfg
2546
+ let savedState = null
2547
+ if (cfg.persist) {
2548
+ this.stateManager = new StateManager(cfg.persist)
2549
+ savedState = this.stateManager.load()
2550
+ if (savedState) {
2551
+ warmUpState = savedState.warmup
2552
+ }
2553
+ }
2554
+ this.logging = cfg.logging ?? true
2555
+ this._log = (msg) => { if (this.logging) process.stdout.write(`[antiban] ${msg}\n`) }
2556
+ this.rateLimiter = new RateLimiter({
2557
+ maxPerMinute: cfg.maxPerMinute,
2558
+ maxPerHour: cfg.maxPerHour,
2559
+ maxPerDay: cfg.maxPerDay,
2560
+ minDelayMs: cfg.minDelayMs,
2561
+ maxDelayMs: cfg.maxDelayMs,
2562
+ newChatDelayMs: cfg.newChatDelayMs,
2563
+ ...(legacyPassthrough?.rateLimiter || {})
2564
+ })
2565
+ if (savedState?.knownChats) {
2566
+ this.rateLimiter.restoreKnownChats(savedState.knownChats)
2567
+ }
2568
+ this.warmUp = new WarmUp(
2569
+ {
2570
+ warmUpDays: cfg.warmupDays,
2571
+ day1Limit: cfg.day1Limit,
2572
+ growthFactor: cfg.growthFactor,
2573
+ inactivityThresholdHours: cfg.inactivityThresholdHours,
2574
+ ...(legacyPassthrough?.warmUp || {})
2575
+ },
2576
+ warmUpState
2577
+ )
2578
+ this.health = new HealthMonitor({
2579
+ autoPauseAt: cfg.autoPauseAt,
2580
+ ...(legacyPassthrough?.health || {}),
2581
+ onRiskChange: status => {
2582
+ const emoji = { low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' }
2583
+ this._log(`${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`)
2584
+ this._log(status.recommendation)
2585
+ status.reasons.forEach(r => this._log(` \u2192 ${r}`))
2586
+ legacyPassthrough?.health?.onRiskChange?.(status)
2587
+ }
2588
+ })
2589
+ this.timelockGuard = new TimelockGuard({
2590
+ ...(legacyPassthrough?.timelock || {}),
2591
+ onTimelockDetected: state => {
2592
+ this.health.recordReachoutTimelock(state.enforcementType)
2593
+ this._log(`REACHOUT TIMELOCKED \u2014 ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`)
2594
+ legacyPassthrough?.timelock?.onTimelockDetected?.(state)
2595
+ },
2596
+ onTimelockLifted: state => {
2597
+ this._log('Timelock lifted \u2014 resuming new contact messages')
2598
+ legacyPassthrough?.timelock?.onTimelockLifted?.(state)
2599
+ }
2600
+ })
2601
+ this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio)
2602
+ this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph)
2603
+ this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence)
2604
+ this.retryTrackerModule = new RetryReasonTracker({
2605
+ ...(legacyPassthrough?.retryTracker || {}),
2606
+ onSpiral: (msgId, reason) => {
2607
+ this._log(`\u26A0\uFE0F Message ${msgId} stuck in retry spiral (${reason})`)
2608
+ legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason)
2609
+ }
2610
+ })
2611
+ this.reconnectThrottleModule = new PostReconnectThrottle({
2612
+ ...(legacyPassthrough?.reconnectThrottle || {}),
2613
+ baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute
2614
+ })
2615
+ if (legacyPassthrough?.jidCanonicalizer?.enabled) {
2616
+ if (legacyPassthrough.jidCanonicalizer.resolver) {
2617
+ this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer)
2618
+ this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver
2619
+ } else {
2620
+ const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig
2621
+ const resolver = new LidResolver(resolverConfig)
2622
+ this.lidResolverModule = resolver
2623
+ this.jidCanonicalizerModule = new JidCanonicalizer({
2624
+ ...legacyPassthrough.jidCanonicalizer,
2625
+ resolver
2626
+ })
2627
+ }
2628
+ } else if (legacyPassthrough?.lidResolver) {
2629
+ this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver)
2630
+ }
2631
+ if (legacyPassthrough?.sessionStability?.enabled) {
2632
+ const healthConfig = {
2633
+ badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
2634
+ badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
2635
+ onDegraded: stats => {
2636
+ this._log(`\u{1F534} SESSION DEGRADED \u2014 Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 6e4}ms`)
2637
+ this._log('Consider restarting session or switching to LID-based canonical form')
2638
+ },
2639
+ onRecovered: () => {
2640
+ this._log('\u{1F7E2} SESSION RECOVERED \u2014 decrypt success rate improved')
2641
+ }
2642
+ }
2643
+ this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig)
2644
+ }
2645
+ }
2646
+ /**
2647
+ * Check if a message can be sent and get required delay.
2648
+ * Call this BEFORE every sendMessage().
2649
+ */
2650
+ async beforeSend(recipient, content) {
2651
+ const healthStatus = this.health.getStatus()
2652
+ if (this.health.isPaused()) {
2653
+ this.stats.messagesBlocked++
2654
+ this._log(`\u26D4 BLOCKED \u2014 health risk too high (${healthStatus.risk})`)
2655
+ return {
2656
+ allowed: false,
2657
+ delayMs: 0,
2658
+ reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
2659
+ health: healthStatus
2660
+ }
2661
+ }
2662
+ const timelockDecision = this.timelockGuard.canSend(recipient)
2663
+ if (!timelockDecision.allowed) {
2664
+ this.stats.messagesBlocked++
2665
+ this._log(`TIMELOCKED \u2014 ${timelockDecision.reason}`)
2666
+ return {
2667
+ allowed: false,
2668
+ delayMs: 0,
2669
+ reason: timelockDecision.reason,
2670
+ health: healthStatus
2671
+ }
2672
+ }
2673
+ if (!this.warmUp.canSend()) {
2674
+ this.stats.messagesBlocked++
2675
+ const warmUpStatus = this.warmUp.getStatus()
2676
+ this._log(`\u23F3 BLOCKED \u2014 warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`)
2677
+ return {
2678
+ allowed: false,
2679
+ delayMs: 0,
2680
+ reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
2681
+ health: healthStatus,
2682
+ warmUpDay: warmUpStatus.day
2683
+ }
2684
+ }
2685
+ const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient)
2686
+ if (!contactGraphDecision.allowed) {
2687
+ this.stats.messagesBlocked++
2688
+ this._log(`\u{1F4CA} BLOCKED \u2014 contact graph: ${contactGraphDecision.reason}`)
2689
+ return {
2690
+ allowed: false,
2691
+ delayMs: 0,
2692
+ reason: `Contact graph: ${contactGraphDecision.reason}`,
2693
+ health: healthStatus
2694
+ }
2695
+ }
2696
+ const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient)
2697
+ if (!replyRatioDecision.allowed) {
2698
+ this.stats.messagesBlocked++
2699
+ this._log(`\u{1F4AC} BLOCKED \u2014 reply ratio: ${replyRatioDecision.reason}`)
2700
+ return {
2701
+ allowed: false,
2702
+ delayMs: 0,
2703
+ reason: `Reply ratio: ${replyRatioDecision.reason}`,
2704
+ health: healthStatus
2705
+ }
2706
+ }
2707
+ const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend()
2708
+ if (!reconnectThrottleDecision.allowed) {
2709
+ this.stats.messagesBlocked++
2710
+ this._log(`\u{1F504} BLOCKED \u2014 reconnect throttle: ${reconnectThrottleDecision.reason}`)
2711
+ return {
2712
+ allowed: false,
2713
+ delayMs: reconnectThrottleDecision.retryAfterMs || 0,
2714
+ reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
2715
+ health: healthStatus
2716
+ }
2717
+ }
2718
+ if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
2719
+ const groupLimits = applyGroupMultiplier(
2720
+ {
2721
+ maxPerMinute: this.resolvedConfig.maxPerMinute,
2722
+ maxPerHour: this.resolvedConfig.maxPerHour,
2723
+ maxPerDay: this.resolvedConfig.maxPerDay
2724
+ },
2725
+ this.resolvedConfig.groupMultiplier
2726
+ )
2727
+ const stats = this.rateLimiter.getStats()
2728
+ if (
2729
+ stats.lastMinute >= groupLimits.maxPerMinute ||
2730
+ stats.lastHour >= groupLimits.maxPerHour ||
2731
+ stats.lastDay >= groupLimits.maxPerDay
2732
+ ) {
2733
+ this.stats.messagesBlocked++
2734
+ this._log(`\u{1F6AB} BLOCKED \u2014 group rate limit exceeded for ${recipient}`)
2735
+ return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus }
2736
+ }
2737
+ }
2738
+ let delay = await this.rateLimiter.getDelay(recipient, content)
2739
+ if (delay === -1) {
2740
+ this.stats.messagesBlocked++
2741
+ this._log(`\u{1F6AB} BLOCKED \u2014 rate limit or identical message spam`)
2742
+ return {
2743
+ allowed: false,
2744
+ delayMs: 0,
2745
+ reason: 'Rate limit exceeded or identical message spam detected',
2746
+ health: healthStatus
2747
+ }
2748
+ }
2749
+ const activityFactor = this.presenceChoreographer.getCurrentActivityFactor()
2750
+ if (activityFactor < 1) {
2751
+ const multiplier = Math.min(5, 1 / activityFactor)
2752
+ delay = Math.floor(delay * multiplier)
2753
+ }
2754
+ const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction()
2755
+ if (distractionCheck.pause) {
2756
+ delay += distractionCheck.durationMs
2757
+ this._log(`\u23F8\uFE0F Distraction pause: +${Math.floor(distractionCheck.durationMs / 6e4)}min`)
2758
+ }
2759
+ const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap()
2760
+ if (offlineCheck.offline) {
2761
+ delay += offlineCheck.durationMs
2762
+ this._log(`\u{1F4F4} Offline gap: +${Math.floor(offlineCheck.durationMs / 6e4)}min`)
2763
+ }
2764
+ this.stats.totalDelayMs += delay
2765
+ return {
2766
+ allowed: true,
2767
+ delayMs: delay,
2768
+ health: healthStatus
2769
+ }
2770
+ }
2771
+ /**
2772
+ * Record a successfully sent message.
2773
+ * Call this AFTER every successful sendMessage().
2774
+ */
2775
+ afterSend(recipient, content) {
2776
+ this.rateLimiter.record(recipient, content)
2777
+ this.warmUp.record()
2778
+ this.replyRatioGuard.recordSent(recipient)
2779
+ this.stats.messagesAllowed++
2780
+ this.persistStateDebounced()
2781
+ }
2782
+ /**
2783
+ * Record a failed message send
2784
+ */
2785
+ afterSendFailed(error) {
2786
+ this.health.recordMessageFailed(error)
2787
+ }
2788
+ /**
2789
+ * Record a disconnection (call from connection.update handler)
2790
+ */
2791
+ onDisconnect(reason) {
2792
+ this.health.recordDisconnect(reason)
2793
+ this.reconnectThrottleModule.onDisconnect()
2794
+ const reasonStr = String(reason)
2795
+ if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
2796
+ this.persistStateImmediate()
2797
+ }
2798
+ }
2799
+ /**
2800
+ * Record a successful reconnection
2801
+ */
2802
+ onReconnect() {
2803
+ this.health.recordReconnect()
2804
+ this.reconnectThrottleModule.onReconnect()
2805
+ }
2806
+ /**
2807
+ * Handle incoming message — record in reply ratio + contact graph.
2808
+ * Returns suggested reply if reply ratio suggests auto-reply.
2809
+ */
2810
+ onIncomingMessage(jid, msgText) {
2811
+ this.replyRatioGuard.recordReceived(jid)
2812
+ this.contactGraphWarmer.onIncomingMessage(jid)
2813
+ return this.replyRatioGuard.suggestReply(jid, msgText)
2814
+ }
2815
+ /**
2816
+ * Get comprehensive stats
2817
+ */
2818
+ getStats() {
2819
+ const stats = {
2820
+ ...this.stats,
2821
+ health: this.health.getStatus(),
2822
+ warmUp: this.warmUp.getStatus(),
2823
+ rateLimiter: this.rateLimiter.getStats()
2824
+ }
2825
+ if (this.replyRatioGuard['config']?.enabled) {
2826
+ stats.replyRatio = this.replyRatioGuard.getStats()
2827
+ }
2828
+ if (this.contactGraphWarmer['config']?.enabled) {
2829
+ stats.contactGraph = this.contactGraphWarmer.getStats()
2830
+ }
2831
+ if (this.presenceChoreographer['config']?.enabled) {
2832
+ stats.presence = this.presenceChoreographer.getStats()
2833
+ }
2834
+ if (this.retryTrackerModule['config']?.enabled) {
2835
+ stats.retryTracker = this.retryTrackerModule.getStats()
2836
+ }
2837
+ if (this.reconnectThrottleModule['config']?.enabled) {
2838
+ stats.reconnectThrottle = this.reconnectThrottleModule.getStats()
2839
+ }
2840
+ if (this.lidResolverModule) {
2841
+ stats.lidResolver = this.lidResolverModule.getStats()
2842
+ }
2843
+ if (this.jidCanonicalizerModule) {
2844
+ stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats()
2845
+ }
2846
+ if (this.sessionStabilityMonitor) {
2847
+ stats.sessionStability = this.sessionStabilityMonitor.getStats()
2848
+ }
2849
+ return stats
2850
+ }
2851
+ /** Get the timelock guard for direct access */
2852
+ get timelock() {
2853
+ return this.timelockGuard
2854
+ }
2855
+ /** Get the reply ratio guard for direct access */
2856
+ get replyRatio() {
2857
+ return this.replyRatioGuard
2858
+ }
2859
+ /** Get the contact graph warmer for direct access */
2860
+ get contactGraph() {
2861
+ return this.contactGraphWarmer
2862
+ }
2863
+ /** Get the presence choreographer for direct access */
2864
+ get presence() {
2865
+ return this.presenceChoreographer
2866
+ }
2867
+ /** Get the retry tracker for direct access */
2868
+ get retryTracker() {
2869
+ return this.retryTrackerModule
2870
+ }
2871
+ /** Get the reconnect throttle for direct access */
2872
+ get reconnectThrottle() {
2873
+ return this.reconnectThrottleModule
2874
+ }
2875
+ /** Get the LID resolver for direct access */
2876
+ get lidResolver() {
2877
+ return this.lidResolverModule
2878
+ }
2879
+ /** Get the JID canonicalizer for direct access */
2880
+ get jidCanonicalizer() {
2881
+ return this.jidCanonicalizerModule
2882
+ }
2883
+ /** Get the session stability monitor for direct access */
2884
+ get sessionStability() {
2885
+ return this.sessionStabilityMonitor
2886
+ }
2887
+ /**
2888
+ * Export warm-up state for persistence between restarts
2889
+ */
2890
+ exportWarmUpState() {
2891
+ return this.warmUp.exportState()
2892
+ }
2893
+ /**
2894
+ * Force pause all sending
2895
+ */
2896
+ pause() {
2897
+ this.health.setPaused(true)
2898
+ this._log('\u23F8\uFE0F Sending paused manually')
2899
+ }
2900
+ /**
2901
+ * Resume sending
2902
+ */
2903
+ resume() {
2904
+ this.health.setPaused(false)
2905
+ this._log('\u25B6\uFE0F Sending resumed')
2906
+ }
2907
+ /**
2908
+ * Reset everything (use after a ban period)
2909
+ */
2910
+ reset() {
2911
+ this.timelockGuard.reset()
2912
+ this.health.reset()
2913
+ this.warmUp.reset()
2914
+ this.replyRatioGuard.reset()
2915
+ this.contactGraphWarmer.reset()
2916
+ this.presenceChoreographer.reset()
2917
+ this.retryTrackerModule.destroy()
2918
+ this.reconnectThrottleModule.destroy()
2919
+ this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 }
2920
+ this._log('\u{1F504} Reset \u2014 starting fresh warm-up')
2921
+ }
2922
+ persistStateDebounced() {
2923
+ if (!this.stateManager) return
2924
+ const state = {
2925
+ warmup: this.warmUp.exportState(),
2926
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
2927
+ savedAt: Date.now(),
2928
+ version: 3
2929
+ }
2930
+ this.stateManager.saveDebounced(state)
2931
+ }
2932
+ persistStateImmediate() {
2933
+ if (!this.stateManager) return
2934
+ const state = {
2935
+ warmup: this.warmUp.exportState(),
2936
+ knownChats: Array.from(this.rateLimiter.getKnownChats()),
2937
+ savedAt: Date.now(),
2938
+ version: 3
2939
+ }
2940
+ this.stateManager.saveImmediate(state)
2941
+ }
2942
+ /**
2943
+ * Clean up all timers and resources.
2944
+ * Call this when disposing of the AntiBan instance or when the socket closes.
2945
+ */
2946
+ destroy() {
2947
+ this.stateManager?.destroy()
2948
+ this.timelockGuard.reset()
2949
+ this.replyRatioGuard.reset()
2950
+ this.contactGraphWarmer.reset()
2951
+ this.presenceChoreographer.reset()
2952
+ this.retryTrackerModule.destroy()
2953
+ this.reconnectThrottleModule.destroy()
2954
+ this.jidCanonicalizerModule?.destroy()
2955
+ this.lidResolverModule?.destroy()
2956
+ this.sessionStabilityMonitor?.reset()
2957
+ this._log('\u{1F9F9} Destroyed \u2014 all timers cleared')
2958
+ }
2959
+ }
2960
+
2961
+ // lidFirstResolver.js
2962
+ var fs2 = __toESM(require('fs'), 1)
2963
+ var path = __toESM(require('path'), 1)
2964
+ var LidFirstResolver = class {
2965
+ lidToPhone = /* @__PURE__ */ new Map()
2966
+ phoneToLid = /* @__PURE__ */ new Map()
2967
+ // phone → lid (quick reverse lookup)
2968
+ /**
2969
+ * Load mappings from Baileys auth state directory.
2970
+ * Looks for lid-mapping-*_reverse.json files.
2971
+ */
2972
+ loadFromAuthDir(authDir) {
2973
+ try {
2974
+ if (!fs2.existsSync(authDir)) {
2975
+ return
2976
+ }
2977
+ const files = fs2.readdirSync(authDir)
2978
+ const reverseMappingFiles = files.filter(f => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'))
2979
+ for (const file of reverseMappingFiles) {
2980
+ const filePath = path.join(authDir, file)
2981
+ const content = fs2.readFileSync(filePath, 'utf-8')
2982
+ const data = JSON.parse(content)
2983
+ for (const [lid, pnJid] of Object.entries(data)) {
2984
+ if (typeof pnJid === 'string') {
2985
+ const phone = this.extractPhone(pnJid)
2986
+ if (phone && lid.endsWith('@lid')) {
2987
+ const mapping = {
2988
+ lid: this.normalizeLid(lid),
2989
+ phone,
2990
+ learnedAt: Date.now(),
2991
+ source: 'auth-dir'
2992
+ }
2993
+ this.lidToPhone.set(mapping.lid, mapping)
2994
+ this.phoneToLid.set(phone, mapping.lid)
2995
+ }
2996
+ }
2997
+ }
2998
+ }
2999
+ } catch (error) {}
3000
+ }
3001
+ /**
3002
+ * Learn a new mapping from a Baileys event (messages, contacts, etc.).
3003
+ * Accepts partial data — will extract what it can.
3004
+ */
3005
+ learnFromEvent(event) {
3006
+ try {
3007
+ if (event.key?.remoteJid) {
3008
+ const jid = event.key.remoteJid
3009
+ this.learnJid(jid, 'event')
3010
+ }
3011
+ if (event.key?.participant) {
3012
+ const jid = event.key.participant
3013
+ this.learnJid(jid, 'event')
3014
+ }
3015
+ if (event.id) {
3016
+ this.learnJid(event.id, 'event')
3017
+ }
3018
+ if (event.pushName && event.key?.remoteJid) {
3019
+ this.learnJid(event.key.remoteJid, 'event')
3020
+ }
3021
+ } catch (error) {}
3022
+ }
3023
+ /**
3024
+ * Resolve phone number or phone JID to LID JID.
3025
+ * Returns null if not known.
3026
+ */
3027
+ resolveToLID(phoneOrJid) {
3028
+ const phone = this.extractPhone(phoneOrJid)
3029
+ if (!phone) return null
3030
+ return this.phoneToLid.get(phone) || null
3031
+ }
3032
+ /**
3033
+ * Resolve LID JID to phone number.
3034
+ * Returns null if not known.
3035
+ */
3036
+ resolveToPhone(lid) {
3037
+ const normalized = this.normalizeLid(lid)
3038
+ const mapping = this.lidToPhone.get(normalized)
3039
+ return mapping ? mapping.phone : null
3040
+ }
3041
+ /**
3042
+ * Get full mapping for a given JID (either LID or phone).
3043
+ * Returns null if not known.
3044
+ */
3045
+ getMapping(jid) {
3046
+ const normalized = this.normalizeLid(jid)
3047
+ const byLid = this.lidToPhone.get(normalized)
3048
+ if (byLid) return byLid
3049
+ const phone = this.extractPhone(jid)
3050
+ if (phone) {
3051
+ const lid = this.phoneToLid.get(phone)
3052
+ if (lid) return this.lidToPhone.get(lid) || null
3053
+ }
3054
+ return null
3055
+ }
3056
+ /**
3057
+ * Get total number of known mappings.
3058
+ */
3059
+ size() {
3060
+ return this.lidToPhone.size
3061
+ }
3062
+ /**
3063
+ * Clear all mappings.
3064
+ */
3065
+ clear() {
3066
+ this.lidToPhone.clear()
3067
+ this.phoneToLid.clear()
3068
+ }
3069
+ // Private helpers
3070
+ learnJid(_jid, _source) {}
3071
+ extractPhone(jid) {
3072
+ if (!jid) return null
3073
+ let cleaned = jid.replace('@s.whatsapp.net', '')
3074
+ cleaned = cleaned.replace(/:\d+$/, '')
3075
+ if (/^\d+$/.test(cleaned)) {
3076
+ return cleaned
3077
+ }
3078
+ return null
3079
+ }
3080
+ normalizeLid(lid) {
3081
+ return lid.replace(/:\d+@/, '@')
3082
+ }
3083
+ }
3084
+ function createLidFirstResolver() {
3085
+ return new LidFirstResolver()
3086
+ }
3087
+
3088
+ // retryReason.js
3089
+ var MessageRetryReason
3090
+ ;(function (MessageRetryReason2) {
3091
+ MessageRetryReason2[(MessageRetryReason2['UnknownError'] = 0)] = 'UnknownError'
3092
+ MessageRetryReason2[(MessageRetryReason2['GenericError'] = 1)] = 'GenericError'
3093
+ MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidKeyId'] = 3)] = 'SignalErrorInvalidKeyId'
3094
+ MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidMessage'] = 4)] = 'SignalErrorInvalidMessage'
3095
+ MessageRetryReason2[(MessageRetryReason2['SignalErrorNoSession'] = 5)] = 'SignalErrorNoSession'
3096
+ MessageRetryReason2[(MessageRetryReason2['SignalErrorBadMac'] = 7)] = 'SignalErrorBadMac'
3097
+ MessageRetryReason2[(MessageRetryReason2['MessageExpired'] = 8)] = 'MessageExpired'
3098
+ MessageRetryReason2[(MessageRetryReason2['DecryptionError'] = 9)] = 'DecryptionError'
3099
+ })(MessageRetryReason || (MessageRetryReason = {}))
3100
+ var MAC_ERROR_CODES = /* @__PURE__ */ new Set([
3101
+ MessageRetryReason.SignalErrorBadMac,
3102
+ MessageRetryReason.SignalErrorInvalidMessage,
3103
+ MessageRetryReason.SignalErrorNoSession,
3104
+ MessageRetryReason.SignalErrorInvalidKeyId
3105
+ ])
3106
+ function parseRetryReason(code) {
3107
+ if (code === void 0 || code === null) {
3108
+ return MessageRetryReason.UnknownError
3109
+ }
3110
+ const n = typeof code === 'string' ? parseInt(code, 10) : code
3111
+ if (isNaN(n)) {
3112
+ return MessageRetryReason.UnknownError
3113
+ }
3114
+ if (Object.values(MessageRetryReason).includes(n)) {
3115
+ return n
3116
+ }
3117
+ return MessageRetryReason.UnknownError
3118
+ }
3119
+ function isMacError(reason) {
3120
+ return MAC_ERROR_CODES.has(reason)
3121
+ }
3122
+ function getRetryReasonDescription(reason) {
3123
+ switch (reason) {
3124
+ case MessageRetryReason.UnknownError:
3125
+ return 'Unknown error'
3126
+ case MessageRetryReason.GenericError:
3127
+ return 'Generic error'
3128
+ case MessageRetryReason.SignalErrorInvalidKeyId:
3129
+ return 'Invalid key ID \u2014 peer prekey rotated'
3130
+ case MessageRetryReason.SignalErrorInvalidMessage:
3131
+ return 'Invalid message format'
3132
+ case MessageRetryReason.SignalErrorNoSession:
3133
+ return 'No session \u2014 peer not initialized'
3134
+ case MessageRetryReason.SignalErrorBadMac:
3135
+ return 'Bad MAC \u2014 encryption session mismatch'
3136
+ case MessageRetryReason.MessageExpired:
3137
+ return 'Message expired \u2014 too old to decrypt'
3138
+ case MessageRetryReason.DecryptionError:
3139
+ return 'Decryption failed'
3140
+ default:
3141
+ return `Unknown reason code ${reason}`
3142
+ }
3143
+ }
3144
+
3145
+ // wrapper.js
3146
+ function wrapSocket(sock, config, warmUpState, wrapOptions) {
3147
+ const antiban = new AntiBan(config, warmUpState)
3148
+ const options = {
3149
+ autoRespondToIncoming: false,
3150
+ ...wrapOptions
3151
+ }
3152
+ if (typeof sock.ev.process === 'function') {
3153
+ sock.ev.process(async events => {
3154
+ if (events['connection.update']) {
3155
+ const update = events['connection.update']
3156
+ if (update.connection === 'close') {
3157
+ const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown'
3158
+ antiban.onDisconnect(reason)
3159
+ }
3160
+ if (update.connection === 'open') {
3161
+ antiban.onReconnect()
3162
+ }
3163
+ if (update.reachoutTimeLock) {
3164
+ antiban.timelock.onTimelockUpdate({
3165
+ isActive: update.reachoutTimeLock.isActive,
3166
+ timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
3167
+ enforcementType: update.reachoutTimeLock.enforcementType
3168
+ })
3169
+ }
3170
+ }
3171
+ if (events['messages.update']) {
3172
+ const updates = events['messages.update']
3173
+ for (const update of updates) {
3174
+ if (update?.update?.messageStubParameters) {
3175
+ const params = update.update.messageStubParameters
3176
+ if (params.includes(463) || params.includes('463')) {
3177
+ antiban.timelock.record463Error()
3178
+ }
3179
+ }
3180
+ antiban.retryTracker.onMessageUpdate(update)
3181
+ }
3182
+ antiban.jidCanonicalizer?.onMessageUpdate(updates)
3183
+ }
3184
+ if (events['messages.upsert']) {
3185
+ const { messages } = events['messages.upsert']
3186
+ antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert'])
3187
+ for (const msg of messages || []) {
3188
+ const jid = msg.key?.remoteJid
3189
+ if (!jid) continue
3190
+ antiban.timelock.registerKnownChat(jid)
3191
+ const isSelf = msg.key?.fromMe || false
3192
+ if (isSelf) continue
3193
+ const msgText =
3194
+ msg.message?.conversation ||
3195
+ msg.message?.extendedTextMessage?.text ||
3196
+ msg.message?.imageMessage?.caption ||
3197
+ msg.message?.videoMessage?.caption ||
3198
+ ''
3199
+ const replySuggestion = antiban.onIncomingMessage(jid, msgText)
3200
+ if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
3201
+ const replyDelay = Math.floor(Math.random() * 12e3) + 3e3
3202
+ setTimeout(async () => {
3203
+ try {
3204
+ await sock.sendMessage(jid, { text: replySuggestion.suggestedText })
3205
+ } catch (error) {}
3206
+ }, replyDelay)
3207
+ }
3208
+ }
3209
+ }
3210
+ })
3211
+ } else {
3212
+ sock.ev.on('connection.update', update => {
3213
+ if (update.connection === 'close') {
3214
+ const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown'
3215
+ antiban.onDisconnect(reason)
3216
+ }
3217
+ if (update.connection === 'open') {
3218
+ antiban.onReconnect()
3219
+ }
3220
+ if (update.reachoutTimeLock) {
3221
+ antiban.timelock.onTimelockUpdate({
3222
+ isActive: update.reachoutTimeLock.isActive,
3223
+ timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
3224
+ enforcementType: update.reachoutTimeLock.enforcementType
3225
+ })
3226
+ }
3227
+ })
3228
+ sock.ev.on('messages.update', updates => {
3229
+ for (const update of updates) {
3230
+ if (update?.update?.messageStubParameters) {
3231
+ const params = update.update.messageStubParameters
3232
+ if (params.includes(463) || params.includes('463')) {
3233
+ antiban.timelock.record463Error()
3234
+ }
3235
+ }
3236
+ antiban.retryTracker.onMessageUpdate(update)
3237
+ }
3238
+ antiban.jidCanonicalizer?.onMessageUpdate(updates)
3239
+ })
3240
+ sock.ev.on('messages.upsert', upsert => {
3241
+ const { messages } = upsert
3242
+ antiban.jidCanonicalizer?.onIncomingEvent(upsert)
3243
+ for (const msg of messages || []) {
3244
+ const jid = msg.key?.remoteJid
3245
+ if (!jid) continue
3246
+ antiban.timelock.registerKnownChat(jid)
3247
+ const isSelf = msg.key?.fromMe || false
3248
+ if (isSelf) continue
3249
+ const msgText =
3250
+ msg.message?.conversation ||
3251
+ msg.message?.extendedTextMessage?.text ||
3252
+ msg.message?.imageMessage?.caption ||
3253
+ msg.message?.videoMessage?.caption ||
3254
+ ''
3255
+ const replySuggestion = antiban.onIncomingMessage(jid, msgText)
3256
+ if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
3257
+ const replyDelay = Math.floor(Math.random() * 12e3) + 3e3
3258
+ setTimeout(async () => {
3259
+ try {
3260
+ await sock.sendMessage(jid, { text: replySuggestion.suggestedText })
3261
+ } catch (error) {}
3262
+ }, replyDelay)
3263
+ }
3264
+ }
3265
+ })
3266
+ }
3267
+ const originalSendMessage = sock.sendMessage.bind(sock)
3268
+ const wrappedSendMessage = async (jid, content, options2) => {
3269
+ const canonicalJid = antiban.jidCanonicalizer?.canonicalizeTarget(jid) || jid
3270
+ const text = content?.text || content?.caption || content?.image?.caption || ''
3271
+ const decision = await antiban.beforeSend(canonicalJid, text)
3272
+ if (!decision.allowed) {
3273
+ throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`)
3274
+ }
3275
+ if (decision.delayMs > 0) {
3276
+ await new Promise(resolve => setTimeout(resolve, decision.delayMs))
3277
+ }
3278
+ try {
3279
+ const result = await originalSendMessage(canonicalJid, content, options2)
3280
+ antiban.afterSend(canonicalJid, text)
3281
+ antiban.timelock.registerKnownChat(canonicalJid)
3282
+ if (result?.key?.id) {
3283
+ antiban.retryTracker.clear(result.key.id)
3284
+ }
3285
+ return result
3286
+ } catch (error) {
3287
+ antiban.afterSendFailed(error instanceof Error ? error.message : String(error))
3288
+ throw error
3289
+ }
3290
+ }
3291
+ const wrapped = Object.create(sock)
3292
+ wrapped.sendMessage = wrappedSendMessage
3293
+ wrapped.antiban = antiban
3294
+ wrapped.antiban.destroy = antiban.destroy.bind(antiban)
3295
+ return wrapped
3296
+ }
3297
+
3298
+ // messageQueue.js
3299
+ var import_events = require('events')
3300
+ var DEFAULT_CONFIG12 = {
3301
+ maxAttempts: 3,
3302
+ retryBaseDelayMs: 3e4,
3303
+ maxQueueSize: 1e3,
3304
+ priorityOrder: true
3305
+ }
3306
+ var MessageQueue = class extends import_events.EventEmitter {
3307
+ config
3308
+ queue = []
3309
+ processing = false
3310
+ sendFn = null
3311
+ drainTimer = null
3312
+ idCounter = 0
3313
+ constructor(config = {}) {
3314
+ super()
3315
+ this.config = { ...DEFAULT_CONFIG12, ...config }
3316
+ }
3317
+ /**
3318
+ * Set the send function (called for each message when drained)
3319
+ * This should be the anti-ban wrapped sendMessage
3320
+ */
3321
+ setSendFunction(fn) {
3322
+ this.sendFn = fn
3323
+ }
3324
+ /**
3325
+ * Add a message to the queue
3326
+ */
3327
+ add(recipient, content, options) {
3328
+ if (this.queue.length >= this.config.maxQueueSize) {
3329
+ throw new Error(`Queue full (${this.config.maxQueueSize} messages)`)
3330
+ }
3331
+ const id = `msg_${Date.now()}_${++this.idCounter}`
3332
+ const message = {
3333
+ id,
3334
+ recipient,
3335
+ content,
3336
+ priority: options?.priority || 'normal',
3337
+ addedAt: Date.now(),
3338
+ attempts: 0,
3339
+ maxAttempts: this.config.maxAttempts,
3340
+ scheduledFor: options?.scheduledFor?.getTime(),
3341
+ metadata: options?.metadata
3342
+ }
3343
+ this.queue.push(message)
3344
+ this.sortQueue()
3345
+ this.emit('added', message)
3346
+ return id
3347
+ }
3348
+ /**
3349
+ * Add multiple messages (e.g., broadcast to many recipients)
3350
+ */
3351
+ addBulk(recipients, content, options) {
3352
+ return recipients.map(r => this.add(r, content, options))
3353
+ }
3354
+ /**
3355
+ * Start processing the queue
3356
+ */
3357
+ start(intervalMs = 1e3) {
3358
+ if (this.drainTimer) return
3359
+ this.drainTimer = setInterval(() => this.processNext(), intervalMs)
3360
+ this.emit('started')
3361
+ }
3362
+ /**
3363
+ * Stop processing
3364
+ */
3365
+ stop() {
3366
+ if (this.drainTimer) {
3367
+ clearInterval(this.drainTimer)
3368
+ this.drainTimer = null
3369
+ }
3370
+ this.emit('stopped')
3371
+ }
3372
+ /**
3373
+ * Clean up all timers and resources.
3374
+ * Call this when disposing of the queue.
3375
+ */
3376
+ destroy() {
3377
+ this.stop()
3378
+ }
3379
+ /**
3380
+ * Process the next message in the queue
3381
+ */
3382
+ async processNext() {
3383
+ if (this.processing || !this.sendFn) return
3384
+ const now = Date.now()
3385
+ const message = this.queue.find(m => !m.scheduledFor || m.scheduledFor <= now)
3386
+ if (!message) return
3387
+ this.processing = true
3388
+ try {
3389
+ message.attempts++
3390
+ await this.sendFn(message.recipient, message.content)
3391
+ this.queue = this.queue.filter(m => m.id !== message.id)
3392
+ this.emit('sent', message)
3393
+ } catch (err) {
3394
+ message.lastError = err.message
3395
+ if (err.message?.includes('baileys-antiban')) {
3396
+ message.attempts--
3397
+ this.emit('delayed', message, err.message)
3398
+ } else if (message.attempts >= message.maxAttempts) {
3399
+ this.queue = this.queue.filter(m => m.id !== message.id)
3400
+ this.emit('failed', message, err.message)
3401
+ } else {
3402
+ const backoff = this.config.retryBaseDelayMs * Math.pow(2, message.attempts - 1)
3403
+ message.scheduledFor = Date.now() + backoff
3404
+ this.emit('retry', message, message.attempts, backoff)
3405
+ }
3406
+ } finally {
3407
+ this.processing = false
3408
+ }
3409
+ }
3410
+ /**
3411
+ * Get queue stats
3412
+ */
3413
+ getStats() {
3414
+ const now = Date.now()
3415
+ return {
3416
+ total: this.queue.length,
3417
+ pending: this.queue.filter(m => !m.scheduledFor || m.scheduledFor <= now).length,
3418
+ scheduled: this.queue.filter(m => m.scheduledFor && m.scheduledFor > now).length,
3419
+ byPriority: {
3420
+ high: this.queue.filter(m => m.priority === 'high').length,
3421
+ normal: this.queue.filter(m => m.priority === 'normal').length,
3422
+ low: this.queue.filter(m => m.priority === 'low').length
3423
+ },
3424
+ processing: this.processing,
3425
+ isRunning: this.drainTimer !== null
3426
+ }
3427
+ }
3428
+ /**
3429
+ * Clear all messages
3430
+ */
3431
+ clear() {
3432
+ const count = this.queue.length
3433
+ this.queue = []
3434
+ this.emit('cleared', count)
3435
+ }
3436
+ /**
3437
+ * Remove a specific message
3438
+ */
3439
+ remove(id) {
3440
+ const before = this.queue.length
3441
+ this.queue = this.queue.filter(m => m.id !== id)
3442
+ return this.queue.length < before
3443
+ }
3444
+ /**
3445
+ * Export queue for persistence
3446
+ */
3447
+ export() {
3448
+ return [...this.queue]
3449
+ }
3450
+ /**
3451
+ * Import queue (e.g., after restart)
3452
+ */
3453
+ import(messages) {
3454
+ this.queue = [...messages]
3455
+ this.sortQueue()
3456
+ }
3457
+ sortQueue() {
3458
+ if (!this.config.priorityOrder) return
3459
+ const priorityWeight = { high: 0, normal: 1, low: 2 }
3460
+ this.queue.sort((a, b) => {
3461
+ const pDiff = priorityWeight[a.priority] - priorityWeight[b.priority]
3462
+ if (pDiff !== 0) return pDiff
3463
+ return a.addedAt - b.addedAt
3464
+ })
3465
+ }
3466
+ }
3467
+
3468
+ // contentVariator.js
3469
+ var DEFAULT_CONFIG13 = {
3470
+ zeroWidthChars: true,
3471
+ punctuationVariation: true,
3472
+ emojiPadding: false,
3473
+ synonyms: false
3474
+ }
3475
+ var ZERO_WIDTH = [
3476
+ '\u200B',
3477
+ // zero-width space
3478
+ '\u200C',
3479
+ // zero-width non-joiner
3480
+ '\u200D',
3481
+ // zero-width joiner
3482
+ '\uFEFF'
3483
+ // zero-width no-break space
3484
+ ]
3485
+ var SYNONYMS = {
3486
+ hello: ['hi', 'hey', 'howdy'],
3487
+ hi: ['hello', 'hey', 'howdy'],
3488
+ thanks: ['thank you', 'thx', 'cheers'],
3489
+ please: ['kindly', 'pls'],
3490
+ great: ['awesome', 'excellent', 'wonderful'],
3491
+ good: ['great', 'nice', 'fine'],
3492
+ buy: ['purchase', 'get', 'grab'],
3493
+ sell: ['offer', 'list'],
3494
+ price: ['cost', 'amount', 'value'],
3495
+ available: ['in stock', 'on offer'],
3496
+ check: ['look at', 'see', 'view'],
3497
+ join: ['participate', 'enter', 'come to'],
3498
+ start: ['begin', 'kick off', 'commence'],
3499
+ end: ['finish', 'close', 'conclude'],
3500
+ bid: ['offer', 'place a bid'],
3501
+ win: ['secure', 'take home'],
3502
+ item: ['lot', 'piece', 'product']
3503
+ }
3504
+ var ContentVariator = class {
3505
+ config
3506
+ counter = 0
3507
+ constructor(config = {}) {
3508
+ this.config = { ...DEFAULT_CONFIG13, ...config }
3509
+ }
3510
+ /**
3511
+ * Create a unique variation of a message
3512
+ * Each call produces a slightly different version
3513
+ */
3514
+ vary(text) {
3515
+ let result = text
3516
+ this.counter++
3517
+ if (this.config.customVariator) {
3518
+ return this.config.customVariator(result, this.counter)
3519
+ }
3520
+ if (this.config.synonyms) {
3521
+ result = this.applySynonyms(result)
3522
+ }
3523
+ if (this.config.zeroWidthChars) {
3524
+ result = this.addZeroWidth(result)
3525
+ }
3526
+ if (this.config.punctuationVariation) {
3527
+ result = this.varyPunctuation(result)
3528
+ }
3529
+ if (this.config.emojiPadding) {
3530
+ result = this.addEmojiPadding(result)
3531
+ }
3532
+ return result
3533
+ }
3534
+ /**
3535
+ * Create N unique variations of a message
3536
+ */
3537
+ varyBulk(text, count) {
3538
+ const results = []
3539
+ const seen = /* @__PURE__ */ new Set()
3540
+ for (let i = 0; i < count; i++) {
3541
+ let variation = this.vary(text)
3542
+ let attempts = 0
3543
+ while (seen.has(variation) && attempts < 10) {
3544
+ variation = this.vary(text)
3545
+ attempts++
3546
+ }
3547
+ seen.add(variation)
3548
+ results.push(variation)
3549
+ }
3550
+ return results
3551
+ }
3552
+ addZeroWidth(text) {
3553
+ const words = text.split(' ')
3554
+ if (words.length < 2) return text
3555
+ const positions = this.randomPositions(words.length - 1, Math.min(2, words.length - 1))
3556
+ return words
3557
+ .map((word, i) => {
3558
+ if (positions.includes(i)) {
3559
+ const zwc = ZERO_WIDTH[Math.floor(Math.random() * ZERO_WIDTH.length)]
3560
+ return word + zwc
3561
+ }
3562
+ return word
3563
+ })
3564
+ .join(' ')
3565
+ }
3566
+ varyPunctuation(text) {
3567
+ const variations = [
3568
+ // Trailing space variations
3569
+ () => text + ' ',
3570
+ () => text + ' ',
3571
+ // Period variations
3572
+ () => (text.endsWith('.') ? text.slice(0, -1) : text + '.'),
3573
+ // Nothing
3574
+ () => text,
3575
+ // Capitalize first letter variation
3576
+ () => (text.charAt(0) === text.charAt(0).toUpperCase() ? text.charAt(0).toLowerCase() + text.slice(1) : text)
3577
+ ]
3578
+ return variations[this.counter % variations.length]()
3579
+ }
3580
+ addEmojiPadding(text) {
3581
+ const emojis = ['', ' \u{1F44D}', ' \u2705', ' \u{1F4CC}', ' \u{1F4AC}', ' \u{1F4E2}']
3582
+ return text + emojis[this.counter % emojis.length]
3583
+ }
3584
+ applySynonyms(text) {
3585
+ const words = text.split(/\b/)
3586
+ let replaced = false
3587
+ return words
3588
+ .map(word => {
3589
+ if (replaced) return word
3590
+ const lower = word.toLowerCase()
3591
+ const synonymList = SYNONYMS[lower]
3592
+ if (synonymList && Math.random() > 0.5) {
3593
+ replaced = true
3594
+ const synonym = synonymList[Math.floor(Math.random() * synonymList.length)]
3595
+ return word[0] === word[0].toUpperCase() ? synonym.charAt(0).toUpperCase() + synonym.slice(1) : synonym
3596
+ }
3597
+ return word
3598
+ })
3599
+ .join('')
3600
+ }
3601
+ randomPositions(max, count) {
3602
+ const positions = []
3603
+ while (positions.length < count) {
3604
+ const pos = Math.floor(Math.random() * max)
3605
+ if (!positions.includes(pos)) positions.push(pos)
3606
+ }
3607
+ return positions
3608
+ }
3609
+ }
3610
+
3611
+ // webhooks.js
3612
+ var DEFAULT_CONFIG14 = {
3613
+ urls: [],
3614
+ minRiskLevel: 'medium',
3615
+ cooldownMs: 3e5,
3616
+ includeStats: true
3617
+ }
3618
+ var WebhookAlerts = class {
3619
+ config
3620
+ lastAlertTime = 0
3621
+ constructor(config = {}) {
3622
+ this.config = { ...DEFAULT_CONFIG14, ...config }
3623
+ }
3624
+ /**
3625
+ * Send alert if risk level warrants it
3626
+ */
3627
+ async alert(data) {
3628
+ const riskOrder = ['low', 'medium', 'high', 'critical']
3629
+ if (riskOrder.indexOf(data.risk) < riskOrder.indexOf(this.config.minRiskLevel)) {
3630
+ return
3631
+ }
3632
+ const now = Date.now()
3633
+ if (now - this.lastAlertTime < this.config.cooldownMs) {
3634
+ return
3635
+ }
3636
+ this.lastAlertTime = now
3637
+ const payload = {
3638
+ source: 'baileys-antiban',
3639
+ timestamp: /* @__PURE__ */ new Date().toISOString(),
3640
+ ...data
3641
+ }
3642
+ for (const url of this.config.urls) {
3643
+ this.postWebhook(url, payload).catch(() => {})
3644
+ }
3645
+ if (this.config.telegram) {
3646
+ const emoji =
3647
+ { low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' }[data.risk] || '\u26AA'
3648
+ const text = `${emoji} *baileys-antiban Alert*
3649
+
3650
+ Risk: *${data.risk.toUpperCase()}* (score: ${data.score})
3651
+ ${data.recommendation}
3652
+
3653
+ Reasons:
3654
+ ${data.reasons.map(r => `\u2022 ${r}`).join('\n')}`
3655
+ this.postWebhook(`https://api.telegram.org/bot${this.config.telegram.botToken}/sendMessage`, {
3656
+ chat_id: this.config.telegram.chatId,
3657
+ text,
3658
+ parse_mode: 'Markdown'
3659
+ }).catch(() => {})
3660
+ }
3661
+ if (this.config.discord) {
3662
+ const color = { low: 65280, medium: 16776960, high: 16746496, critical: 16711680 }[data.risk] || 0
3663
+ this.postWebhook(this.config.discord.webhookUrl, {
3664
+ embeds: [
3665
+ {
3666
+ title: '\u{1F6E1}\uFE0F baileys-antiban Alert',
3667
+ color,
3668
+ fields: [
3669
+ { name: 'Risk', value: data.risk.toUpperCase(), inline: true },
3670
+ { name: 'Score', value: String(data.score), inline: true },
3671
+ { name: 'Recommendation', value: data.recommendation },
3672
+ { name: 'Reasons', value: data.reasons.join('\n') }
3673
+ ],
3674
+ timestamp: /* @__PURE__ */ new Date().toISOString()
3675
+ }
3676
+ ]
3677
+ }).catch(() => {})
3678
+ }
3679
+ }
3680
+ async postWebhook(url, payload) {
3681
+ try {
3682
+ const response = await fetch(url, {
3683
+ method: 'POST',
3684
+ headers: {
3685
+ 'Content-Type': 'application/json',
3686
+ ...this.config.headers
3687
+ },
3688
+ body: JSON.stringify(payload)
3689
+ })
3690
+ if (!response.ok) {
3691
+ process.stderr.write(`[baileys-antiban] Webhook failed: ${response.status}\n`)
3692
+ }
3693
+ } catch (err) {
3694
+ process.stderr.write(`[baileys-antiban] Webhook error: ${err}\n`)
3695
+ }
3696
+ }
3697
+ }
3698
+
3699
+ // scheduler.js
3700
+ var DEFAULT_CONFIG15 = {
3701
+ timezone: 'UTC',
3702
+ activeHours: [8, 21],
3703
+ weekendFactor: 0.5,
3704
+ peakHours: [10, 14],
3705
+ peakFactor: 1.3,
3706
+ lunchBreak: [12, 13],
3707
+ lunchFactor: 0.5
3708
+ }
3709
+ var Scheduler = class {
3710
+ config
3711
+ constructor(config = {}) {
3712
+ this.config = { ...DEFAULT_CONFIG15, ...config }
3713
+ }
3714
+ /**
3715
+ * Check if now is within active hours
3716
+ */
3717
+ isActiveTime() {
3718
+ const hour = this.getCurrentHour()
3719
+ const [start, end] = this.config.activeHours
3720
+ return hour >= start && hour < end
3721
+ }
3722
+ /**
3723
+ * Get the speed multiplier for current time
3724
+ * > 1 = faster, < 1 = slower, 0 = don't send
3725
+ */
3726
+ getSpeedFactor() {
3727
+ if (!this.isActiveTime()) return 0
3728
+ const hour = this.getCurrentHour()
3729
+ const day = this.getCurrentDay()
3730
+ let factor = 1
3731
+ if (day === 0 || day === 6) {
3732
+ factor *= this.config.weekendFactor
3733
+ }
3734
+ const [peakStart, peakEnd] = this.config.peakHours
3735
+ if (hour >= peakStart && hour < peakEnd) {
3736
+ factor *= this.config.peakFactor
3737
+ }
3738
+ const [lunchStart, lunchEnd] = this.config.lunchBreak
3739
+ if (hour >= lunchStart && hour < lunchEnd) {
3740
+ factor *= this.config.lunchFactor
3741
+ }
3742
+ return factor
3743
+ }
3744
+ /**
3745
+ * Get ms until next active window
3746
+ */
3747
+ msUntilActive() {
3748
+ if (this.isActiveTime()) return 0
3749
+ const now = /* @__PURE__ */ new Date()
3750
+ const hour = now.getHours()
3751
+ const [start] = this.config.activeHours
3752
+ let nextActive
3753
+ if (hour >= this.config.activeHours[1]) {
3754
+ nextActive = new Date(now)
3755
+ nextActive.setDate(nextActive.getDate() + 1)
3756
+ nextActive.setHours(start, 0, 0, 0)
3757
+ } else {
3758
+ nextActive = new Date(now)
3759
+ nextActive.setHours(start, 0, 0, 0)
3760
+ }
3761
+ return nextActive.getTime() - now.getTime()
3762
+ }
3763
+ /**
3764
+ * Adjust a delay based on current time factors
3765
+ */
3766
+ adjustDelay(baseDelayMs) {
3767
+ const factor = this.getSpeedFactor()
3768
+ if (factor === 0) return -1
3769
+ return Math.round(baseDelayMs / factor)
3770
+ }
3771
+ /**
3772
+ * Get current schedule status
3773
+ */
3774
+ getStatus() {
3775
+ const hour = this.getCurrentHour()
3776
+ const day = this.getCurrentDay()
3777
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
3778
+ return {
3779
+ active: this.isActiveTime(),
3780
+ currentHour: hour,
3781
+ day: dayNames[day],
3782
+ isWeekend: day === 0 || day === 6,
3783
+ speedFactor: this.getSpeedFactor(),
3784
+ msUntilActive: this.msUntilActive(),
3785
+ activeWindow: `${this.config.activeHours[0]}:00 - ${this.config.activeHours[1]}:00`
3786
+ }
3787
+ }
3788
+ getCurrentHour() {
3789
+ return /* @__PURE__ */ new Date().getHours()
3790
+ }
3791
+ getCurrentDay() {
3792
+ return /* @__PURE__ */ new Date().getDay()
3793
+ }
3794
+ }
3795
+
3796
+ // stateAdapter.js
3797
+ var _fsPromises = require('fs/promises')
3798
+ var _fsSync = require('fs')
3799
+ var _path3 = require('path')
3800
+ var FileStateAdapter = class {
3801
+ basePath
3802
+ constructor(basePath) {
3803
+ this.basePath = basePath
3804
+ }
3805
+ async save(key, state) {
3806
+ const filePath = _path3.join(this.basePath, `${key}.json`)
3807
+ await _fsPromises.mkdir(this.basePath, { recursive: true })
3808
+ await _fsPromises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8')
3809
+ }
3810
+ async load(key) {
3811
+ const filePath = _path3.join(this.basePath, `${key}.json`)
3812
+ try {
3813
+ const data = await _fsPromises.readFile(filePath, 'utf-8')
3814
+ return JSON.parse(data)
3815
+ } catch (err) {
3816
+ if (err.code === 'ENOENT') return null
3817
+ throw err
3818
+ }
3819
+ }
3820
+ async delete(key) {
3821
+ const filePath = _path3.join(this.basePath, `${key}.json`)
3822
+ try {
3823
+ await _fsPromises.unlink(filePath)
3824
+ } catch (err) {
3825
+ if (err.code !== 'ENOENT') throw err
3826
+ }
3827
+ }
3828
+ async list() {
3829
+ try {
3830
+ const files = await _fsPromises.readdir(this.basePath)
3831
+ return files.filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''))
3832
+ } catch (err) {
3833
+ if (err.code === 'ENOENT') return []
3834
+ throw err
3835
+ }
3836
+ }
3837
+ }
3838
+
3839
+ // messageRecovery.js
3840
+ var DEFAULT_CONFIG16 = {
3841
+ maxTrackedChats: 1e3,
3842
+ maxGapMs: 30 * 6e4,
3843
+ // 30 minutes
3844
+ persistDebounceMs: 2e3,
3845
+ onGapFilled: () => {},
3846
+ logger: {
3847
+ info: () => {},
3848
+ warn: () => {},
3849
+ error: () => {}
3850
+ }
3851
+ }
3852
+ function messageRecovery(sock, config) {
3853
+ const cfg = { ...DEFAULT_CONFIG16, ...config }
3854
+ const logger = cfg.logger
3855
+ const lastSeen = /* @__PURE__ */ new Map()
3856
+ let disconnectedAt = null
3857
+ let totalRecovered = 0
3858
+ let lastReconnectAt = null
3859
+ let lastGapMs = null
3860
+ let persistTimer = null
3861
+ let loggedFetchWarning = false
3862
+ if (cfg.persistPath) {
3863
+ loadPersistence()
3864
+ }
3865
+ const messagesListener = sock.ev.process ? setupProcessListener() : setupLegacyListener()
3866
+ const connectionListener = update => {
3867
+ if (update.connection === 'close') {
3868
+ disconnectedAt = Date.now()
3869
+ logger.info?.(`[messageRecovery] Disconnected at ${new Date(disconnectedAt).toISOString()}`)
3870
+ }
3871
+ if (update.connection === 'open' && disconnectedAt !== null) {
3872
+ void recoverMessages()
3873
+ }
3874
+ }
3875
+ sock.ev.on('connection.update', connectionListener)
3876
+ function setupProcessListener() {
3877
+ const listener = async events => {
3878
+ if (events['messages.upsert']) {
3879
+ const { messages, type } = events['messages.upsert']
3880
+ if (type === 'notify') {
3881
+ for (const msg of messages || []) {
3882
+ trackMessage(msg)
3883
+ }
3884
+ }
3885
+ }
3886
+ }
3887
+ sock.ev.process(listener)
3888
+ return listener
3889
+ }
3890
+ function setupLegacyListener() {
3891
+ const listener = upsert => {
3892
+ const { messages, type } = upsert
3893
+ if (type === 'notify') {
3894
+ for (const msg of messages || []) {
3895
+ trackMessage(msg)
3896
+ }
3897
+ }
3898
+ }
3899
+ sock.ev.on('messages.upsert', listener)
3900
+ return listener
3901
+ }
3902
+ function trackMessage(msg) {
3903
+ const jid = msg.key?.remoteJid
3904
+ const messageId = msg.key?.id
3905
+ const timestamp = msg.messageTimestamp
3906
+ if (!jid || !messageId || !timestamp) return
3907
+ if (msg.key?.fromMe) return
3908
+ const now = Date.now()
3909
+ lastSeen.set(jid, {
3910
+ messageId,
3911
+ timestamp: typeof timestamp === 'number' ? timestamp : parseInt(timestamp, 10),
3912
+ lastTouchedAt: now
3913
+ })
3914
+ if (lastSeen.size > cfg.maxTrackedChats) {
3915
+ evictOldest()
3916
+ }
3917
+ schedulePersist()
3918
+ }
3919
+ function evictOldest() {
3920
+ let oldestJid = null
3921
+ let oldestTime = Infinity
3922
+ for (const [jid, entry] of lastSeen) {
3923
+ if (entry.lastTouchedAt < oldestTime) {
3924
+ oldestTime = entry.lastTouchedAt
3925
+ oldestJid = jid
3926
+ }
3927
+ }
3928
+ if (oldestJid) {
3929
+ lastSeen.delete(oldestJid)
3930
+ }
3931
+ }
3932
+ async function recoverMessages() {
3933
+ const recoveryStartMs = Date.now()
3934
+ const gapMs = recoveryStartMs - disconnectedAt
3935
+ logger.info?.(`[messageRecovery] Reconnected after ${(gapMs / 1e3).toFixed(1)}s`)
3936
+ if (gapMs > cfg.maxGapMs) {
3937
+ logger.warn?.(
3938
+ `[messageRecovery] Gap too large (${(gapMs / 1e3).toFixed(0)}s > ${(cfg.maxGapMs / 1e3).toFixed(0)}s) \u2014 skipping recovery`
3939
+ )
3940
+ disconnectedAt = null
3941
+ lastGapMs = gapMs
3942
+ await cfg.onGapTooLarge?.(gapMs)
3943
+ return
3944
+ }
3945
+ let recovered = 0
3946
+ const chatsToRecover = Array.from(lastSeen.entries())
3947
+ if (typeof sock.fetchMessageHistory !== 'function') {
3948
+ if (!loggedFetchWarning) {
3949
+ logger.warn?.(
3950
+ `[messageRecovery] sock.fetchMessageHistory not available \u2014 recovery disabled. Baileys version may not support history fetch. User must implement manual reconciliation.`
3951
+ )
3952
+ loggedFetchWarning = true
3953
+ }
3954
+ disconnectedAt = null
3955
+ lastReconnectAt = /* @__PURE__ */ new Date()
3956
+ lastGapMs = gapMs
3957
+ await cfg.onRecoveryComplete?.({
3958
+ chats: 0,
3959
+ recovered: 0,
3960
+ durationMs: Date.now() - recoveryStartMs
3961
+ })
3962
+ return
3963
+ }
3964
+ for (const [jid, lastSeenEntry] of chatsToRecover) {
3965
+ try {
3966
+ const messages = await sock.fetchMessageHistory(jid, 50, {
3967
+ before: void 0
3968
+ // Get latest
3969
+ })
3970
+ if (!messages || !Array.isArray(messages)) continue
3971
+ const gapMessages = messages.filter(msg => {
3972
+ const ts = msg.messageTimestamp
3973
+ if (!ts) return false
3974
+ const msgTs = typeof ts === 'number' ? ts : parseInt(ts, 10)
3975
+ return msgTs > lastSeenEntry.timestamp
3976
+ })
3977
+ gapMessages.sort((a, b) => {
3978
+ const aTs = typeof a.messageTimestamp === 'number' ? a.messageTimestamp : parseInt(a.messageTimestamp, 10)
3979
+ const bTs = typeof b.messageTimestamp === 'number' ? b.messageTimestamp : parseInt(b.messageTimestamp, 10)
3980
+ return aTs - bTs
3981
+ })
3982
+ for (const msg of gapMessages) {
3983
+ await cfg.onGapFilled(msg, jid)
3984
+ recovered++
3985
+ const msgTs =
3986
+ typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : parseInt(msg.messageTimestamp, 10)
3987
+ if (msgTs > lastSeenEntry.timestamp) {
3988
+ lastSeenEntry.timestamp = msgTs
3989
+ lastSeenEntry.messageId = msg.key?.id || lastSeenEntry.messageId
3990
+ lastSeenEntry.lastTouchedAt = Date.now()
3991
+ }
3992
+ }
3993
+ if (gapMessages.length > 0) {
3994
+ logger.info?.(`[messageRecovery] Recovered ${gapMessages.length} messages from ${jid}`)
3995
+ }
3996
+ } catch (err) {
3997
+ logger.error?.(`[messageRecovery] Failed to recover from ${jid}: ${err.message}`)
3998
+ }
3999
+ }
4000
+ totalRecovered += recovered
4001
+ lastReconnectAt = /* @__PURE__ */ new Date()
4002
+ lastGapMs = gapMs
4003
+ disconnectedAt = null
4004
+ logger.info?.(
4005
+ `[messageRecovery] Recovery complete: ${recovered} messages across ${chatsToRecover.length} chats in ${Date.now() - recoveryStartMs}ms`
4006
+ )
4007
+ await cfg.onRecoveryComplete?.({
4008
+ chats: chatsToRecover.length,
4009
+ recovered,
4010
+ durationMs: Date.now() - recoveryStartMs
4011
+ })
4012
+ }
4013
+ function schedulePersist() {
4014
+ if (!cfg.persistPath) return
4015
+ if (persistTimer) {
4016
+ clearTimeout(persistTimer)
4017
+ }
4018
+ persistTimer = setTimeout(() => {
4019
+ void flushPersistence()
4020
+ }, cfg.persistDebounceMs)
4021
+ }
4022
+ async function flushPersistence() {
4023
+ if (!cfg.persistPath) return
4024
+ try {
4025
+ const data = {}
4026
+ for (const [jid, entry] of lastSeen) {
4027
+ data[jid] = {
4028
+ id: entry.messageId,
4029
+ timestamp: entry.timestamp
4030
+ }
4031
+ }
4032
+ await _fsPromises.writeFile(cfg.persistPath, JSON.stringify(data, null, 2), 'utf-8')
4033
+ } catch (err) {
4034
+ logger.error?.(`[messageRecovery] Failed to persist state: ${err.message}`)
4035
+ }
4036
+ }
4037
+ function loadPersistence() {
4038
+ if (!cfg.persistPath) return
4039
+ try {
4040
+ const fs4 = require('fs')
4041
+ if (!fs4.existsSync(cfg.persistPath)) return
4042
+ const raw = fs4.readFileSync(cfg.persistPath, 'utf-8')
4043
+ const data = JSON.parse(raw)
4044
+ for (const [jid, entry] of Object.entries(data)) {
4045
+ lastSeen.set(jid, {
4046
+ messageId: entry.id,
4047
+ timestamp: entry.timestamp,
4048
+ lastTouchedAt: Date.now()
4049
+ })
4050
+ }
4051
+ logger.info?.(`[messageRecovery] Loaded ${lastSeen.size} entries from ${cfg.persistPath}`)
4052
+ } catch (err) {
4053
+ logger.warn?.(`[messageRecovery] Failed to load persisted state: ${err.message}`)
4054
+ }
4055
+ }
4056
+ return {
4057
+ async stop() {
4058
+ sock.ev.off('connection.update', connectionListener)
4059
+ if (!sock.ev.process) {
4060
+ sock.ev.off('messages.upsert', messagesListener)
4061
+ }
4062
+ if (persistTimer) {
4063
+ clearTimeout(persistTimer)
4064
+ persistTimer = null
4065
+ }
4066
+ await flushPersistence()
4067
+ logger.info?.(`[messageRecovery] Stopped \u2014 total recovered: ${totalRecovered}`)
4068
+ },
4069
+ markSeen(chatJid, messageId, timestamp) {
4070
+ lastSeen.set(chatJid, {
4071
+ messageId,
4072
+ timestamp,
4073
+ lastTouchedAt: Date.now()
4074
+ })
4075
+ schedulePersist()
4076
+ },
4077
+ getStats() {
4078
+ return {
4079
+ trackedChats: lastSeen.size,
4080
+ totalRecovered,
4081
+ lastReconnectAt,
4082
+ lastGapMs
4083
+ }
4084
+ }
4085
+ }
4086
+ }
4087
+
4088
+ // deviceFingerprint.js
4089
+ var DEFAULT_APP_VERSION_POOL = [
4090
+ [2, 24, 5, 18],
4091
+ [2, 24, 5, 17],
4092
+ [2, 24, 4, 77],
4093
+ [2, 24, 5, 15],
4094
+ [2, 24, 3, 91],
4095
+ [2, 24, 5, 20]
4096
+ ]
4097
+ var DEFAULT_OS_VERSION_POOL = ['10', '11', '12', '13', '14']
4098
+ var DEFAULT_DEVICE_MODEL_POOL = [
4099
+ 'Pixel 6',
4100
+ 'Pixel 7',
4101
+ 'Galaxy S22',
4102
+ 'Galaxy S23',
4103
+ 'Xiaomi 13',
4104
+ 'Xiaomi 12',
4105
+ 'OnePlus 11',
4106
+ 'Moto G84',
4107
+ 'Moto G54',
4108
+ 'Realme 11',
4109
+ 'Vivo V29',
4110
+ 'Oppo Find X6'
4111
+ ]
4112
+ var SeededRandom = class {
4113
+ state
4114
+ constructor(seed) {
4115
+ let hash = 0
4116
+ for (let i = 0; i < seed.length; i++) {
4117
+ hash = (hash << 5) - hash + seed.charCodeAt(i)
4118
+ hash = hash & hash
4119
+ }
4120
+ this.state = Math.abs(hash) || 1
4121
+ }
4122
+ next() {
4123
+ let t = (this.state += 1831565813)
4124
+ t = Math.imul(t ^ (t >>> 15), t | 1)
4125
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
4126
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
4127
+ }
4128
+ pick(array) {
4129
+ return array[Math.floor(this.next() * array.length)]
4130
+ }
4131
+ }
4132
+ function generateFingerprint(config = {}, sessionId) {
4133
+ const {
4134
+ enabled = true,
4135
+ randomizeAppVersion = true,
4136
+ randomizeOsVersion = true,
4137
+ randomizeDeviceModel = true,
4138
+ seed,
4139
+ appVersionPool = DEFAULT_APP_VERSION_POOL,
4140
+ osVersionPool = DEFAULT_OS_VERSION_POOL,
4141
+ deviceModelPool = DEFAULT_DEVICE_MODEL_POOL
4142
+ } = config
4143
+ const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`
4144
+ const rng = new SeededRandom(seed || finalSessionId)
4145
+ const appVersion = enabled && randomizeAppVersion ? rng.pick(appVersionPool) : appVersionPool[0]
4146
+ const osVersion = enabled && randomizeOsVersion ? rng.pick(osVersionPool) : osVersionPool[0]
4147
+ const deviceModel = enabled && randomizeDeviceModel ? rng.pick(deviceModelPool) : deviceModelPool[0]
4148
+ return {
4149
+ appVersion: [...appVersion],
4150
+ // Copy to avoid mutation
4151
+ osVersion,
4152
+ deviceModel,
4153
+ sessionId: finalSessionId
4154
+ }
4155
+ }
4156
+ function applyFingerprint(socketConfig, fp) {
4157
+ const config = { ...socketConfig }
4158
+ if (config.version !== void 0 || 'version' in config || true) {
4159
+ config.version = fp.appVersion
4160
+ }
4161
+ if (config.browser !== void 0 || 'browser' in config || true) {
4162
+ config.browser = [fp.deviceModel, fp.osVersion, `WhatsApp/${fp.appVersion.join('.')}`]
4163
+ }
4164
+ return config
4165
+ }
4166
+
4167
+ // credsSnapshot.js
4168
+ var import_fs = require('fs')
4169
+ var path2 = __toESM(require('path'), 1)
4170
+ var noop = () => {}
4171
+ function credsSnapshot(config) {
4172
+ const { credsPath, snapshotDir = path2.join(path2.dirname(credsPath), '.snapshots'), keep = 3, logger = {} } = config
4173
+ const log = {
4174
+ info: logger.info || noop,
4175
+ warn: logger.warn || noop,
4176
+ error: logger.error || noop
4177
+ }
4178
+ async function take() {
4179
+ try {
4180
+ try {
4181
+ await import_fs.promises.access(credsPath)
4182
+ } catch {
4183
+ log.warn(`[credsSnapshot] Creds file not found: ${credsPath}`)
4184
+ return null
4185
+ }
4186
+ await import_fs.promises.mkdir(snapshotDir, { recursive: true })
4187
+ const timestamp = /* @__PURE__ */ new Date().toISOString().replace(/[:.]/g, '-')
4188
+ const snapshotPath = path2.join(snapshotDir, `creds-${timestamp}.json`)
4189
+ const tmpPath = `${snapshotPath}.tmp`
4190
+ await import_fs.promises.copyFile(credsPath, tmpPath)
4191
+ await import_fs.promises.rename(tmpPath, snapshotPath)
4192
+ log.info(`[credsSnapshot] Snapshot taken: ${snapshotPath}`)
4193
+ await rotate()
4194
+ return snapshotPath
4195
+ } catch (err) {
4196
+ log.error(`[credsSnapshot] Failed to take snapshot: ${err}`)
4197
+ return null
4198
+ }
4199
+ }
4200
+ async function rotate() {
4201
+ try {
4202
+ const snapshots = await list()
4203
+ const toDelete = snapshots.slice(keep)
4204
+ for (const snap of toDelete) {
4205
+ await import_fs.promises.unlink(snap.path)
4206
+ log.info(`[credsSnapshot] Rotated out: ${snap.path}`)
4207
+ }
4208
+ } catch (err) {
4209
+ log.error(`[credsSnapshot] Rotation failed: ${err}`)
4210
+ }
4211
+ }
4212
+ async function list() {
4213
+ try {
4214
+ await import_fs.promises.access(snapshotDir)
4215
+ } catch {
4216
+ return []
4217
+ }
4218
+ try {
4219
+ const files = await import_fs.promises.readdir(snapshotDir)
4220
+ const snapshots = await Promise.all(
4221
+ files
4222
+ .filter(f => f.startsWith('creds-') && f.endsWith('.json'))
4223
+ .map(async f => {
4224
+ const fullPath = path2.join(snapshotDir, f)
4225
+ const stat = await import_fs.promises.stat(fullPath)
4226
+ return {
4227
+ path: fullPath,
4228
+ takenAt: stat.mtime,
4229
+ size: stat.size
4230
+ }
4231
+ })
4232
+ )
4233
+ return snapshots.sort((a, b) => b.takenAt.getTime() - a.takenAt.getTime())
4234
+ } catch (err) {
4235
+ log.error(`[credsSnapshot] Failed to list snapshots: ${err}`)
4236
+ return []
4237
+ }
4238
+ }
4239
+ async function restoreLatest() {
4240
+ const snapshots = await list()
4241
+ if (snapshots.length === 0) {
4242
+ log.warn('[credsSnapshot] No snapshots available to restore')
4243
+ return false
4244
+ }
4245
+ return restore(snapshots[0].path)
4246
+ }
4247
+ async function restore(snapshotPath) {
4248
+ try {
4249
+ await import_fs.promises.access(snapshotPath)
4250
+ const tmpPath = `${credsPath}.tmp`
4251
+ await import_fs.promises.copyFile(snapshotPath, tmpPath)
4252
+ await import_fs.promises.rename(tmpPath, credsPath)
4253
+ log.info(`[credsSnapshot] Restored from: ${snapshotPath}`)
4254
+ return true
4255
+ } catch (err) {
4256
+ log.error(`[credsSnapshot] Failed to restore from ${snapshotPath}: ${err}`)
4257
+ return false
4258
+ }
4259
+ }
4260
+ return {
4261
+ take,
4262
+ restoreLatest,
4263
+ restore,
4264
+ list
4265
+ }
4266
+ }
4267
+
4268
+ // readReceiptVariance.js
4269
+ function gaussianRandom() {
4270
+ let u = 0
4271
+ let v = 0
4272
+ while (u === 0) u = Math.random()
4273
+ while (v === 0) v = Math.random()
4274
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
4275
+ }
4276
+ function readReceiptVariance(config = {}) {
4277
+ const { meanMs = 1500, stdDevMs = 800, minMs = 200, maxMs = 8e3, skipIfOlderThanMs = 6e4 } = config
4278
+ const pendingTimers = /* @__PURE__ */ new Set()
4279
+ function delayMs() {
4280
+ const gaussian = gaussianRandom()
4281
+ const value = meanMs + gaussian * stdDevMs
4282
+ return Math.max(minMs, Math.min(maxMs, value))
4283
+ }
4284
+ function wrap(sock) {
4285
+ const originalReadMessages = sock.readMessages.bind(sock)
4286
+ const wrappedReadMessages = async keys => {
4287
+ const now = Date.now()
4288
+ const oldMessages = keys.every(key => {
4289
+ if (!key.messageTimestamp) return false
4290
+ const msgTime =
4291
+ typeof key.messageTimestamp === 'number'
4292
+ ? key.messageTimestamp * 1e3
4293
+ : parseInt(key.messageTimestamp, 10) * 1e3
4294
+ return now - msgTime > skipIfOlderThanMs
4295
+ })
4296
+ if (oldMessages) {
4297
+ return originalReadMessages(keys)
4298
+ }
4299
+ const delay = delayMs()
4300
+ return new Promise((resolve, reject) => {
4301
+ const timer = setTimeout(async () => {
4302
+ pendingTimers.delete(timer)
4303
+ try {
4304
+ const result = await originalReadMessages(keys)
4305
+ resolve(result)
4306
+ } catch (err) {
4307
+ reject(err)
4308
+ }
4309
+ }, delay)
4310
+ pendingTimers.add(timer)
4311
+ })
4312
+ }
4313
+ return new Proxy(sock, {
4314
+ get(target, prop) {
4315
+ if (prop === 'readMessages') {
4316
+ return wrappedReadMessages
4317
+ }
4318
+ return target[prop]
4319
+ }
4320
+ })
4321
+ }
4322
+ function stop() {
4323
+ for (const timer of pendingTimers) {
4324
+ clearTimeout(timer)
4325
+ }
4326
+ pendingTimers.clear()
4327
+ }
4328
+ return {
4329
+ wrap,
4330
+ delayMs,
4331
+ stop
4332
+ }
4333
+ }
4334
+
4335
+ // proxyRotator.js
4336
+ var import_node_module = require('node:module')
4337
+ var require2 = (0, import_node_module.createRequire)(__importMetaUrl)
4338
+ var NoopLogger = {
4339
+ info: () => {},
4340
+ warn: () => {},
4341
+ error: () => {}
4342
+ }
4343
+ function proxyRotator(config) {
4344
+ const {
4345
+ pool,
4346
+ strategy = 'round-robin',
4347
+ rotateOn = ['disconnect', 'ban-warning'],
4348
+ scheduledIntervalMs = 0,
4349
+ maxFailures = 3,
4350
+ deadCooldownMs = 6e5,
4351
+ // 10 minutes
4352
+ logger = NoopLogger
4353
+ } = config
4354
+ if (!pool || pool.length === 0) {
4355
+ throw new Error('proxyRotator: pool cannot be empty')
4356
+ }
4357
+ if (pool.length === 1) {
4358
+ logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.')
4359
+ }
4360
+ if (scheduledIntervalMs > 0 && scheduledIntervalMs < 6e4) {
4361
+ logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`)
4362
+ }
4363
+ const states = pool.map(endpoint => ({
4364
+ endpoint,
4365
+ failures: 0,
4366
+ lastUsedAt: null,
4367
+ isDead: false
4368
+ }))
4369
+ let currentIndex = 0
4370
+ let totalRotations = 0
4371
+ const rotationsByTrigger = {}
4372
+ let scheduledTimer = null
4373
+ const agentCache = /* @__PURE__ */ new Map()
4374
+ const moduleCache = {}
4375
+ function buildProxyUrl(endpoint) {
4376
+ const { type, host, port, username, password } = endpoint
4377
+ const auth = username && password ? `${username}:${password}@` : ''
4378
+ return `${type}://${auth}${host}:${port}`
4379
+ }
4380
+ function createAgentForEndpointSync(endpoint) {
4381
+ if (agentCache.has(endpoint)) {
4382
+ return agentCache.get(endpoint)
4383
+ }
4384
+ const url = buildProxyUrl(endpoint)
4385
+ let agent = null
4386
+ try {
4387
+ if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
4388
+ if (!moduleCache['socks-proxy-agent']) {
4389
+ try {
4390
+ moduleCache['socks-proxy-agent'] = require2('socks-proxy-agent')
4391
+ } catch {
4392
+ logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent')
4393
+ return null
4394
+ }
4395
+ }
4396
+ agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url)
4397
+ } else if (endpoint.type === 'http') {
4398
+ if (!moduleCache['http-proxy-agent']) {
4399
+ try {
4400
+ moduleCache['http-proxy-agent'] = require2('http-proxy-agent')
4401
+ } catch {
4402
+ logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent')
4403
+ return null
4404
+ }
4405
+ }
4406
+ agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url)
4407
+ } else if (endpoint.type === 'https') {
4408
+ if (!moduleCache['https-proxy-agent']) {
4409
+ try {
4410
+ moduleCache['https-proxy-agent'] = require2('https-proxy-agent')
4411
+ } catch {
4412
+ logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent')
4413
+ return null
4414
+ }
4415
+ }
4416
+ agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url)
4417
+ } else {
4418
+ logger.error?.(`Unknown proxy type: ${endpoint.type}`)
4419
+ return null
4420
+ }
4421
+ if (agent) {
4422
+ agentCache.set(endpoint, agent)
4423
+ }
4424
+ return agent
4425
+ } catch (err) {
4426
+ logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`)
4427
+ return null
4428
+ }
4429
+ }
4430
+ function getAliveEndpoints() {
4431
+ const now = Date.now()
4432
+ return states
4433
+ .map((s, idx) => {
4434
+ if (s.isDead && s.lastUsedAt) {
4435
+ if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
4436
+ s.isDead = false
4437
+ s.failures = 0
4438
+ logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`)
4439
+ }
4440
+ }
4441
+ const cooldown = s.endpoint.cooldownMs || 0
4442
+ if (cooldown > 0 && s.lastUsedAt) {
4443
+ if (now - s.lastUsedAt.getTime() < cooldown) {
4444
+ return -1
4445
+ }
4446
+ }
4447
+ return !s.isDead ? idx : -1
4448
+ })
4449
+ .filter(idx => idx !== -1)
4450
+ }
4451
+ function selectNextIndex(alive) {
4452
+ if (alive.length === 0) return currentIndex
4453
+ if (strategy === 'round-robin') {
4454
+ const afterCurrent = alive.filter(idx => idx > currentIndex)
4455
+ if (afterCurrent.length > 0) return afterCurrent[0]
4456
+ return alive[0]
4457
+ }
4458
+ if (strategy === 'random') {
4459
+ return alive[Math.floor(Math.random() * alive.length)]
4460
+ }
4461
+ if (strategy === 'least-recently-used') {
4462
+ const neverUsed = alive.filter(idx => states[idx].lastUsedAt === null)
4463
+ if (neverUsed.length > 0) {
4464
+ return neverUsed[0]
4465
+ }
4466
+ let oldestIdx = alive[0]
4467
+ let oldestTime = states[oldestIdx].lastUsedAt.getTime()
4468
+ for (const idx of alive) {
4469
+ const time = states[idx].lastUsedAt.getTime()
4470
+ if (time < oldestTime) {
4471
+ oldestTime = time
4472
+ oldestIdx = idx
4473
+ }
4474
+ }
4475
+ return oldestIdx
4476
+ }
4477
+ if (strategy === 'weighted') {
4478
+ const weights = alive.map(idx => {
4479
+ const failures = states[idx].failures
4480
+ return 1 / (failures + 1)
4481
+ })
4482
+ const totalWeight = weights.reduce((a, b) => a + b, 0)
4483
+ let rand = Math.random() * totalWeight
4484
+ for (let i = 0; i < alive.length; i++) {
4485
+ rand -= weights[i]
4486
+ if (rand <= 0) return alive[i]
4487
+ }
4488
+ return alive[alive.length - 1]
4489
+ }
4490
+ return alive[0]
4491
+ }
4492
+ function rotateImpl(reason = 'manual') {
4493
+ if (pool.length === 1) {
4494
+ return states[0].endpoint
4495
+ }
4496
+ const alive = getAliveEndpoints()
4497
+ if (alive.length === 0) {
4498
+ logger.warn?.('All endpoints are dead. Cannot rotate.')
4499
+ return states[currentIndex].endpoint
4500
+ }
4501
+ const nextIdx = selectNextIndex(alive)
4502
+ if (nextIdx === currentIndex && alive.length > 1) {
4503
+ const others = alive.filter(idx => idx !== currentIndex)
4504
+ if (others.length > 0) {
4505
+ currentIndex = others[0]
4506
+ } else {
4507
+ currentIndex = nextIdx
4508
+ }
4509
+ } else {
4510
+ currentIndex = nextIdx
4511
+ }
4512
+ states[currentIndex].lastUsedAt = /* @__PURE__ */ new Date()
4513
+ totalRotations++
4514
+ rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1
4515
+ const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host
4516
+ logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`)
4517
+ return states[currentIndex].endpoint
4518
+ }
4519
+ function markFailureImpl() {
4520
+ const state = states[currentIndex]
4521
+ state.failures++
4522
+ const label = state.endpoint.label || state.endpoint.host
4523
+ logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`)
4524
+ if (state.failures >= maxFailures) {
4525
+ state.isDead = true
4526
+ logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`)
4527
+ const alive = getAliveEndpoints()
4528
+ if (alive.length > 0) {
4529
+ rotateImpl('manual')
4530
+ }
4531
+ }
4532
+ }
4533
+ function resurrectAllImpl() {
4534
+ let count = 0
4535
+ for (const state of states) {
4536
+ if (state.isDead) {
4537
+ state.isDead = false
4538
+ state.failures = 0
4539
+ count++
4540
+ }
4541
+ }
4542
+ if (count > 0) {
4543
+ logger.info?.(`Resurrected ${count} dead endpoint(s)`)
4544
+ }
4545
+ }
4546
+ function stopImpl() {
4547
+ if (scheduledTimer) {
4548
+ clearInterval(scheduledTimer)
4549
+ scheduledTimer = null
4550
+ logger.info?.('Stopped scheduled rotation timer')
4551
+ }
4552
+ }
4553
+ function getStatsImpl() {
4554
+ return {
4555
+ totalRotations,
4556
+ rotationsByTrigger: { ...rotationsByTrigger },
4557
+ endpointHealth: states.map(s => ({
4558
+ label: s.endpoint.label || s.endpoint.host,
4559
+ inUse: states[currentIndex] === s,
4560
+ failures: s.failures,
4561
+ lastUsedAt: s.lastUsedAt,
4562
+ isDead: s.isDead
4563
+ })),
4564
+ currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host
4565
+ }
4566
+ }
4567
+ function currentAgentImpl() {
4568
+ const endpoint = states[currentIndex].endpoint
4569
+ return createAgentForEndpointSync(endpoint)
4570
+ }
4571
+ function currentImpl() {
4572
+ return states[currentIndex].endpoint
4573
+ }
4574
+ if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
4575
+ scheduledTimer = setInterval(() => {
4576
+ rotateImpl('scheduled')
4577
+ }, scheduledIntervalMs)
4578
+ logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`)
4579
+ }
4580
+ states[0].lastUsedAt = /* @__PURE__ */ new Date()
4581
+ return {
4582
+ currentAgent: currentAgentImpl,
4583
+ current: currentImpl,
4584
+ rotate: rotateImpl,
4585
+ markFailure: markFailureImpl,
4586
+ resurrectAll: resurrectAllImpl,
4587
+ stop: stopImpl,
4588
+ getStats: getStatsImpl
4589
+ }
4590
+ }
4591
+ // Annotate the CommonJS export names for ESM import in node:
4592
+ 0 &&
4593
+ (module.exports = {
4594
+ AntiBan,
4595
+ ContactGraphWarmer,
4596
+ ContentVariator,
4597
+ FileStateAdapter,
4598
+ HealthMonitor,
4599
+ JidCanonicalizer,
4600
+ LidFirstResolver,
4601
+ LidResolver,
4602
+ MAC_ERROR_CODES,
4603
+ MessageQueue,
4604
+ MessageRetryReason,
4605
+ PRESETS,
4606
+ PostReconnectThrottle,
4607
+ PresenceChoreographer,
4608
+ RateLimiter,
4609
+ ReplyRatioGuard,
4610
+ RetryReasonTracker,
4611
+ Scheduler,
4612
+ SessionHealthMonitor,
4613
+ StateManager,
4614
+ TimelockGuard,
4615
+ WarmUp,
4616
+ WebhookAlerts,
4617
+ applyFingerprint,
4618
+ applyGroupMultiplier,
4619
+ classifyDisconnect,
4620
+ createLidFirstResolver,
4621
+ credsSnapshot,
4622
+ generateFingerprint,
4623
+ getCircadianMultiplier,
4624
+ getRetryReasonDescription,
4625
+ isBroadcast,
4626
+ isGroup,
4627
+ isMacError,
4628
+ isNewsletter,
4629
+ messageRecovery,
4630
+ parseRetryReason,
4631
+ proxyRotator,
4632
+ readReceiptVariance,
4633
+ resolveConfig,
4634
+ shouldUseGroupProfile,
4635
+ wrapSocket,
4636
+ wrapWithSessionStability
4637
+ })