@prototypevip/baileys 0.0.8 → 0.0.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 (54) hide show
  1. package/lib/AntiBan/antiban.js +922 -0
  2. package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
  3. package/lib/AntiBan/cli.js +177 -0
  4. package/lib/AntiBan/contactGraph.js +236 -0
  5. package/lib/AntiBan/contentVariator.js +150 -0
  6. package/lib/AntiBan/credsSnapshot.js +121 -0
  7. package/lib/AntiBan/deliveryTracker.js +107 -0
  8. package/lib/AntiBan/deviceFingerprint.js +106 -0
  9. package/lib/AntiBan/fleetEventStore.js +157 -0
  10. package/lib/AntiBan/groupOperationGuard.js +116 -0
  11. package/lib/AntiBan/health.js +207 -0
  12. package/lib/AntiBan/humanEntropy.js +337 -0
  13. package/lib/AntiBan/index.js +77 -0
  14. package/lib/AntiBan/instanceCoordinator.js +175 -0
  15. package/lib/AntiBan/jidCanonicalizer.js +256 -0
  16. package/lib/AntiBan/jidCircuitBreaker.js +186 -0
  17. package/lib/AntiBan/legitimacySignalInjector.js +231 -0
  18. package/lib/AntiBan/lidFirstResolver.js +174 -0
  19. package/lib/AntiBan/lidResolver.js +324 -0
  20. package/lib/AntiBan/messageQueue.js +187 -0
  21. package/lib/AntiBan/messageRecovery.js +283 -0
  22. package/lib/AntiBan/messageTypeRegistry.js +369 -0
  23. package/lib/AntiBan/observability.js +145 -0
  24. package/lib/AntiBan/patch.js +302 -0
  25. package/lib/AntiBan/persist.js +87 -0
  26. package/lib/AntiBan/presenceChoreographer.js +430 -0
  27. package/lib/AntiBan/presets.js +105 -0
  28. package/lib/AntiBan/profiles.js +31 -0
  29. package/lib/AntiBan/proxyRotator.js +306 -0
  30. package/lib/AntiBan/rateLimiter.js +261 -0
  31. package/lib/AntiBan/readReceiptVariance.js +88 -0
  32. package/lib/AntiBan/reconnectThrottle.js +180 -0
  33. package/lib/AntiBan/replyRatio.js +161 -0
  34. package/lib/AntiBan/reputationVoucher.js +373 -0
  35. package/lib/AntiBan/retryReason.js +91 -0
  36. package/lib/AntiBan/retryTracker.js +172 -0
  37. package/lib/AntiBan/scheduler.js +111 -0
  38. package/lib/AntiBan/sessionFingerprint.js +248 -0
  39. package/lib/AntiBan/sessionStability.js +330 -0
  40. package/lib/AntiBan/stateAdapter.js +73 -0
  41. package/lib/AntiBan/stateExport.js +152 -0
  42. package/lib/AntiBan/stealthConnect.js +130 -0
  43. package/lib/AntiBan/timelockGuard.js +181 -0
  44. package/lib/AntiBan/topologyThrottler.js +339 -0
  45. package/lib/AntiBan/warmup.js +140 -0
  46. package/lib/AntiBan/webhooks.js +80 -0
  47. package/lib/AntiBan/wrapper.js +456 -0
  48. package/lib/Socket/groups.js +26 -21
  49. package/lib/Socket/index.js +139 -7
  50. package/lib/Socket/messages-send.js +68 -5
  51. package/lib/Utils/ai-rich-formatter.js +256 -0
  52. package/lib/Utils/index.js +1 -0
  53. package/lib/index.js +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Observability — Prometheus metrics export + pluggable structured logging
3
+ *
4
+ * Usage:
5
+ * // Logging
6
+ * import { createConsoleLogger } from 'baileys-antiban';
7
+ * const logger = createConsoleLogger('[my-bot]');
8
+ * logger.info('Message sent', { recipient: jid });
9
+ *
10
+ * // Metrics export for Express
11
+ * import { createMetricsHandler } from 'baileys-antiban';
12
+ * const metricsHandler = createMetricsHandler(() => antiban.getStats());
13
+ * app.get('/metrics', metricsHandler.handle);
14
+ *
15
+ * // Periodic push to external system
16
+ * import { createPeriodicExporter } from 'baileys-antiban';
17
+ * const exporter = createPeriodicExporter(() => antiban.getStats(), {
18
+ * intervalMs: 30_000,
19
+ * onMetrics: (text) => pushToVictoriaMetrics(text),
20
+ * });
21
+ */
22
+ /**
23
+ * Create a simple console logger with timestamps and prefix
24
+ */
25
+ export function createConsoleLogger(prefix = '[baileys-antiban]') {
26
+ const format = (level, msg, meta) => {
27
+ const timestamp = new Date().toISOString();
28
+ const metaStr = meta ? ' ' + JSON.stringify(meta) : '';
29
+ return `${timestamp} ${level} ${prefix} ${msg}${metaStr}`;
30
+ };
31
+ return {
32
+ debug: (msg, meta) => console.log(format('DEBUG', msg, meta)),
33
+ info: (msg, meta) => console.log(format('INFO', msg, meta)),
34
+ warn: (msg, meta) => console.warn(format('WARN', msg, meta)),
35
+ error: (msg, meta) => console.error(format('ERROR', msg, meta)),
36
+ };
37
+ }
38
+ /**
39
+ * Export AntiBan stats as Prometheus text format (exposition format v0.0.4)
40
+ */
41
+ export function exportPrometheusMetrics(stats, labels) {
42
+ const labelMap = { instance: 'default', ...labels };
43
+ const labelStr = Object.entries(labelMap)
44
+ .map(([k, v]) => `${k}="${v}"`)
45
+ .join(',');
46
+ const lines = [];
47
+ const addMetric = (name, type, help, value) => {
48
+ lines.push(`# HELP ${name} ${help}`);
49
+ lines.push(`# TYPE ${name} ${type}`);
50
+ lines.push(`${name}{${labelStr}} ${value}`);
51
+ };
52
+ // Counters
53
+ addMetric('antiban_messages_allowed_total', 'counter', 'Total messages allowed by AntiBan', stats.messagesAllowed);
54
+ addMetric('antiban_messages_blocked_total', 'counter', 'Total messages blocked by AntiBan', stats.messagesBlocked);
55
+ addMetric('antiban_total_delay_ms_total', 'counter', 'Total delay imposed (milliseconds)', stats.totalDelayMs);
56
+ // Health gauges
57
+ const riskMap = { low: 0, medium: 1, high: 2, critical: 3 };
58
+ addMetric('antiban_health_score', 'gauge', 'Current health score (0=best, 100=worst)', stats.health.score);
59
+ addMetric('antiban_health_risk', 'gauge', 'Risk level encoded as number (0=low, 1=medium, 2=high, 3=critical)', riskMap[stats.health.risk] ?? 0);
60
+ addMetric('antiban_disconnects_last_hour', 'gauge', 'Disconnects in last hour', stats.health.stats.disconnectsLastHour);
61
+ addMetric('antiban_failed_messages_last_hour', 'gauge', 'Failed messages in last hour', stats.health.stats.failedMessagesLastHour);
62
+ addMetric('antiban_uptime_ms', 'gauge', 'Session uptime in milliseconds', stats.health.stats.uptimeMs);
63
+ // Warm-up gauges
64
+ addMetric('antiban_warmup_progress', 'gauge', 'Warm-up progress (0.0 to 1.0)', stats.warmUp.progress);
65
+ addMetric('antiban_warmup_day', 'gauge', 'Current warm-up day', stats.warmUp.day);
66
+ addMetric('antiban_warmup_today_sent', 'gauge', 'Messages sent today during warm-up', stats.warmUp.todaySent);
67
+ addMetric('antiban_warmup_today_limit', 'gauge', 'Message limit for today during warm-up', stats.warmUp.todayLimit);
68
+ // Rate limiter gauges
69
+ addMetric('antiban_rate_last_minute', 'gauge', 'Messages sent in last minute', stats.rateLimiter.lastMinute);
70
+ addMetric('antiban_rate_last_hour', 'gauge', 'Messages sent in last hour', stats.rateLimiter.lastHour);
71
+ addMetric('antiban_rate_last_day', 'gauge', 'Messages sent in last day', stats.rateLimiter.lastDay);
72
+ addMetric('antiban_rate_limit_per_minute', 'gauge', 'Rate limit per minute', stats.rateLimiter.limits.perMinute);
73
+ addMetric('antiban_rate_limit_per_hour', 'gauge', 'Rate limit per hour', stats.rateLimiter.limits.perHour);
74
+ addMetric('antiban_rate_limit_per_day', 'gauge', 'Rate limit per day', stats.rateLimiter.limits.perDay);
75
+ addMetric('antiban_known_chats', 'gauge', 'Number of known chats', stats.rateLimiter.knownChats);
76
+ // Reply ratio gauges (optional)
77
+ if (stats.replyRatio) {
78
+ addMetric('antiban_contacts_on_cooldown', 'gauge', 'Contacts currently on reply cooldown', stats.replyRatio.contactsOnCooldown);
79
+ }
80
+ else {
81
+ addMetric('antiban_contacts_on_cooldown', 'gauge', 'Contacts currently on reply cooldown', 0);
82
+ }
83
+ // Contact graph gauges (optional)
84
+ if (stats.contactGraph) {
85
+ addMetric('antiban_known_contacts', 'gauge', 'Number of known contacts', stats.contactGraph.knownContacts);
86
+ addMetric('antiban_pending_handshakes', 'gauge', 'Pending handshakes', stats.contactGraph.pendingHandshakes);
87
+ }
88
+ else {
89
+ addMetric('antiban_known_contacts', 'gauge', 'Number of known contacts', 0);
90
+ addMetric('antiban_pending_handshakes', 'gauge', 'Pending handshakes', 0);
91
+ }
92
+ // Retry tracker gauges (optional)
93
+ if (stats.retryTracker) {
94
+ addMetric('antiban_retry_spirals', 'gauge', 'Number of retry spirals detected', stats.retryTracker.spiralsDetected);
95
+ addMetric('antiban_active_retries', 'gauge', 'Currently active retries', stats.retryTracker.activeRetries);
96
+ }
97
+ else {
98
+ addMetric('antiban_retry_spirals', 'gauge', 'Number of retry spirals detected', 0);
99
+ addMetric('antiban_active_retries', 'gauge', 'Currently active retries', 0);
100
+ }
101
+ // Reconnect throttle gauges (optional)
102
+ if (stats.reconnectThrottle) {
103
+ addMetric('antiban_reconnect_throttled', 'gauge', 'Whether reconnect is currently throttled (0=no, 1=yes)', stats.reconnectThrottle.isThrottled ? 1 : 0);
104
+ addMetric('antiban_lifetime_reconnects', 'gauge', 'Total reconnects in session lifetime', stats.reconnectThrottle.lifetimeReconnects);
105
+ }
106
+ else {
107
+ addMetric('antiban_reconnect_throttled', 'gauge', 'Whether reconnect is currently throttled (0=no, 1=yes)', 0);
108
+ addMetric('antiban_lifetime_reconnects', 'gauge', 'Total reconnects in session lifetime', 0);
109
+ }
110
+ return lines.join('\n') + '\n';
111
+ }
112
+ /**
113
+ * Create an HTTP handler for Prometheus metrics (Express/Fastify compatible)
114
+ */
115
+ export function createMetricsHandler(getStats, labels) {
116
+ return {
117
+ /**
118
+ * HTTP request handler (Express/Fastify compatible)
119
+ */
120
+ handle: (_req, res) => {
121
+ const metrics = exportPrometheusMetrics(getStats(), labels);
122
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
123
+ res.end(metrics);
124
+ },
125
+ /**
126
+ * Get metrics as plain text (for non-HTTP usage)
127
+ */
128
+ text: () => {
129
+ return exportPrometheusMetrics(getStats(), labels);
130
+ },
131
+ };
132
+ }
133
+ /**
134
+ * Create a periodic metrics exporter that calls onMetrics on an interval
135
+ */
136
+ export function createPeriodicExporter(getStats, config) {
137
+ const intervalMs = config.intervalMs ?? 30_000;
138
+ const intervalId = setInterval(() => {
139
+ const metrics = exportPrometheusMetrics(getStats(), config.labels);
140
+ config.onMetrics(metrics);
141
+ }, intervalMs);
142
+ return {
143
+ stop: () => clearInterval(intervalId),
144
+ };
145
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * baileys-antiban patch command
3
+ *
4
+ * Patches an installed Baileys package to wrap makeWASocket with antiban middleware.
5
+ * Designed for frameworks (OpenClaw, etc.) that create the socket internally and
6
+ * don't expose a socket:ready hook.
7
+ *
8
+ * Idempotent: safe to re-run after plugin updates. Keeps a .antiban-backup file.
9
+ *
10
+ * Usage:
11
+ * npx baileys-antiban patch [--path <baileys-dir>] [--dry-run] [--preset conservative|moderate|aggressive]
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ const PATCH_MARKER_START = '// [BAILEYS-ANTIBAN-PATCH-START]';
16
+ const PATCH_MARKER_END = '// [BAILEYS-ANTIBAN-PATCH-END]';
17
+ function findBaileysDir(candidates) {
18
+ for (const candidate of candidates) {
19
+ if (!candidate)
20
+ continue;
21
+ const pkgPath = path.join(candidate, 'package.json');
22
+ if (fs.existsSync(pkgPath)) {
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
25
+ if (pkg.name === 'baileys' || pkg.name === '@whiskeysockets/baileys' || pkg.name === '@oxidezap/baileyrs') {
26
+ return path.resolve(candidate);
27
+ }
28
+ }
29
+ catch {
30
+ // not a valid package.json, skip
31
+ }
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ function findMakeWASocketFile(baileyDir) {
37
+ // Ordered by likelihood of containing the actual makeWASocket definition
38
+ const candidates = [
39
+ 'lib/socket/index.js',
40
+ 'lib/index.js',
41
+ 'lib/socket.js',
42
+ 'src/socket/index.js',
43
+ 'dist/socket/index.js',
44
+ 'dist/index.js',
45
+ ];
46
+ for (const rel of candidates) {
47
+ const full = path.join(baileyDir, rel);
48
+ if (!fs.existsSync(full))
49
+ continue;
50
+ const source = fs.readFileSync(full, 'utf-8');
51
+ // Look for makeWASocket being defined (not just re-exported via export *)
52
+ if (source.includes('function makeWASocket') ||
53
+ source.includes('const makeWASocket') ||
54
+ source.includes('exports.makeWASocket') ||
55
+ source.includes('makeWASocket =')) {
56
+ const isEsm = source.includes('export function makeWASocket') ||
57
+ source.includes('export const makeWASocket') ||
58
+ source.includes('export {') ||
59
+ source.includes('export default');
60
+ return { file: full, format: isEsm ? 'esm' : 'cjs' };
61
+ }
62
+ }
63
+ // Fallback: search lib/ directory for the defining file
64
+ const libDir = path.join(baileyDir, 'lib');
65
+ if (fs.existsSync(libDir)) {
66
+ const jsFiles = fs
67
+ .readdirSync(libDir, { recursive: true })
68
+ .filter((f) => typeof f === 'string' && f.endsWith('.js'))
69
+ .map((f) => path.join(libDir, f));
70
+ for (const full of jsFiles) {
71
+ try {
72
+ const source = fs.readFileSync(full, 'utf-8');
73
+ if (source.includes('function makeWASocket') && source.includes('makeWASocket')) {
74
+ const isEsm = source.includes('export');
75
+ return { file: full, format: isEsm ? 'esm' : 'cjs' };
76
+ }
77
+ }
78
+ catch {
79
+ // skip unreadable files
80
+ }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ function buildCjsPatch(opts) {
86
+ return `
87
+ ${PATCH_MARKER_START}
88
+ // Auto-generated by baileys-antiban patch. Re-run after Baileys/plugin updates.
89
+ (function() {
90
+ try {
91
+ const { wrapSocket } = require('baileys-antiban');
92
+ const _orig = module.exports.makeWASocket;
93
+ if (typeof _orig === 'function') {
94
+ module.exports.makeWASocket = function(...args) {
95
+ return wrapSocket(_orig.apply(this, args), {
96
+ preset: process.env.ANTIBAN_PRESET || '${opts.preset}',
97
+ delayBetweenMessages: {
98
+ min: parseInt(process.env.ANTIBAN_MIN_DELAY || '${opts.minDelay}'),
99
+ max: parseInt(process.env.ANTIBAN_MAX_DELAY || '${opts.maxDelay}'),
100
+ },
101
+ typingIndicator: (process.env.ANTIBAN_TYPING || '${opts.typing}') !== 'false',
102
+ });
103
+ };
104
+ }
105
+ } catch(e) {
106
+ console.warn('[baileys-antiban] patch inactive:', e.message);
107
+ }
108
+ })();
109
+ ${PATCH_MARKER_END}
110
+ `;
111
+ }
112
+ function buildEsmPatch(source, opts) {
113
+ // Strategy A: function declaration — rename and re-export wrapped version
114
+ if (source.includes('export function makeWASocket')) {
115
+ const patched = source.replace(/export function makeWASocket\b/, 'function _antiban_makeWASocket_orig');
116
+ return (patched +
117
+ `
118
+ ${PATCH_MARKER_START}
119
+ // Auto-generated by baileys-antiban patch. Re-run after Baileys/plugin updates.
120
+ import { wrapSocket as _antibanWrap } from 'baileys-antiban';
121
+ export const makeWASocket = (...args) => _antibanWrap(_antiban_makeWASocket_orig(...args), {
122
+ preset: process.env.ANTIBAN_PRESET || '${opts.preset}',
123
+ delayBetweenMessages: {
124
+ min: parseInt(process.env.ANTIBAN_MIN_DELAY || '${opts.minDelay}'),
125
+ max: parseInt(process.env.ANTIBAN_MAX_DELAY || '${opts.maxDelay}'),
126
+ },
127
+ typingIndicator: (process.env.ANTIBAN_TYPING || '${opts.typing}') !== 'false',
128
+ });
129
+ ${PATCH_MARKER_END}
130
+ `);
131
+ }
132
+ // Strategy B: const/let declaration — rename and re-export
133
+ if (source.match(/export\s+(const|let)\s+makeWASocket\s*=/)) {
134
+ const patched = source.replace(/export\s+(const|let)\s+makeWASocket\s*=/, 'const _antiban_makeWASocket_orig =');
135
+ return (patched +
136
+ `
137
+ ${PATCH_MARKER_START}
138
+ // Auto-generated by baileys-antiban patch. Re-run after Baileys/plugin updates.
139
+ import { wrapSocket as _antibanWrap } from 'baileys-antiban';
140
+ export const makeWASocket = (...args) => _antibanWrap(_antiban_makeWASocket_orig(...args), {
141
+ preset: process.env.ANTIBAN_PRESET || '${opts.preset}',
142
+ delayBetweenMessages: {
143
+ min: parseInt(process.env.ANTIBAN_MIN_DELAY || '${opts.minDelay}'),
144
+ max: parseInt(process.env.ANTIBAN_MAX_DELAY || '${opts.maxDelay}'),
145
+ },
146
+ typingIndicator: (process.env.ANTIBAN_TYPING || '${opts.typing}') !== 'false',
147
+ });
148
+ ${PATCH_MARKER_END}
149
+ `);
150
+ }
151
+ // Strategy C: named re-export — intercept at this boundary
152
+ if (source.includes('makeWASocket')) {
153
+ // Append import + re-export override at end of file
154
+ return (source +
155
+ `
156
+ ${PATCH_MARKER_START}
157
+ // Auto-generated by baileys-antiban patch. Re-run after Baileys/plugin updates.
158
+ // NOTE: Strategy C (named export intercept) — less precise than A/B.
159
+ // If this breaks, use --path to target the file where makeWASocket is defined.
160
+ import { wrapSocket as _antibanWrap } from 'baileys-antiban';
161
+ const _antiban_raw = makeWASocket;
162
+ export { makeWASocket }; // shadowed below if engine supports block-level export rebind
163
+ // Fallback: log advisory if strategy C can't reassign
164
+ if (typeof makeWASocket === 'function') {
165
+ // eslint-disable-next-line no-global-assign
166
+ try { makeWASocket = (...a) => _antibanWrap(_antiban_raw(...a), { preset: '${opts.preset}', delayBetweenMessages: { min: ${opts.minDelay}, max: ${opts.maxDelay} }, typingIndicator: ${opts.typing} }); } catch {}
167
+ }
168
+ ${PATCH_MARKER_END}
169
+ `);
170
+ }
171
+ throw new Error('Could not locate makeWASocket in this file. Try --path to a different Baileys file.');
172
+ }
173
+ export function applyPatch(opts) {
174
+ const { preset = 'conservative', minDelay = 1500, maxDelay = 4000, typingIndicator = true, dryRun = false, } = opts;
175
+ // 1. Locate Baileys
176
+ const defaultSearchPaths = [
177
+ // OC-style nested location
178
+ './node_modules/@openclaw/whatsapp/node_modules/baileys',
179
+ './node_modules/@openclaw/whatsapp/node_modules/@whiskeysockets/baileys',
180
+ // Standard locations
181
+ './node_modules/baileys',
182
+ './node_modules/@whiskeysockets/baileys',
183
+ // Walk up
184
+ '../node_modules/baileys',
185
+ '../../node_modules/baileys',
186
+ ];
187
+ const searchPaths = opts.baileysPaths?.length ? opts.baileysPaths : defaultSearchPaths;
188
+ const baileyDir = findBaileysDir(searchPaths);
189
+ if (!baileyDir) {
190
+ return {
191
+ success: false,
192
+ baileyDir: '(not found)',
193
+ targetFile: '(not found)',
194
+ alreadyPatched: false,
195
+ format: 'cjs',
196
+ message: 'Could not find Baileys package. Use --path <dir> to specify its location.\n' +
197
+ 'Example: npx baileys-antiban patch --path ./node_modules/@openclaw/whatsapp/node_modules/baileys',
198
+ };
199
+ }
200
+ // 2. Find file containing makeWASocket
201
+ const found = findMakeWASocketFile(baileyDir);
202
+ if (!found) {
203
+ return {
204
+ success: false,
205
+ baileyDir,
206
+ targetFile: '(not found)',
207
+ alreadyPatched: false,
208
+ format: 'cjs',
209
+ message: `Found Baileys at ${baileyDir} but could not locate makeWASocket definition.\n` +
210
+ 'The package structure may have changed. Please open an issue.',
211
+ };
212
+ }
213
+ const { file: targetFile, format } = found;
214
+ const source = fs.readFileSync(targetFile, 'utf-8');
215
+ // 3. Check idempotency
216
+ if (source.includes(PATCH_MARKER_START)) {
217
+ return {
218
+ success: true,
219
+ baileyDir,
220
+ targetFile,
221
+ alreadyPatched: true,
222
+ format,
223
+ message: `Already patched: ${targetFile}\nRe-run with --force to re-apply.`,
224
+ };
225
+ }
226
+ if (dryRun) {
227
+ return {
228
+ success: true,
229
+ baileyDir,
230
+ targetFile,
231
+ alreadyPatched: false,
232
+ format,
233
+ message: `[dry-run] Would patch ${targetFile} (format: ${format})`,
234
+ };
235
+ }
236
+ // 4. Backup
237
+ const backupPath = targetFile + '.antiban-backup';
238
+ if (!fs.existsSync(backupPath)) {
239
+ fs.copyFileSync(targetFile, backupPath);
240
+ }
241
+ // 5. Build and apply patch
242
+ const patchOpts = { preset, minDelay, maxDelay, typing: typingIndicator };
243
+ let patched;
244
+ try {
245
+ if (format === 'cjs') {
246
+ patched = source + buildCjsPatch(patchOpts);
247
+ }
248
+ else {
249
+ patched = buildEsmPatch(source, patchOpts);
250
+ }
251
+ }
252
+ catch (e) {
253
+ return {
254
+ success: false,
255
+ baileyDir,
256
+ targetFile,
257
+ alreadyPatched: false,
258
+ format,
259
+ message: `Patch generation failed: ${e.message}`,
260
+ };
261
+ }
262
+ fs.writeFileSync(targetFile, patched, 'utf-8');
263
+ return {
264
+ success: true,
265
+ baileyDir,
266
+ targetFile,
267
+ alreadyPatched: false,
268
+ format,
269
+ backupPath,
270
+ message: `✅ Patched: ${targetFile}\n` +
271
+ ` Format: ${format}\n` +
272
+ ` Backup: ${backupPath}\n` +
273
+ ` Preset: ${preset} (${minDelay}–${maxDelay}ms delays, typing=${typingIndicator})\n` +
274
+ `\nTo auto-re-patch after plugin updates, add to your package.json:\n` +
275
+ ` "scripts": { "postinstall": "npx baileys-antiban patch" }\n\n` +
276
+ `Env overrides: ANTIBAN_PRESET, ANTIBAN_MIN_DELAY, ANTIBAN_MAX_DELAY, ANTIBAN_TYPING`,
277
+ };
278
+ }
279
+ export function unpatchFile(targetFile) {
280
+ if (!fs.existsSync(targetFile)) {
281
+ return { success: false, message: `File not found: ${targetFile}` };
282
+ }
283
+ const backupPath = targetFile + '.antiban-backup';
284
+ if (!fs.existsSync(backupPath)) {
285
+ // Try removing the patch block from source
286
+ const source = fs.readFileSync(targetFile, 'utf-8');
287
+ if (!source.includes(PATCH_MARKER_START)) {
288
+ return { success: true, message: 'File is not patched.' };
289
+ }
290
+ const startIdx = source.indexOf(PATCH_MARKER_START);
291
+ const endIdx = source.indexOf(PATCH_MARKER_END);
292
+ if (startIdx === -1 || endIdx === -1) {
293
+ return { success: false, message: 'Patch markers found but malformed. Restore manually from backup.' };
294
+ }
295
+ const cleaned = source.slice(0, startIdx) + source.slice(endIdx + PATCH_MARKER_END.length);
296
+ fs.writeFileSync(targetFile, cleaned, 'utf-8');
297
+ return { success: true, message: `Patch removed from ${targetFile}` };
298
+ }
299
+ fs.copyFileSync(backupPath, targetFile);
300
+ fs.unlinkSync(backupPath);
301
+ return { success: true, message: `Restored from backup: ${backupPath}` };
302
+ }
@@ -0,0 +1,87 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ const KNOWN_CHATS_MAX = 1000;
4
+ const DEBOUNCE_MS = 5000;
5
+ /**
6
+ * Manages persisted state for a single baileys-antiban instance.
7
+ *
8
+ * **Single-writer assumption:** No file lock is used. Two processes sharing
9
+ * the same state file will race on concurrent writes. Use separate state
10
+ * files per process to avoid data corruption.
11
+ */
12
+ export class StateManager {
13
+ path;
14
+ debounceTimer = null;
15
+ constructor(filePath) {
16
+ // Resolve to absolute path and reject null bytes to prevent path injection
17
+ if (filePath.includes('\0'))
18
+ throw new Error('[baileys-antiban] Invalid state file path: null byte');
19
+ this.path = path.resolve(filePath);
20
+ }
21
+ load() {
22
+ try {
23
+ const raw = fs.readFileSync(this.path, 'utf-8');
24
+ const parsed = JSON.parse(raw);
25
+ // Strict shape validation before trusting file content
26
+ if (typeof parsed !== 'object' || parsed === null ||
27
+ parsed.version !== 3 ||
28
+ typeof parsed.savedAt !== 'number' ||
29
+ !Array.isArray(parsed.knownChats)) {
30
+ console.warn('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh');
31
+ return null;
32
+ }
33
+ return parsed;
34
+ }
35
+ catch {
36
+ // Missing file = silent null. Corrupt JSON = warn.
37
+ if (fs.existsSync(this.path)) {
38
+ console.warn('[baileys-antiban] WARN: corrupt state file, starting fresh');
39
+ }
40
+ return null;
41
+ }
42
+ }
43
+ /** Debounced save — called after every send (5s delay) */
44
+ saveDebounced(state) {
45
+ if (this.debounceTimer) {
46
+ clearTimeout(this.debounceTimer);
47
+ }
48
+ this.debounceTimer = setTimeout(() => {
49
+ this.writeFile(state);
50
+ this.debounceTimer = null;
51
+ }, DEBOUNCE_MS);
52
+ }
53
+ /** Immediate save — called after health events (ban/restriction) */
54
+ saveImmediate(state) {
55
+ if (this.debounceTimer) {
56
+ clearTimeout(this.debounceTimer);
57
+ this.debounceTimer = null;
58
+ }
59
+ this.writeFile(state);
60
+ }
61
+ /** Flush/cancel pending debounced write (for tests and process exit) */
62
+ flush() {
63
+ if (this.debounceTimer) {
64
+ clearTimeout(this.debounceTimer);
65
+ this.debounceTimer = null;
66
+ }
67
+ }
68
+ destroy() {
69
+ this.flush();
70
+ }
71
+ writeFile(state) {
72
+ const toSave = {
73
+ ...state,
74
+ savedAt: Date.now(),
75
+ // LRU eviction: keep last KNOWN_CHATS_MAX entries
76
+ knownChats: state.knownChats.length > KNOWN_CHATS_MAX
77
+ ? state.knownChats.slice(-KNOWN_CHATS_MAX)
78
+ : state.knownChats,
79
+ };
80
+ try {
81
+ fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8');
82
+ }
83
+ catch (err) {
84
+ console.warn(`[baileys-antiban] WARN: failed to write state to ${this.path}:`, err);
85
+ }
86
+ }
87
+ }