baileys-antiban 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +291 -0
  3. package/package.json +32 -0
  4. package/stress-test.ts +199 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kobus Wentzel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # baileys-antiban ๐Ÿ›ก๏ธ
2
+
3
+ Anti-ban middleware for [Baileys](https://github.com/WhiskeySockets/Baileys) โ€” protect your WhatsApp number with human-like messaging patterns.
4
+
5
+ ## Why?
6
+
7
+ WhatsApp bans numbers that behave like bots. This library makes your Baileys bot behave like a human:
8
+
9
+ - **Rate limiting** with human-like timing (gaussian jitter, typing simulation)
10
+ - **Warm-up** for new numbers (gradual activity increase over 7 days)
11
+ - **Health monitoring** that detects ban warning signs before it's too late
12
+ - **Auto-pause** when risk gets too high
13
+ - **Drop-in wrapper** โ€” one line to protect your existing bot
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install baileys-antiban
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Option 1: Wrap your socket (easiest)
24
+
25
+ ```typescript
26
+ import makeWASocket from 'baileys';
27
+ import { wrapSocket } from 'baileys-antiban';
28
+
29
+ const sock = makeWASocket({ /* your config */ });
30
+ const safeSock = wrapSocket(sock);
31
+
32
+ // Use safeSock instead of sock โ€” sendMessage is now protected
33
+ await safeSock.sendMessage(jid, { text: 'Hello!' });
34
+
35
+ // Check health anytime
36
+ console.log(safeSock.antiban.getStats());
37
+ ```
38
+
39
+ ### Option 2: Manual control
40
+
41
+ ```typescript
42
+ import { AntiBan } from 'baileys-antiban';
43
+
44
+ const antiban = new AntiBan();
45
+
46
+ // Before every message:
47
+ const decision = await antiban.beforeSend(recipient, content);
48
+
49
+ if (decision.allowed) {
50
+ // Wait the recommended delay
51
+ await new Promise(r => setTimeout(r, decision.delayMs));
52
+
53
+ try {
54
+ await sock.sendMessage(recipient, { text: content });
55
+ antiban.afterSend(recipient, content);
56
+ } catch (err) {
57
+ antiban.afterSendFailed(err.message);
58
+ }
59
+ } else {
60
+ console.log('Blocked:', decision.reason);
61
+ }
62
+
63
+ // In your connection.update handler:
64
+ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
65
+ if (connection === 'close') {
66
+ antiban.onDisconnect(lastDisconnect?.error?.output?.statusCode);
67
+ }
68
+ if (connection === 'open') {
69
+ antiban.onReconnect();
70
+ }
71
+ });
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ ```typescript
77
+ const antiban = new AntiBan({
78
+ rateLimiter: {
79
+ maxPerMinute: 8, // Max messages per minute (default: 8)
80
+ maxPerHour: 200, // Max messages per hour (default: 200)
81
+ maxPerDay: 1500, // Max messages per day (default: 1500)
82
+ minDelayMs: 1500, // Min delay between messages (default: 1500ms)
83
+ maxDelayMs: 5000, // Max delay between messages (default: 5000ms)
84
+ newChatDelayMs: 3000, // Extra delay for first message to new chat
85
+ maxIdenticalMessages: 3, // Block after 3 identical messages
86
+ burstAllowance: 3, // Fast messages before rate limiting kicks in
87
+ },
88
+ warmUp: {
89
+ warmUpDays: 7, // Days to full capacity (default: 7)
90
+ day1Limit: 20, // Messages allowed on day 1 (default: 20)
91
+ growthFactor: 1.8, // Daily limit multiplier (~doubles each day)
92
+ inactivityThresholdHours: 72, // Re-enter warm-up after 3 days inactive
93
+ },
94
+ health: {
95
+ disconnectWarningThreshold: 3, // Disconnects/hour before warning
96
+ disconnectCriticalThreshold: 5, // Disconnects/hour before critical
97
+ failedMessageThreshold: 5, // Failed messages/hour before warning
98
+ autoPauseAt: 'high', // Auto-pause at this risk level
99
+ onRiskChange: (status) => {
100
+ // Custom handler โ€” send alert, log, etc.
101
+ console.log(`Risk: ${status.risk}`, status.recommendation);
102
+ },
103
+ },
104
+ logging: true, // Console logging (default: true)
105
+ });
106
+ ```
107
+
108
+ ## Health Monitor
109
+
110
+ The health monitor tracks ban warning signs:
111
+
112
+ | Signal | Risk Score | What It Means |
113
+ |--------|-----------|---------------|
114
+ | Frequent disconnects | +15 to +30 | WhatsApp dropping your connection |
115
+ | 403 Forbidden | +40 per event | WhatsApp actively blocking you |
116
+ | 401 Logged Out | +60 | Possible temporary ban |
117
+ | Failed messages | +20 | Messages not going through |
118
+
119
+ Risk levels:
120
+ - ๐ŸŸข **Low** (0-29): Operating normally
121
+ - ๐ŸŸก **Medium** (30-59): Reduce messaging rate by 50%
122
+ - ๐ŸŸ  **High** (60-84): Reduce by 80%, consider pausing
123
+ - ๐Ÿ”ด **Critical** (85-100): **STOP IMMEDIATELY**
124
+
125
+ ```typescript
126
+ const status = antiban.getStats().health;
127
+ console.log(status.risk); // 'low' | 'medium' | 'high' | 'critical'
128
+ console.log(status.score); // 0-100
129
+ console.log(status.recommendation); // Human-readable advice
130
+ ```
131
+
132
+ ## Warm-Up Schedule
133
+
134
+ New numbers ramp up gradually:
135
+
136
+ | Day | Message Limit |
137
+ |-----|--------------|
138
+ | 1 | 20 |
139
+ | 2 | 36 |
140
+ | 3 | 65 |
141
+ | 4 | 117 |
142
+ | 5 | 210 |
143
+ | 6 | 378 |
144
+ | 7 | 680 |
145
+ | 8+ | Unlimited |
146
+
147
+ Persist warm-up state between restarts:
148
+
149
+ ```typescript
150
+ // Save state
151
+ const state = antiban.exportWarmUpState();
152
+ fs.writeFileSync('warmup.json', JSON.stringify(state));
153
+
154
+ // Restore state
155
+ const saved = JSON.parse(fs.readFileSync('warmup.json', 'utf-8'));
156
+ const antiban = new AntiBan(config, saved);
157
+ ```
158
+
159
+ ## Rate Limiter Details
160
+
161
+ The rate limiter mimics human behavior:
162
+
163
+ - **Gaussian jitter**: Delays clustered around the middle of the range, not uniform random
164
+ - **Typing simulation**: Longer messages get longer delays (~30ms per character)
165
+ - **New chat penalty**: First message to an unknown recipient gets extra delay
166
+ - **Burst allowance**: First 3 messages are faster (humans do this too)
167
+ - **Identical message detection**: Blocks sending the same text more than 3 times
168
+ - **Time-of-day awareness**: Built-in support for custom schedules
169
+
170
+ ## Message Queue
171
+
172
+ Queue messages for safe, paced delivery with auto-retry:
173
+
174
+ ```typescript
175
+ import { MessageQueue } from 'baileys-antiban';
176
+
177
+ const queue = new MessageQueue({ maxAttempts: 3 });
178
+ queue.setSendFunction(async (jid, content) => {
179
+ await safeSock.sendMessage(jid, content);
180
+ });
181
+
182
+ // Queue messages
183
+ queue.add('group@g.us', { text: 'Hello!' });
184
+ queue.add('group@g.us', { text: 'Important!' }, { priority: 'high' });
185
+ queue.addBulk(['user1@s.whatsapp.net', 'user2@s.whatsapp.net'], { text: 'Broadcast' });
186
+
187
+ // Start processing
188
+ queue.start();
189
+
190
+ // Events
191
+ queue.on('sent', (msg) => console.log('Sent:', msg.id));
192
+ queue.on('failed', (msg, err) => console.log('Failed:', msg.id, err));
193
+ queue.on('retry', (msg, attempt, delay) => console.log(`Retry #${attempt} in ${delay}ms`));
194
+ ```
195
+
196
+ ## Content Variator
197
+
198
+ Auto-vary messages to avoid identical message detection:
199
+
200
+ ```typescript
201
+ import { ContentVariator } from 'baileys-antiban';
202
+
203
+ const variator = new ContentVariator({
204
+ zeroWidthChars: true, // Invisible character variations
205
+ punctuationVariation: true, // Subtle punctuation changes
206
+ synonyms: true, // Replace common words with synonyms
207
+ });
208
+
209
+ // Each call returns a unique variation
210
+ const msg1 = variator.vary('Check out our auction today!');
211
+ const msg2 = variator.vary('Check out our auction today!');
212
+ // msg1 !== msg2 (technically different, looks the same to humans)
213
+
214
+ // Generate bulk variations for broadcast
215
+ const variations = variator.varyBulk('Hello everyone!', 50);
216
+ ```
217
+
218
+ ## Smart Scheduler
219
+
220
+ Send during safe hours with realistic daily patterns:
221
+
222
+ ```typescript
223
+ import { Scheduler } from 'baileys-antiban';
224
+
225
+ const scheduler = new Scheduler({
226
+ timezone: 'Africa/Johannesburg',
227
+ activeHours: [8, 21], // 8 AM to 9 PM
228
+ weekendFactor: 0.5, // Half speed on weekends
229
+ peakHours: [10, 14], // Faster during business hours
230
+ lunchBreak: [12, 13], // Slow down at lunch
231
+ });
232
+
233
+ if (scheduler.isActiveTime()) {
234
+ const adjustedDelay = scheduler.adjustDelay(baseDelay);
235
+ // Send with adjusted timing
236
+ } else {
237
+ console.log(`Next active in ${scheduler.msUntilActive()}ms`);
238
+ }
239
+ ```
240
+
241
+ ## Webhook Alerts
242
+
243
+ Get notified when risk level changes:
244
+
245
+ ```typescript
246
+ import { WebhookAlerts } from 'baileys-antiban';
247
+
248
+ const alerts = new WebhookAlerts({
249
+ // Telegram alerts
250
+ telegram: { botToken: 'BOT_TOKEN', chatId: 'CHAT_ID' },
251
+ // Discord alerts
252
+ discord: { webhookUrl: 'https://discord.com/api/webhooks/...' },
253
+ // Generic webhooks
254
+ urls: ['https://your-server.com/webhook'],
255
+ minRiskLevel: 'medium',
256
+ });
257
+
258
+ // Wire into health monitor
259
+ const antiban = new AntiBan({
260
+ health: {
261
+ onRiskChange: (status) => alerts.alert(status),
262
+ },
263
+ });
264
+ ```
265
+
266
+ ## Emergency Controls
267
+
268
+ ```typescript
269
+ // Manually pause all sending
270
+ antiban.pause();
271
+
272
+ // Resume
273
+ antiban.resume();
274
+
275
+ // Nuclear reset (use after serving a ban period)
276
+ antiban.reset();
277
+ ```
278
+
279
+ ## Best Practices
280
+
281
+ 1. **Always warm up new numbers** โ€” Don't send 1000 messages on day 1
282
+ 2. **Use a real phone number** โ€” Virtual/VOIP numbers get banned faster
283
+ 3. **Don't send identical messages** โ€” Vary your content even slightly
284
+ 4. **Respect the health monitor** โ€” When it says stop, STOP
285
+ 5. **Persist warm-up state** โ€” Don't lose progress on restart
286
+ 6. **Monitor your stats** โ€” Check `getStats()` regularly
287
+ 7. **Have a backup number** โ€” Bans happen despite best efforts
288
+
289
+ ## License
290
+
291
+ MIT โ€” Built by [WhatsAuction](https://whatsauction.co.za) ๐Ÿ‡ฟ๐Ÿ‡ฆ
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "baileys-antiban",
3
+ "version": "1.0.0",
4
+ "description": "Anti-ban middleware for Baileys โ€” human-like messaging patterns to protect your WhatsApp number",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "jest"
11
+ },
12
+ "keywords": [
13
+ "baileys",
14
+ "whatsapp",
15
+ "anti-ban",
16
+ "rate-limit",
17
+ "middleware"
18
+ ],
19
+ "author": "Kobus Wentzel <kobie@pop.co.za>",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/kobie3717/baileys-antiban"
24
+ },
25
+ "peerDependencies": {
26
+ "baileys": ">=6.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.0.0",
30
+ "@types/node": "^20.0.0"
31
+ }
32
+ }
package/stress-test.ts ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Stress Test โ€” Send 1000 messages as fast as safely possible
3
+ *
4
+ * This pushes the anti-ban to its limits while keeping your number alive.
5
+ * Run: AUTH_DIR=<path> npx tsx stress-test.ts <group-jid>
6
+ */
7
+
8
+ import makeWASocket, { useMultiFileAuthState, DisconnectReason } from 'baileys';
9
+ import pino from 'pino';
10
+ import { AntiBan } from './src/antiban.js';
11
+ import { ContentVariator } from './src/contentVariator.js';
12
+
13
+ const AUTH_DIR = process.env.AUTH_DIR || './test-auth';
14
+ const GROUP_JID = process.argv[2];
15
+ const TARGET = parseInt(process.argv[3] || '1000');
16
+
17
+ if (!GROUP_JID) {
18
+ console.log('Usage: AUTH_DIR=<path> npx tsx stress-test.ts <group-jid> [count]');
19
+ process.exit(1);
20
+ }
21
+
22
+ // Aggressive but safe config
23
+ const antiban = new AntiBan({
24
+ rateLimiter: {
25
+ maxPerMinute: 12, // Push it โ€” 12/min
26
+ maxPerHour: 500, // 500/hr
27
+ maxPerDay: 2000, // 2000/day
28
+ minDelayMs: 800, // Fast but not instant
29
+ maxDelayMs: 3000, // Cap delay at 3s
30
+ burstAllowance: 5, // Allow 5 fast messages
31
+ maxIdenticalMessages: 50, // High โ€” we use variator
32
+ newChatDelayMs: 500, // Low โ€” same group every time
33
+ },
34
+ warmUp: { warmUpDays: 0 }, // Skip warm-up for stress test
35
+ health: {
36
+ autoPauseAt: 'high',
37
+ onRiskChange: (status) => {
38
+ console.log(`\nโš ๏ธ RISK: ${status.risk.toUpperCase()} (score: ${status.score}) โ€” ${status.recommendation}\n`);
39
+ },
40
+ },
41
+ logging: false,
42
+ });
43
+
44
+ const variator = new ContentVariator({
45
+ zeroWidthChars: true,
46
+ punctuationVariation: true,
47
+ emojiPadding: false,
48
+ synonyms: false,
49
+ });
50
+
51
+ // Message templates
52
+ const templates = [
53
+ '๐Ÿ›ก๏ธ Stress test #{n}/1000 โ€” anti-ban holding strong',
54
+ 'โšก Message #{n} โ€” speed run in progress',
55
+ '๐Ÿงช #{n} โ€” pushing the limits safely',
56
+ '๐Ÿ“Š #{n}/1000 โ€” rate limiter active',
57
+ '๐Ÿ”ฅ #{n} โ€” no ban, no problem',
58
+ 'โœ… #{n} sent โ€” number still alive',
59
+ '๐ŸŽ๏ธ Fast message #{n} โ€” jitter applied',
60
+ '๐Ÿ’ช #{n}/1000 โ€” endurance test',
61
+ '๐ŸŽฏ #{n} โ€” precision timing',
62
+ '๐ŸŒŠ Wave #{n} โ€” steady flow',
63
+ 'โฑ๏ธ #{n} โ€” clock is ticking',
64
+ '๐Ÿš€ #{n}/1000 โ€” full throttle safe mode',
65
+ '๐Ÿ“ˆ #{n} โ€” stats looking good',
66
+ '๐Ÿ”’ #{n} โ€” protected by baileys-antiban',
67
+ '๐Ÿ #{n} โ€” race to 1000',
68
+ ];
69
+
70
+ console.log('๐ŸŽ๏ธ baileys-antiban STRESS TEST');
71
+ console.log('='.repeat(60));
72
+ console.log(`Target: ${GROUP_JID}`);
73
+ console.log(`Messages: ${TARGET}`);
74
+ console.log(`Config: 12/min, 500/hr, 800-3000ms delay`);
75
+ console.log(`Estimated time: ${Math.round(TARGET * 2 / 60)} - ${Math.round(TARGET * 4 / 60)} minutes`);
76
+ console.log('='.repeat(60));
77
+
78
+ async function main() {
79
+ const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
80
+
81
+ const sock = makeWASocket({
82
+ auth: state,
83
+ printQRInTerminal: false,
84
+ logger: pino({ level: 'silent' }),
85
+ });
86
+
87
+ sock.ev.on('creds.update', saveCreds);
88
+
89
+ // Monitor disconnects
90
+ sock.ev.on('connection.update', async (update) => {
91
+ if (update.connection === 'close') {
92
+ const reason = (update.lastDisconnect?.error as any)?.output?.statusCode;
93
+ antiban.onDisconnect(reason || 'unknown');
94
+ console.log(`\nโŒ DISCONNECTED: ${DisconnectReason[reason] || reason}`);
95
+
96
+ if (reason === 403) {
97
+ console.log('๐Ÿ”ด FORBIDDEN โ€” WhatsApp is blocking us. Test stopped.');
98
+ process.exit(1);
99
+ }
100
+ if (reason === 401) {
101
+ console.log('๐Ÿ”ด LOGGED OUT โ€” Possible ban. Test stopped.');
102
+ process.exit(1);
103
+ }
104
+ }
105
+ });
106
+
107
+ return new Promise<void>((resolve) => {
108
+ sock.ev.on('connection.update', async (update) => {
109
+ if (update.connection !== 'open') return;
110
+
111
+ console.log('\nโœ… Connected! Starting stress test...\n');
112
+ antiban.onReconnect();
113
+
114
+ let sent = 0;
115
+ let blocked = 0;
116
+ let errors = 0;
117
+ const startTime = Date.now();
118
+ const milestones = new Set([10, 50, 100, 250, 500, 750, 1000]);
119
+
120
+ for (let i = 1; i <= TARGET; i++) {
121
+ const template = templates[(i - 1) % templates.length];
122
+ const baseMsg = template.replace(/#{n}/g, String(i));
123
+ const message = variator.vary(baseMsg);
124
+
125
+ const decision = await antiban.beforeSend(GROUP_JID, message);
126
+
127
+ if (!decision.allowed) {
128
+ blocked++;
129
+ // Wait a bit and retry once
130
+ await new Promise(r => setTimeout(r, 5000));
131
+ const retry = await antiban.beforeSend(GROUP_JID, message);
132
+ if (!retry.allowed) {
133
+ if (i % 50 === 0) console.log(`โ›” ${i}: Still blocked โ€” ${retry.reason}`);
134
+ continue;
135
+ }
136
+ await new Promise(r => setTimeout(r, retry.delayMs));
137
+ } else {
138
+ await new Promise(r => setTimeout(r, decision.delayMs));
139
+ }
140
+
141
+ try {
142
+ await sock.sendMessage(GROUP_JID, { text: message });
143
+ antiban.afterSend(GROUP_JID, message);
144
+ sent++;
145
+
146
+ // Progress indicator
147
+ if (sent % 10 === 0) {
148
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
149
+ const rate = (sent / (parseInt(elapsed) || 1) * 60).toFixed(1);
150
+ const eta = Math.round((TARGET - sent) / (parseFloat(rate) / 60));
151
+ process.stdout.write(`\r๐Ÿ“Š ${sent}/${TARGET} sent | ${blocked} blocked | ${rate}/min | ${elapsed}s elapsed | ETA: ${eta}s `);
152
+ }
153
+
154
+ if (milestones.has(sent)) {
155
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
156
+ const stats = antiban.getStats();
157
+ console.log(`\n\n๐Ÿ† MILESTONE: ${sent} messages sent!`);
158
+ console.log(` Time: ${elapsed}s | Health: ${stats.health.risk} (score: ${stats.health.score})`);
159
+ console.log(` Rate: ${stats.rateLimiter.lastMinute}/min, ${stats.rateLimiter.lastHour}/hr`);
160
+ console.log(` Avg delay: ${(stats.totalDelayMs / sent / 1000).toFixed(1)}s per message\n`);
161
+ }
162
+ } catch (err: any) {
163
+ errors++;
164
+ antiban.afterSendFailed(err.message);
165
+ if (errors > 10) {
166
+ console.log(`\n\n๐Ÿ”ด Too many errors (${errors}). Stopping.`);
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
173
+ const finalStats = antiban.getStats();
174
+
175
+ console.log(`\n\n${'='.repeat(60)}`);
176
+ console.log(`๐Ÿ STRESS TEST COMPLETE`);
177
+ console.log(`${'='.repeat(60)}`);
178
+ console.log(`โœ… Sent: ${sent}/${TARGET}`);
179
+ console.log(`โ›” Blocked: ${blocked}`);
180
+ console.log(`โŒ Errors: ${errors}`);
181
+ console.log(`โฑ๏ธ Total time: ${totalTime}s (${(parseFloat(totalTime) / 60).toFixed(1)} min)`);
182
+ console.log(`๐Ÿ“ˆ Average rate: ${(sent / parseFloat(totalTime) * 60).toFixed(1)} msgs/min`);
183
+ console.log(`โณ Average delay: ${(finalStats.totalDelayMs / sent / 1000).toFixed(1)}s per message`);
184
+ console.log(`๐Ÿฅ Final health: ${finalStats.health.risk} (score: ${finalStats.health.score})`);
185
+ console.log(`${sent === TARGET ? '๐ŸŽ‰ ALL MESSAGES DELIVERED โ€” NUMBER IS SAFE!' : 'โš ๏ธ Did not complete all messages'}`);
186
+ console.log(`${'='.repeat(60)}\n`);
187
+
188
+ setTimeout(() => {
189
+ sock.end(undefined);
190
+ resolve();
191
+ }, 2000);
192
+ });
193
+ });
194
+ }
195
+
196
+ main().then(() => process.exit(0)).catch(err => {
197
+ console.error('Fatal:', err);
198
+ process.exit(1);
199
+ });