@kangwifi-pro/waliwa 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +600 -0
  3. package/dist/advanced/features.d.ts +211 -0
  4. package/dist/advanced/features.d.ts.map +1 -0
  5. package/dist/advanced/features.js +671 -0
  6. package/dist/advanced/features.js.map +1 -0
  7. package/dist/auth/auth-state.d.ts +89 -0
  8. package/dist/auth/auth-state.d.ts.map +1 -0
  9. package/dist/auth/auth-state.js +330 -0
  10. package/dist/auth/auth-state.js.map +1 -0
  11. package/dist/auth/pairing-code.d.ts +78 -0
  12. package/dist/auth/pairing-code.d.ts.map +1 -0
  13. package/dist/auth/pairing-code.js +191 -0
  14. package/dist/auth/pairing-code.js.map +1 -0
  15. package/dist/auth/qr-code.d.ts +60 -0
  16. package/dist/auth/qr-code.d.ts.map +1 -0
  17. package/dist/auth/qr-code.js +151 -0
  18. package/dist/auth/qr-code.js.map +1 -0
  19. package/dist/calls/handler.d.ts +61 -0
  20. package/dist/calls/handler.d.ts.map +1 -0
  21. package/dist/calls/handler.js +117 -0
  22. package/dist/calls/handler.js.map +1 -0
  23. package/dist/core/binary.d.ts +74 -0
  24. package/dist/core/binary.d.ts.map +1 -0
  25. package/dist/core/binary.js +448 -0
  26. package/dist/core/binary.js.map +1 -0
  27. package/dist/core/crypto.d.ts +60 -0
  28. package/dist/core/crypto.d.ts.map +1 -0
  29. package/dist/core/crypto.js +170 -0
  30. package/dist/core/crypto.js.map +1 -0
  31. package/dist/core/noise.d.ts +106 -0
  32. package/dist/core/noise.d.ts.map +1 -0
  33. package/dist/core/noise.js +307 -0
  34. package/dist/core/noise.js.map +1 -0
  35. package/dist/events/emitter.d.ts +44 -0
  36. package/dist/events/emitter.d.ts.map +1 -0
  37. package/dist/events/emitter.js +79 -0
  38. package/dist/events/emitter.js.map +1 -0
  39. package/dist/features/index.d.ts +342 -0
  40. package/dist/features/index.d.ts.map +1 -0
  41. package/dist/features/index.js +755 -0
  42. package/dist/features/index.js.map +1 -0
  43. package/dist/fixes/index.d.ts +333 -0
  44. package/dist/fixes/index.d.ts.map +1 -0
  45. package/dist/fixes/index.js +762 -0
  46. package/dist/fixes/index.js.map +1 -0
  47. package/dist/groups/management.d.ts +86 -0
  48. package/dist/groups/management.d.ts.map +1 -0
  49. package/dist/groups/management.js +443 -0
  50. package/dist/groups/management.js.map +1 -0
  51. package/dist/index.d.ts +65 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +236 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/messages/media.d.ts +93 -0
  56. package/dist/messages/media.d.ts.map +1 -0
  57. package/dist/messages/media.js +252 -0
  58. package/dist/messages/media.js.map +1 -0
  59. package/dist/messages/message-encoder.d.ts +70 -0
  60. package/dist/messages/message-encoder.d.ts.map +1 -0
  61. package/dist/messages/message-encoder.js +453 -0
  62. package/dist/messages/message-encoder.js.map +1 -0
  63. package/dist/messages/send.d.ts +101 -0
  64. package/dist/messages/send.d.ts.map +1 -0
  65. package/dist/messages/send.js +409 -0
  66. package/dist/messages/send.js.map +1 -0
  67. package/dist/recovery/index.d.ts +125 -0
  68. package/dist/recovery/index.d.ts.map +1 -0
  69. package/dist/recovery/index.js +584 -0
  70. package/dist/recovery/index.js.map +1 -0
  71. package/dist/skdm/index.d.ts +220 -0
  72. package/dist/skdm/index.d.ts.map +1 -0
  73. package/dist/skdm/index.js +600 -0
  74. package/dist/skdm/index.js.map +1 -0
  75. package/dist/socket/hybrid.d.ts +118 -0
  76. package/dist/socket/hybrid.d.ts.map +1 -0
  77. package/dist/socket/hybrid.js +352 -0
  78. package/dist/socket/hybrid.js.map +1 -0
  79. package/dist/socket/wa-socket.d.ts +300 -0
  80. package/dist/socket/wa-socket.d.ts.map +1 -0
  81. package/dist/socket/wa-socket.js +1094 -0
  82. package/dist/socket/wa-socket.js.map +1 -0
  83. package/dist/socket/ws-socket.d.ts +96 -0
  84. package/dist/socket/ws-socket.d.ts.map +1 -0
  85. package/dist/socket/ws-socket.js +302 -0
  86. package/dist/socket/ws-socket.js.map +1 -0
  87. package/dist/types/index.d.ts +444 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +7 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/utils/jid.d.ts +46 -0
  92. package/dist/utils/jid.d.ts.map +1 -0
  93. package/dist/utils/jid.js +152 -0
  94. package/dist/utils/jid.js.map +1 -0
  95. package/dist/utils/logger.d.ts +45 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +79 -0
  98. package/dist/utils/logger.js.map +1 -0
  99. package/dist/utils/retry.d.ts +43 -0
  100. package/dist/utils/retry.d.ts.map +1 -0
  101. package/dist/utils/retry.js +175 -0
  102. package/dist/utils/retry.js.map +1 -0
  103. package/docs/API.md +745 -0
  104. package/docs/ARCHITECTURE.md +307 -0
  105. package/docs/BAILEYS_FIXES.md +360 -0
  106. package/docs/FEATURES.md +532 -0
  107. package/docs/PROTOCOL.md +489 -0
  108. package/docs/RECOVERY.md +409 -0
  109. package/docs/SKDM.md +233 -0
  110. package/examples/ai-bot/index.ts +479 -0
  111. package/examples/ai-bot/package.json +17 -0
  112. package/examples/echo-bot/index.ts +171 -0
  113. package/examples/echo-bot/package.json +18 -0
  114. package/examples/group-bot/index.ts +557 -0
  115. package/examples/group-bot/package.json +17 -0
  116. package/examples/rest-gateway/index.ts +499 -0
  117. package/examples/rest-gateway/package.json +19 -0
  118. package/package.json +75 -0
  119. package/src/advanced/features.ts +817 -0
  120. package/src/auth/auth-state.ts +342 -0
  121. package/src/auth/pairing-code.ts +246 -0
  122. package/src/auth/qr-code.ts +191 -0
  123. package/src/calls/handler.ts +153 -0
  124. package/src/core/binary.ts +464 -0
  125. package/src/core/crypto.ts +189 -0
  126. package/src/core/noise.ts +406 -0
  127. package/src/events/emitter.ts +88 -0
  128. package/src/features/index.ts +921 -0
  129. package/src/fixes/index.ts +882 -0
  130. package/src/groups/management.ts +497 -0
  131. package/src/index.ts +274 -0
  132. package/src/messages/media.ts +372 -0
  133. package/src/messages/message-encoder.ts +520 -0
  134. package/src/messages/send.ts +521 -0
  135. package/src/recovery/index.ts +704 -0
  136. package/src/skdm/index.ts +693 -0
  137. package/src/socket/hybrid.ts +414 -0
  138. package/src/socket/wa-socket.ts +1347 -0
  139. package/src/socket/ws-socket.ts +355 -0
  140. package/src/types/index.ts +457 -0
  141. package/src/utils/jid.ts +156 -0
  142. package/src/utils/logger.ts +84 -0
  143. package/src/utils/retry.ts +205 -0
  144. package/tsconfig.json +30 -0
@@ -0,0 +1,755 @@
1
+ "use strict";
2
+ /**
3
+ * Waliwa - New Features Module
4
+ *
5
+ * Module yang berisi fitur-fitur baru yang gak ada di Baileys:
6
+ *
7
+ * 1. Health Monitor - real-time health tracking dengan auto-recovery
8
+ * 2. Multi-account Manager - manage multiple WhatsApp accounts
9
+ * 3. Webhook System - reliable webhooks dengan retry + HMAC signature
10
+ * 4. Circuit Breaker - prevent retry storms untuk failed sends
11
+ * 5. Rate Limit Tracker - per-recipient rate limit tracking
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.MessagePersistence = exports.RateLimitTracker = exports.CircuitBreaker = exports.WebhookManager = exports.MultiAccountManager = exports.HealthMonitor = void 0;
15
+ const events_1 = require("events");
16
+ const logger_js_1 = require("../utils/logger.js");
17
+ const crypto_1 = require("crypto");
18
+ class HealthMonitor extends events_1.EventEmitter {
19
+ logger;
20
+ startTime;
21
+ lastPingTime = 0;
22
+ reconnectCount = 0;
23
+ messagesSent = 0;
24
+ messagesReceived = 0;
25
+ sendFailures = 0;
26
+ latencies = [];
27
+ maxLatencySamples = 100;
28
+ checkInterval = null;
29
+ autoRecoveryEnabled;
30
+ constructor(opts = {}) {
31
+ super();
32
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
33
+ this.startTime = Date.now();
34
+ this.autoRecoveryEnabled = opts.autoRecovery ?? true;
35
+ }
36
+ /**
37
+ * Start monitoring - periodic health checks
38
+ */
39
+ start(intervalMs = 60000) {
40
+ if (this.checkInterval)
41
+ return;
42
+ this.checkInterval = setInterval(() => {
43
+ const status = this.getHealth();
44
+ this.emit('health:check', status);
45
+ if (status.status === 'unhealthy' && this.autoRecoveryEnabled) {
46
+ this.logger.warn('Health degraded, triggering recovery actions');
47
+ this.emit('health:recovery', status);
48
+ }
49
+ }, intervalMs);
50
+ }
51
+ stop() {
52
+ if (this.checkInterval) {
53
+ clearInterval(this.checkInterval);
54
+ this.checkInterval = null;
55
+ }
56
+ }
57
+ recordPing() {
58
+ this.lastPingTime = Date.now();
59
+ }
60
+ recordReconnect() {
61
+ this.reconnectCount++;
62
+ }
63
+ recordSend(latencyMs, success = true) {
64
+ if (success) {
65
+ this.messagesSent++;
66
+ this.latencies.push(latencyMs);
67
+ if (this.latencies.length > this.maxLatencySamples) {
68
+ this.latencies.shift();
69
+ }
70
+ }
71
+ else {
72
+ this.sendFailures++;
73
+ }
74
+ }
75
+ recordReceive() {
76
+ this.messagesReceived++;
77
+ }
78
+ getHealth() {
79
+ const memUsage = process.memoryUsage();
80
+ const avgLatency = this.latencies.length > 0
81
+ ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length
82
+ : 0;
83
+ const issues = [];
84
+ let score = 100;
85
+ // Check connection
86
+ const connected = this.lastPingTime > 0 && (Date.now() - this.lastPingTime) < 60000;
87
+ if (!connected) {
88
+ issues.push('Connection appears down (no recent ping)');
89
+ score -= 40;
90
+ }
91
+ // Check reconnect frequency
92
+ if (this.reconnectCount > 10) {
93
+ issues.push(`High reconnect count: ${this.reconnectCount}`);
94
+ score -= 20;
95
+ }
96
+ // Check send failure rate
97
+ const totalSends = this.messagesSent + this.sendFailures;
98
+ const failureRate = totalSends > 0 ? this.sendFailures / totalSends : 0;
99
+ if (failureRate > 0.1) {
100
+ issues.push(`High send failure rate: ${(failureRate * 100).toFixed(1)}%`);
101
+ score -= 25;
102
+ }
103
+ // Check latency
104
+ if (avgLatency > 5000) {
105
+ issues.push(`High average latency: ${avgLatency.toFixed(0)}ms`);
106
+ score -= 15;
107
+ }
108
+ // Check memory
109
+ const memMb = memUsage.rss / 1024 / 1024;
110
+ if (memMb > 500) {
111
+ issues.push(`High memory usage: ${memMb.toFixed(1)}MB`);
112
+ score -= 10;
113
+ }
114
+ let status;
115
+ if (score >= 80)
116
+ status = 'healthy';
117
+ else if (score >= 50)
118
+ status = 'degraded';
119
+ else
120
+ status = 'unhealthy';
121
+ return {
122
+ status,
123
+ score: Math.max(0, score),
124
+ connection: {
125
+ connected,
126
+ uptime: Date.now() - this.startTime,
127
+ lastPingTime: this.lastPingTime,
128
+ reconnectCount: this.reconnectCount
129
+ },
130
+ metrics: {
131
+ messagesSent: this.messagesSent,
132
+ messagesReceived: this.messagesReceived,
133
+ sendFailures: this.sendFailures,
134
+ avgLatencyMs: avgLatency
135
+ },
136
+ resources: {
137
+ memoryUsageMb: memMb,
138
+ pendingSends: 0,
139
+ queueSize: 0
140
+ },
141
+ issues,
142
+ timestamp: Date.now()
143
+ };
144
+ }
145
+ reset() {
146
+ this.startTime = Date.now();
147
+ this.lastPingTime = 0;
148
+ this.reconnectCount = 0;
149
+ this.messagesSent = 0;
150
+ this.messagesReceived = 0;
151
+ this.sendFailures = 0;
152
+ this.latencies = [];
153
+ }
154
+ }
155
+ exports.HealthMonitor = HealthMonitor;
156
+ /**
157
+ * MultiAccountManager - manage multiple WhatsApp accounts dari satu process.
158
+ * Berguna untuk: customer service bots, multiple business accounts, dll.
159
+ *
160
+ * RAM optimization: lazy-load accounts, only active ones consume resources.
161
+ */
162
+ class MultiAccountManager extends events_1.EventEmitter {
163
+ accounts = new Map();
164
+ logger;
165
+ maxAccounts;
166
+ constructor(opts = {}) {
167
+ super();
168
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
169
+ this.maxAccounts = opts.maxAccounts ?? 10;
170
+ }
171
+ /**
172
+ * Register new account
173
+ */
174
+ registerAccount(config) {
175
+ if (this.accounts.size >= this.maxAccounts) {
176
+ throw new Error(`Max accounts (${this.maxAccounts}) reached`);
177
+ }
178
+ if (this.accounts.has(config.id)) {
179
+ throw new Error(`Account with id ${config.id} already exists`);
180
+ }
181
+ this.accounts.set(config.id, {
182
+ config,
183
+ socket: null,
184
+ status: {
185
+ id: config.id,
186
+ name: config.name,
187
+ connected: false,
188
+ lastActive: Date.now(),
189
+ messagesSent: 0,
190
+ messagesReceived: 0
191
+ }
192
+ });
193
+ this.logger.info(`Account registered: ${config.id} (${config.name})`);
194
+ }
195
+ /**
196
+ * Connect account by ID
197
+ */
198
+ async connectAccount(accountId) {
199
+ const entry = this.accounts.get(accountId);
200
+ if (!entry)
201
+ throw new Error(`Account ${accountId} not found`);
202
+ if (entry.socket) {
203
+ this.logger.warn(`Account ${accountId} already connected`);
204
+ return;
205
+ }
206
+ // Lazy load makeWASocket untuk hemat RAM
207
+ const { makeWASocket } = await import('../socket/wa-socket.js');
208
+ const { useFileAuthState } = await import('../auth/auth-state.js');
209
+ const state = await useFileAuthState(entry.config.authFolder, accountId);
210
+ const socket = makeWASocket({
211
+ ...entry.config.config,
212
+ authState: state
213
+ });
214
+ // Track connection
215
+ socket.ev.on('connection.update', (update) => {
216
+ if (update.connection === 'open') {
217
+ entry.status.connected = true;
218
+ entry.status.user = socket.user;
219
+ entry.status.lastActive = Date.now();
220
+ this.emit('account:connected', accountId);
221
+ }
222
+ else if (update.connection === 'close') {
223
+ entry.status.connected = false;
224
+ this.emit('account:disconnected', accountId);
225
+ }
226
+ });
227
+ socket.ev.on('message.upsert', ({ messages }) => {
228
+ entry.status.messagesReceived += messages.length;
229
+ entry.status.lastActive = Date.now();
230
+ this.emit('account:message', accountId, messages);
231
+ });
232
+ entry.socket = socket;
233
+ this.logger.info(`Account ${accountId} connected`);
234
+ }
235
+ /**
236
+ * Disconnect account
237
+ */
238
+ async disconnectAccount(accountId) {
239
+ const entry = this.accounts.get(accountId);
240
+ if (!entry || !entry.socket)
241
+ return;
242
+ await entry.socket.end();
243
+ entry.socket = null;
244
+ entry.status.connected = false;
245
+ this.logger.info(`Account ${accountId} disconnected`);
246
+ }
247
+ /**
248
+ * Get account socket (untuk send messages)
249
+ */
250
+ getSocket(accountId) {
251
+ const entry = this.accounts.get(accountId);
252
+ return entry?.socket;
253
+ }
254
+ /**
255
+ * List all accounts dengan status
256
+ */
257
+ listAccounts() {
258
+ return Array.from(this.accounts.values()).map(e => e.status);
259
+ }
260
+ /**
261
+ * Send message from specific account
262
+ */
263
+ async sendMessageFrom(accountId, jid, content) {
264
+ const entry = this.accounts.get(accountId);
265
+ if (!entry?.socket) {
266
+ throw new Error(`Account ${accountId} not connected`);
267
+ }
268
+ const result = await entry.socket.sendMessage(jid, content);
269
+ entry.status.messagesSent++;
270
+ entry.status.lastActive = Date.now();
271
+ return result;
272
+ }
273
+ /**
274
+ * Broadcast message dari multiple accounts
275
+ */
276
+ async broadcast(jid, content, accountIds) {
277
+ const ids = accountIds ?? Array.from(this.accounts.keys());
278
+ const promises = ids.map(async (id) => {
279
+ try {
280
+ await this.sendMessageFrom(id, jid, content);
281
+ return { id, success: true };
282
+ }
283
+ catch (err) {
284
+ this.logger.error(`Broadcast from ${id} failed: ${err.message}`);
285
+ return { id, success: false, error: err.message };
286
+ }
287
+ });
288
+ await Promise.all(promises);
289
+ }
290
+ /**
291
+ * Disconnect all accounts
292
+ */
293
+ async disconnectAll() {
294
+ const promises = Array.from(this.accounts.keys()).map(id => this.disconnectAccount(id));
295
+ await Promise.all(promises);
296
+ }
297
+ }
298
+ exports.MultiAccountManager = MultiAccountManager;
299
+ /**
300
+ * WebhookManager - reliable webhook delivery dengan retry + HMAC signature.
301
+ * Fix common Baileys issue: webhooks tidak reliable.
302
+ *
303
+ * Strategy:
304
+ * - HMAC-SHA256 signature untuk verify authenticity
305
+ * - Exponential backoff retry
306
+ * - Queue pending webhooks jika network issue
307
+ * - Dead letter queue untuk permanent failures
308
+ */
309
+ class WebhookManager {
310
+ webhooks = new Map();
311
+ pendingQueue = [];
312
+ deadLetterQueue = [];
313
+ logger;
314
+ processingTimer = null;
315
+ isProcessing = false;
316
+ stats = { sent: 0, failed: 0, retried: 0 };
317
+ constructor(opts = {}) {
318
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
319
+ // Process queue every 5 seconds
320
+ this.processingTimer = setInterval(() => this.processQueue(), 5000);
321
+ }
322
+ /**
323
+ * Register webhook
324
+ */
325
+ register(config) {
326
+ this.webhooks.set(config.url, config);
327
+ this.logger.info(`Webhook registered: ${config.url} for events: ${config.events.join(', ')}`);
328
+ }
329
+ /**
330
+ * Unregister webhook
331
+ */
332
+ unregister(url) {
333
+ this.webhooks.delete(url);
334
+ }
335
+ /**
336
+ * Trigger webhook delivery
337
+ */
338
+ trigger(event, payload) {
339
+ for (const config of this.webhooks.values()) {
340
+ if (!config.enabled)
341
+ continue;
342
+ if (config.events.includes('*') || config.events.includes(event)) {
343
+ const id = `wh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
344
+ this.pendingQueue.push({
345
+ id,
346
+ url: config.url,
347
+ payload: { event, timestamp: Date.now(), data: payload },
348
+ secret: config.secret,
349
+ attempts: 0,
350
+ nextRetryAt: Date.now()
351
+ });
352
+ }
353
+ }
354
+ }
355
+ async processQueue() {
356
+ if (this.isProcessing)
357
+ return;
358
+ this.isProcessing = true;
359
+ const now = Date.now();
360
+ const toProcess = this.pendingQueue.filter(w => w.nextRetryAt <= now);
361
+ this.pendingQueue = this.pendingQueue.filter(w => w.nextRetryAt > now);
362
+ for (const webhook of toProcess) {
363
+ await this.deliver(webhook);
364
+ }
365
+ this.isProcessing = false;
366
+ }
367
+ async deliver(webhook) {
368
+ const config = this.webhooks.get(webhook.url);
369
+ if (!config) {
370
+ // Webhook was unregistered, drop
371
+ return;
372
+ }
373
+ webhook.attempts++;
374
+ const body = JSON.stringify(webhook.payload);
375
+ // Sign with HMAC jika secret provided
376
+ const headers = {
377
+ 'Content-Type': 'application/json'
378
+ };
379
+ if (webhook.secret) {
380
+ const signature = (0, crypto_1.createHmac)('sha256', webhook.secret)
381
+ .update(body)
382
+ .digest('hex');
383
+ headers['X-Waliwa-Signature'] = `sha256=${signature}`;
384
+ headers['X-Waliwa-Event'] = webhook.payload.event;
385
+ }
386
+ try {
387
+ const controller = new AbortController();
388
+ const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
389
+ const response = await fetch(webhook.url, {
390
+ method: 'POST',
391
+ headers,
392
+ body,
393
+ signal: controller.signal
394
+ });
395
+ clearTimeout(timeout);
396
+ if (response.status >= 200 && response.status < 300) {
397
+ this.stats.sent++;
398
+ this.logger.debug(`Webhook ${webhook.id} delivered successfully`);
399
+ }
400
+ else if (response.status >= 400 && response.status < 500) {
401
+ // Client error - don't retry (4xx = permanent failure)
402
+ this.stats.failed++;
403
+ this.deadLetterQueue.push(webhook);
404
+ this.logger.warn(`Webhook ${webhook.id} rejected with ${response.status}, moved to DLQ`);
405
+ }
406
+ else {
407
+ // Server error - retry
408
+ throw new Error(`HTTP ${response.status}`);
409
+ }
410
+ }
411
+ catch (err) {
412
+ this.logger.warn(`Webhook ${webhook.id} delivery failed (attempt ${webhook.attempts}): ${err.message}`);
413
+ if (webhook.attempts >= config.retryCount) {
414
+ // Move to dead letter queue
415
+ this.deadLetterQueue.push(webhook);
416
+ this.stats.failed++;
417
+ this.logger.error(`Webhook ${webhook.id} permanently failed, moved to DLQ`);
418
+ }
419
+ else {
420
+ // Schedule retry dengan exponential backoff
421
+ const delay = config.retryDelayMs * Math.pow(2, webhook.attempts - 1);
422
+ webhook.nextRetryAt = Date.now() + delay;
423
+ this.pendingQueue.push(webhook);
424
+ this.stats.retried++;
425
+ }
426
+ }
427
+ }
428
+ getStats() {
429
+ return {
430
+ ...this.stats,
431
+ pending: this.pendingQueue.length,
432
+ deadLettered: this.deadLetterQueue.length
433
+ };
434
+ }
435
+ /**
436
+ * Get dead letter queue (untuk manual retry)
437
+ */
438
+ getDeadLetterQueue() {
439
+ return this.deadLetterQueue;
440
+ }
441
+ /**
442
+ * Retry dari DLQ
443
+ */
444
+ retryDeadLetter(id) {
445
+ if (id) {
446
+ const idx = this.deadLetterQueue.findIndex(w => w.id === id);
447
+ if (idx !== -1) {
448
+ const webhook = this.deadLetterQueue.splice(idx, 1)[0];
449
+ webhook.attempts = 0;
450
+ webhook.nextRetryAt = Date.now();
451
+ this.pendingQueue.push(webhook);
452
+ }
453
+ }
454
+ else {
455
+ // Retry all
456
+ for (const webhook of this.deadLetterQueue) {
457
+ webhook.attempts = 0;
458
+ webhook.nextRetryAt = Date.now();
459
+ this.pendingQueue.push(webhook);
460
+ }
461
+ this.deadLetterQueue = [];
462
+ }
463
+ }
464
+ destroy() {
465
+ if (this.processingTimer) {
466
+ clearInterval(this.processingTimer);
467
+ this.processingTimer = null;
468
+ }
469
+ this.pendingQueue = [];
470
+ this.deadLetterQueue = [];
471
+ }
472
+ }
473
+ exports.WebhookManager = WebhookManager;
474
+ // ============================================================
475
+ // FEATURE 4: Circuit Breaker untuk failed sends
476
+ // ============================================================
477
+ /**
478
+ * CircuitBreaker - prevent retry storms untuk failed recipients.
479
+ * Fix Baileys issue: failed sends terus retry, flood server, get banned.
480
+ *
481
+ * Strategy:
482
+ * - Track failure count per recipient
483
+ * - When threshold reached, open circuit (stop sending)
484
+ * - After cooldown, try again (half-open state)
485
+ * - If success, close circuit; if fail, re-open
486
+ */
487
+ class CircuitBreaker {
488
+ states = new Map();
489
+ failureThreshold;
490
+ cooldownMs;
491
+ logger;
492
+ constructor(opts = {}) {
493
+ this.failureThreshold = opts.failureThreshold ?? 5;
494
+ this.cooldownMs = opts.cooldownMs ?? 60 * 1000; // 1 minute default
495
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
496
+ }
497
+ /**
498
+ * Check if can send to recipient
499
+ */
500
+ canSend(recipientJid) {
501
+ const state = this.states.get(recipientJid);
502
+ if (!state)
503
+ return true; // No state = closed
504
+ if (state.state === 'closed')
505
+ return true;
506
+ if (state.state === 'open') {
507
+ // Check if cooldown elapsed
508
+ if (Date.now() - state.lastFailureTime > this.cooldownMs) {
509
+ // Half-open: allow one attempt
510
+ state.state = 'half-open';
511
+ this.logger.info(`Circuit half-open for ${recipientJid}`);
512
+ return true;
513
+ }
514
+ return false;
515
+ }
516
+ // half-open: only allow one concurrent request
517
+ return true;
518
+ }
519
+ /**
520
+ * Record successful send
521
+ */
522
+ recordSuccess(recipientJid) {
523
+ const state = this.states.get(recipientJid);
524
+ if (state) {
525
+ state.failureCount = 0;
526
+ state.state = 'closed';
527
+ }
528
+ }
529
+ /**
530
+ * Record failed send
531
+ */
532
+ recordFailure(recipientJid) {
533
+ let state = this.states.get(recipientJid);
534
+ if (!state) {
535
+ state = {
536
+ failureCount: 0,
537
+ lastFailureTime: Date.now(),
538
+ state: 'closed'
539
+ };
540
+ this.states.set(recipientJid, state);
541
+ }
542
+ state.failureCount++;
543
+ state.lastFailureTime = Date.now();
544
+ if (state.failureCount >= this.failureThreshold) {
545
+ state.state = 'open';
546
+ this.logger.warn(`Circuit opened for ${recipientJid} after ${state.failureCount} failures`);
547
+ }
548
+ }
549
+ /**
550
+ * Manually reset circuit untuk recipient
551
+ */
552
+ reset(recipientJid) {
553
+ this.states.delete(recipientJid);
554
+ }
555
+ /**
556
+ * Get circuit state untuk recipient
557
+ */
558
+ getState(recipientJid) {
559
+ return this.states.get(recipientJid)?.state ?? 'closed';
560
+ }
561
+ /**
562
+ * Get all open circuits (untuk monitoring)
563
+ */
564
+ getOpenCircuits() {
565
+ const result = [];
566
+ for (const [jid, state] of this.states.entries()) {
567
+ if (state.state === 'open') {
568
+ result.push({
569
+ jid,
570
+ failureCount: state.failureCount,
571
+ lastFailureTime: state.lastFailureTime
572
+ });
573
+ }
574
+ }
575
+ return result;
576
+ }
577
+ /**
578
+ * Cleanup expired circuits (cooldown elapsed and still open)
579
+ */
580
+ cleanup() {
581
+ const now = Date.now();
582
+ for (const [jid, state] of this.states.entries()) {
583
+ if (state.state === 'open' && (now - state.lastFailureTime) > this.cooldownMs * 2) {
584
+ this.states.delete(jid);
585
+ }
586
+ }
587
+ }
588
+ }
589
+ exports.CircuitBreaker = CircuitBreaker;
590
+ // ============================================================
591
+ // FEATURE 5: Rate Limit Tracker per recipient
592
+ // ============================================================
593
+ /**
594
+ * RateLimitTracker - per-recipient rate limiting.
595
+ * Berguna untuk: prevent spam, comply dengan WA server limits.
596
+ *
597
+ * WhatsApp server limits (approximate):
598
+ * - 5 messages/sec per recipient
599
+ * - 60 messages/min per recipient
600
+ * - 1000 messages/hour per recipient
601
+ */
602
+ class RateLimitTracker {
603
+ counts = new Map();
604
+ limits;
605
+ logger;
606
+ constructor(opts = {}) {
607
+ this.limits = {
608
+ perSecond: opts.perSecond ?? 5,
609
+ perMinute: opts.perMinute ?? 60,
610
+ perHour: opts.perHour ?? 1000
611
+ };
612
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
613
+ }
614
+ /**
615
+ * Check if can send to recipient (and increment counter)
616
+ */
617
+ canSend(recipientJid) {
618
+ const now = Date.now();
619
+ let entry = this.counts.get(recipientJid);
620
+ if (!entry) {
621
+ entry = {
622
+ second: 0,
623
+ minute: 0,
624
+ hour: 0,
625
+ lastReset: { second: now, minute: now, hour: now }
626
+ };
627
+ this.counts.set(recipientJid, entry);
628
+ }
629
+ // Reset counters jika time window elapsed
630
+ if (now - entry.lastReset.second > 1000) {
631
+ entry.second = 0;
632
+ entry.lastReset.second = now;
633
+ }
634
+ if (now - entry.lastReset.minute > 60000) {
635
+ entry.minute = 0;
636
+ entry.lastReset.minute = now;
637
+ }
638
+ if (now - entry.lastReset.hour > 3600000) {
639
+ entry.hour = 0;
640
+ entry.lastReset.hour = now;
641
+ }
642
+ // Check limits
643
+ if (entry.second >= this.limits.perSecond)
644
+ return false;
645
+ if (entry.minute >= this.limits.perMinute)
646
+ return false;
647
+ if (entry.hour >= this.limits.perHour)
648
+ return false;
649
+ // Increment
650
+ entry.second++;
651
+ entry.minute++;
652
+ entry.hour++;
653
+ return true;
654
+ }
655
+ /**
656
+ * Get remaining quota untuk recipient
657
+ */
658
+ getRemainingQuota(recipientJid) {
659
+ const entry = this.counts.get(recipientJid);
660
+ if (!entry) {
661
+ return { second: this.limits.perSecond, minute: this.limits.perMinute, hour: this.limits.perHour };
662
+ }
663
+ const now = Date.now();
664
+ return {
665
+ second: Math.max(0, this.limits.perSecond - (now - entry.lastReset.second > 1000 ? 0 : entry.second)),
666
+ minute: Math.max(0, this.limits.perMinute - (now - entry.lastReset.minute > 60000 ? 0 : entry.minute)),
667
+ hour: Math.max(0, this.limits.perHour - (now - entry.lastReset.hour > 3600000 ? 0 : entry.hour))
668
+ };
669
+ }
670
+ /**
671
+ * Reset counter untuk recipient (e.g., on successful interaction)
672
+ */
673
+ reset(recipientJid) {
674
+ this.counts.delete(recipientJid);
675
+ }
676
+ /**
677
+ * Cleanup inactive recipients (no sends in last hour)
678
+ */
679
+ cleanup() {
680
+ const now = Date.now();
681
+ for (const [jid, entry] of this.counts.entries()) {
682
+ if (now - entry.lastReset.hour > 3600000) {
683
+ this.counts.delete(jid);
684
+ }
685
+ }
686
+ }
687
+ getStats() {
688
+ return {
689
+ trackedRecipients: this.counts.size,
690
+ limits: { perSecond: this.limits.perSecond, perMinute: this.limits.perMinute, perHour: this.limits.perHour }
691
+ };
692
+ }
693
+ }
694
+ exports.RateLimitTracker = RateLimitTracker;
695
+ class MessagePersistence {
696
+ messages = new Map();
697
+ maxRetention;
698
+ logger;
699
+ constructor(opts = {}) {
700
+ this.maxRetention = opts.maxRetention ?? 10000; // Keep last 10k messages
701
+ this.logger = opts.logger ?? new logger_js_1.SilentLogger();
702
+ }
703
+ save(message) {
704
+ this.messages.set(message.id, message);
705
+ // Trim if over max
706
+ if (this.messages.size > this.maxRetention) {
707
+ const firstKey = this.messages.keys().next().value;
708
+ if (firstKey)
709
+ this.messages.delete(firstKey);
710
+ }
711
+ }
712
+ get(id) {
713
+ return this.messages.get(id);
714
+ }
715
+ update(id, update) {
716
+ const existing = this.messages.get(id);
717
+ if (existing) {
718
+ this.messages.set(id, { ...existing, ...update, updatedAt: Date.now() });
719
+ }
720
+ }
721
+ /**
722
+ * Get all pending messages (untuk retry on restart)
723
+ */
724
+ getPending() {
725
+ return Array.from(this.messages.values()).filter(m => m.status === 'pending');
726
+ }
727
+ /**
728
+ * Get all failed messages (untuk manual retry)
729
+ */
730
+ getFailed() {
731
+ return Array.from(this.messages.values()).filter(m => m.status === 'failed');
732
+ }
733
+ delete(id) {
734
+ this.messages.delete(id);
735
+ }
736
+ /**
737
+ * Cleanup old messages (older than X ms)
738
+ */
739
+ cleanup(maxAgeMs = 24 * 60 * 60 * 1000) {
740
+ const cutoff = Date.now() - maxAgeMs;
741
+ let cleaned = 0;
742
+ for (const [id, msg] of this.messages.entries()) {
743
+ if (msg.updatedAt < cutoff) {
744
+ this.messages.delete(id);
745
+ cleaned++;
746
+ }
747
+ }
748
+ return cleaned;
749
+ }
750
+ size() {
751
+ return this.messages.size;
752
+ }
753
+ }
754
+ exports.MessagePersistence = MessagePersistence;
755
+ //# sourceMappingURL=index.js.map