@badzz88/baileys 8.4.7 → 8.4.9

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