aunubaileys 1.0.10 → 1.0.12
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.
- package/README.md +8 -0
- package/lib/Defaults/index.d.ts +1 -1
- package/lib/Socket/socket.js +64 -0
- package/lib/Utils/antiban.d.ts +21 -0
- package/lib/Utils/antiban.js +170 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -64,6 +64,14 @@ Everything else is pure upstream. When upstream moves, we rebase the patch layer
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
### 🆕 What's new in 1.0.12 / Baru di 1.0.12
|
|
68
|
+
|
|
69
|
+
- 🔑 **Pairing waits for the socket — and explains itself when it can't.** Field report from a real VPS deploy: `requestPairingCode` threw a bare Boom 428 "Connection Closed" every time, leaving consumers unable to tell an unready socket from a server rejection. Now `requestPairingCode` **waits internally (up to 30s) for the transport to open** before sending the pairing IQ, fails fast if the connection dies while waiting, and **every pairing failure carries the last disconnect reason** (status code + message) instead of the generic text. The 1.0.10 single-flight guard is kept and covers the wait.
|
|
70
|
+
|
|
71
|
+
### 🆕 What's new in 1.0.11 / Baru di 1.0.11
|
|
72
|
+
|
|
73
|
+
- 🚑 **Hotfix for broken 1.0.9 / 1.0.10 tarballs** — the published packages exported `./Utils/antiban.js` from `lib/index.js` but the file itself was never committed to the repository, so every install crashed on import with `ERR_MODULE_NOT_FOUND`. The AntiBan module (and its types) is now in the repo, and this release's tarball was verified with a clean install + import test. Also in: RafaelPivots' `INITIAL_PREKEY_COUNT` type correction (30 → 812, matching the runtime) and repository URLs updated to the repo's new home, **AunuCore/AunuBaileys-lib**. If you are on 1.0.9 or 1.0.10, upgrade to 1.0.11.
|
|
74
|
+
|
|
67
75
|
### 🆕 What's new in 1.0.10 / Baru di 1.0.10
|
|
68
76
|
|
|
69
77
|
- 🛠️ **Reliability hardening against classic upstream weaknesses** — (1) **Retry receipts now get answered even without a custom `getMessage` store:** a built-in per-socket sent-message cache (last 256 relayed messages, whatsmeow-style) backs up your store, ending the silent "message not available" failures behind stuck unreadable/undelivered messages. Disable with `sentMessagesCacheSize: 0`. (2) **Pairing is now single-flight:** `requestPairingCode` refuses to double-fire (double requests invalidate the first code) and refuses outright on already-registered sessions, with clear errors instead of silent corruption. (3) **Handshake timeout raised:** default `connectTimeoutMs` goes 20s → 60s so pairing no longer times out on slow networks.
|
package/lib/Defaults/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare const MEDIA_HKDF_KEY_MAPPING: {
|
|
|
42
42
|
};
|
|
43
43
|
export declare const MEDIA_KEYS: MediaType[];
|
|
44
44
|
export declare const MIN_PREKEY_COUNT = 5;
|
|
45
|
-
export declare const INITIAL_PREKEY_COUNT =
|
|
45
|
+
export declare const INITIAL_PREKEY_COUNT = 812;
|
|
46
46
|
export declare const DEFAULT_CACHE_TTLS: {
|
|
47
47
|
SIGNAL_STORE: number;
|
|
48
48
|
MSG_RETRY: number;
|
package/lib/Socket/socket.js
CHANGED
|
@@ -49,6 +49,7 @@ export const makeSocket = (config) => {
|
|
|
49
49
|
let keepAliveReq;
|
|
50
50
|
let qrTimer;
|
|
51
51
|
let closed = false;
|
|
52
|
+
let lastDisconnectError;
|
|
52
53
|
const uqTagId = generateMdTagPrefix();
|
|
53
54
|
const generateMessageTag = () => `${uqTagId}${epoch++}`;
|
|
54
55
|
const sendPromise = promisify(ws.send);
|
|
@@ -255,6 +256,7 @@ export const makeSocket = (config) => {
|
|
|
255
256
|
return;
|
|
256
257
|
}
|
|
257
258
|
closed = true;
|
|
259
|
+
lastDisconnectError = error;
|
|
258
260
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
259
261
|
clearInterval(keepAliveReq);
|
|
260
262
|
clearTimeout(qrTimer);
|
|
@@ -371,6 +373,59 @@ export const makeSocket = (config) => {
|
|
|
371
373
|
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
372
374
|
};
|
|
373
375
|
let pairingCodeRequestInFlight = false;
|
|
376
|
+
const PAIRING_READY_TIMEOUT_MS = 30000;
|
|
377
|
+
const describeLastDisconnect = () => {
|
|
378
|
+
if (!lastDisconnectError) {
|
|
379
|
+
return 'no disconnect recorded yet';
|
|
380
|
+
}
|
|
381
|
+
const code = lastDisconnectError?.data?.statusCode ?? lastDisconnectError?.output?.statusCode;
|
|
382
|
+
return `${code ? `status ${code} — ` : ''}${lastDisconnectError?.message || String(lastDisconnectError)}`;
|
|
383
|
+
};
|
|
384
|
+
// AunuBaileys: pairing must not fire its IQ into a socket that is not
|
|
385
|
+
// open yet (upstream throws a bare 428 'Connection Closed'). Wait for
|
|
386
|
+
// the transport to open, fail fast when the connection dies meanwhile,
|
|
387
|
+
// and always surface WHY via the last disconnect reason.
|
|
388
|
+
const waitForPairingReady = async () => {
|
|
389
|
+
if (ws.isOpen) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (closed || ws.isClosed || ws.isClosing) {
|
|
393
|
+
throw new Boom(`requestPairingCode: connection is already closed (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed });
|
|
394
|
+
}
|
|
395
|
+
let onUpdate;
|
|
396
|
+
const closedWhileWaiting = new Promise((_, reject) => {
|
|
397
|
+
onUpdate = (update) => {
|
|
398
|
+
if (update.connection === 'close') {
|
|
399
|
+
reject(new Boom(`requestPairingCode: connection closed while waiting for the socket to open (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed }));
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
ev.on('connection.update', onUpdate);
|
|
403
|
+
});
|
|
404
|
+
let timer;
|
|
405
|
+
const timedOut = new Promise((_, reject) => {
|
|
406
|
+
timer = setTimeout(() => {
|
|
407
|
+
reject(new Boom(`requestPairingCode: socket did not open within ${PAIRING_READY_TIMEOUT_MS / 1000}s (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.timedOut }));
|
|
408
|
+
}, PAIRING_READY_TIMEOUT_MS);
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
await Promise.race([
|
|
412
|
+
waitForSocketOpen().catch(() => {
|
|
413
|
+
throw new Boom(`requestPairingCode: connection closed while waiting for the socket to open (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed });
|
|
414
|
+
}),
|
|
415
|
+
closedWhileWaiting,
|
|
416
|
+
timedOut
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
if (onUpdate) {
|
|
422
|
+
try {
|
|
423
|
+
ev.off('connection.update', onUpdate);
|
|
424
|
+
}
|
|
425
|
+
catch { }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
374
429
|
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
375
430
|
if (authState.creds.registered) {
|
|
376
431
|
throw new Error('requestPairingCode: this session is already registered — pairing codes are only for new logins');
|
|
@@ -384,6 +439,7 @@ export const makeSocket = (config) => {
|
|
|
384
439
|
}
|
|
385
440
|
pairingCodeRequestInFlight = true;
|
|
386
441
|
try {
|
|
442
|
+
await waitForPairingReady();
|
|
387
443
|
authState.creds.pairingCode = pairingCode;
|
|
388
444
|
authState.creds.me = {
|
|
389
445
|
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
@@ -438,6 +494,14 @@ export const makeSocket = (config) => {
|
|
|
438
494
|
});
|
|
439
495
|
return authState.creds.pairingCode;
|
|
440
496
|
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
const msg = err?.message || '';
|
|
499
|
+
const isClosedErr = err?.output?.statusCode === DisconnectReason.connectionClosed || /connection closed/i.test(msg);
|
|
500
|
+
if (isClosedErr && !msg.startsWith('requestPairingCode')) {
|
|
501
|
+
throw new Boom(`requestPairingCode failed: connection closed before the pairing request could be delivered (last disconnect: ${describeLastDisconnect()}). Original error: ${msg}`, { statusCode: DisconnectReason.connectionClosed });
|
|
502
|
+
}
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
441
505
|
finally {
|
|
442
506
|
pairingCodeRequestInFlight = false;
|
|
443
507
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WASocket } from '../Types';
|
|
2
|
+
export declare const gaussianJitter: (mean?: number, sigma?: number) => number;
|
|
3
|
+
export declare class RateLimiter {
|
|
4
|
+
constructor(max: number, windowMs: number);
|
|
5
|
+
acquire(key: string): Promise<number>;
|
|
6
|
+
tryAcquire(key: string): boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface DisconnectClassification { category: string; action: 'stop' | 'restart' | 'reconnect' | 'backoff'; detail: string; }
|
|
9
|
+
export declare const classifyDisconnect: (error: unknown) => DisconnectClassification;
|
|
10
|
+
export interface AunuAntiBanOptions {
|
|
11
|
+
perChatPerMinute?: number; globalPerMinute?: number; minDelayMs?: number; jitterMs?: number;
|
|
12
|
+
warmupDays?: number; warmupStart?: number; typingSimulation?: boolean;
|
|
13
|
+
groupAddMax?: number; groupAddWindowMs?: number; groupCreateMax?: number; groupCreateWindowMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class AunuAntiBan {
|
|
16
|
+
constructor(options?: AunuAntiBanOptions);
|
|
17
|
+
warmupFactor(): number;
|
|
18
|
+
effectiveLimit(base: number): number;
|
|
19
|
+
wrap<T extends WASocket>(sock: T): T;
|
|
20
|
+
}
|
|
21
|
+
export declare const wrapAntiBan: (sock: WASocket, options?: AunuAntiBanOptions) => WASocket;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AunuAntiBan — lapisan perlindungan akun bawaan AunuBaileys.
|
|
3
|
+
* Konsep terinspirasi baileys-antiban (kobie3717), diimplementasikan ulang
|
|
4
|
+
* secara bersih di dalam paket: rate limiting dengan jitter Gaussian,
|
|
5
|
+
* jadwal warmup akun baru, penjaga operasi grup, simulasi mengetik,
|
|
6
|
+
* dan klasifikasi disconnect. Tanpa telemetri, tanpa jaringan tambahan.
|
|
7
|
+
*/
|
|
8
|
+
import { Boom } from '@hapi/boom';
|
|
9
|
+
import { DisconnectReason } from '../Types/index.js';
|
|
10
|
+
import { delay } from './generics.js';
|
|
11
|
+
|
|
12
|
+
/** Jitter Gaussian (Box-Muller), hasil dibatasi >= 0. */
|
|
13
|
+
export const gaussianJitter = (mean = 0, sigma = 1) => {
|
|
14
|
+
let u = 0;
|
|
15
|
+
let v = 0;
|
|
16
|
+
while (u === 0)
|
|
17
|
+
u = Math.random();
|
|
18
|
+
while (v === 0)
|
|
19
|
+
v = Math.random();
|
|
20
|
+
const n = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
21
|
+
return Math.max(0, mean + n * sigma);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Rate limiter jendela geser per kunci. */
|
|
25
|
+
export class RateLimiter {
|
|
26
|
+
constructor(max, windowMs) {
|
|
27
|
+
this.max = max;
|
|
28
|
+
this.windowMs = windowMs;
|
|
29
|
+
this.hits = new Map();
|
|
30
|
+
}
|
|
31
|
+
/** Tunggu sampai ada slot untuk key; kembalikan waktu tunggu (ms). */
|
|
32
|
+
async acquire(key) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
let arr = this.hits.get(key);
|
|
35
|
+
if (!arr) {
|
|
36
|
+
arr = [];
|
|
37
|
+
this.hits.set(key, arr);
|
|
38
|
+
}
|
|
39
|
+
while (arr.length && now - arr[0] >= this.windowMs)
|
|
40
|
+
arr.shift();
|
|
41
|
+
if (arr.length >= this.max) {
|
|
42
|
+
const wait = this.windowMs - (now - arr[0]) + 5;
|
|
43
|
+
await delay(wait);
|
|
44
|
+
return this.acquire(key);
|
|
45
|
+
}
|
|
46
|
+
arr.push(Date.now());
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/** Cek tanpa menunggu; true bila masih boleh. */
|
|
50
|
+
tryAcquire(key) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
let arr = this.hits.get(key);
|
|
53
|
+
if (!arr) {
|
|
54
|
+
arr = [];
|
|
55
|
+
this.hits.set(key, arr);
|
|
56
|
+
}
|
|
57
|
+
while (arr.length && now - arr[0] >= this.windowMs)
|
|
58
|
+
arr.shift();
|
|
59
|
+
if (arr.length >= this.max) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
arr.push(now);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Klasifikasi error disconnect -> kategori + tindakan yang disarankan. */
|
|
68
|
+
export const classifyDisconnect = (error) => {
|
|
69
|
+
const code = error?.output?.statusCode ?? error?.statusCode ?? error;
|
|
70
|
+
switch (code) {
|
|
71
|
+
case DisconnectReason.loggedOut:
|
|
72
|
+
return { category: 'logged_out', action: 'stop', detail: 'Sesi logout — perlu pairing ulang. Jangan auto-reconnect.' };
|
|
73
|
+
case DisconnectReason.restartRequired:
|
|
74
|
+
return { category: 'restart_required', action: 'restart', detail: 'Server meminta restart — hubungkan ulang sekali.' };
|
|
75
|
+
case DisconnectReason.connectionReplaced:
|
|
76
|
+
return { category: 'replaced', action: 'stop', detail: 'Sesi digantikan koneksi lain — periksa perangkat tertaut.' };
|
|
77
|
+
case DisconnectReason.badSession:
|
|
78
|
+
return { category: 'bad_session', action: 'stop', detail: 'Sesi rusak — hapus auth state dan pairing ulang.' };
|
|
79
|
+
case DisconnectReason.multideviceMismatch:
|
|
80
|
+
return { category: 'mismatch', action: 'stop', detail: 'Ketidakcocokan multidevice — pairing ulang.' };
|
|
81
|
+
case 403:
|
|
82
|
+
return { category: 'forbidden', action: 'stop', detail: '403 — kemungkinan dibatasi/diblokir. Berhenti dan periksa akun.' };
|
|
83
|
+
case 429:
|
|
84
|
+
return { category: 'rate_limited', action: 'backoff', detail: '429 — terlalu cepat. Mundur dan turunkan laju kirim.' };
|
|
85
|
+
case DisconnectReason.timedOut:
|
|
86
|
+
case DisconnectReason.connectionLost:
|
|
87
|
+
case DisconnectReason.connectionClosed:
|
|
88
|
+
default:
|
|
89
|
+
return { category: 'transient', action: 'reconnect', detail: 'Gangguan jaringan/server sementara — aman reconnect dengan jeda.' };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export class AunuAntiBan {
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.options = {
|
|
96
|
+
perChatPerMinute: 12,
|
|
97
|
+
globalPerMinute: 60,
|
|
98
|
+
minDelayMs: 700,
|
|
99
|
+
jitterMs: 900,
|
|
100
|
+
warmupDays: 7,
|
|
101
|
+
warmupStart: Date.now(),
|
|
102
|
+
typingSimulation: true,
|
|
103
|
+
groupAddMax: 3,
|
|
104
|
+
groupAddWindowMs: 600000,
|
|
105
|
+
groupCreateMax: 2,
|
|
106
|
+
groupCreateWindowMs: 600000,
|
|
107
|
+
...options
|
|
108
|
+
};
|
|
109
|
+
const o = this.options;
|
|
110
|
+
this.perChat = new RateLimiter(o.perChatPerMinute, 60000);
|
|
111
|
+
this.global = new RateLimiter(o.globalPerMinute, 60000);
|
|
112
|
+
this.groupAdd = new RateLimiter(o.groupAddMax, o.groupAddWindowMs);
|
|
113
|
+
this.groupCreate = new RateLimiter(o.groupCreateMax, o.groupCreateWindowMs);
|
|
114
|
+
}
|
|
115
|
+
/** Faktor warmup 0.3 -> 1.0 selama warmupDays sejak warmupStart. */
|
|
116
|
+
warmupFactor() {
|
|
117
|
+
const days = (Date.now() - this.options.warmupStart) / 86400000;
|
|
118
|
+
if (days >= this.options.warmupDays) {
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
return 0.3 + 0.7 * Math.max(0, days / this.options.warmupDays);
|
|
122
|
+
}
|
|
123
|
+
/** Batasi laju efektif mengikuti warmup. */
|
|
124
|
+
effectiveLimit(base) {
|
|
125
|
+
return Math.max(1, Math.floor(base * this.warmupFactor()));
|
|
126
|
+
}
|
|
127
|
+
/** Terapkan perlindungan ke socket; socket yang sama dikembalikan. */
|
|
128
|
+
wrap(sock) {
|
|
129
|
+
const self = this;
|
|
130
|
+
const origSend = sock.sendMessage.bind(sock);
|
|
131
|
+
sock.sendMessage = async (jid, content, options) => {
|
|
132
|
+
await self.global.acquire('__global__');
|
|
133
|
+
await self.perChat.acquire(jid);
|
|
134
|
+
if (self.options.typingSimulation && (content?.text || content?.caption) && typeof sock.sendPresenceUpdate === 'function') {
|
|
135
|
+
try {
|
|
136
|
+
await sock.sendPresenceUpdate('composing', jid);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
const wait = self.options.minDelayMs * self.warmupFactor() + gaussianJitter(0, self.options.jitterMs / 2);
|
|
141
|
+
if (wait > 0) {
|
|
142
|
+
await delay(wait);
|
|
143
|
+
}
|
|
144
|
+
return origSend(jid, content, options);
|
|
145
|
+
};
|
|
146
|
+
if (typeof sock.groupParticipantsUpdate === 'function') {
|
|
147
|
+
const orig = sock.groupParticipantsUpdate.bind(sock);
|
|
148
|
+
sock.groupParticipantsUpdate = async (jid, participants, action, inviteMessage) => {
|
|
149
|
+
if (action === 'add' && !self.groupAdd.tryAcquire(jid)) {
|
|
150
|
+
throw new Boom('AunuAntiBan: batas tambah anggota grup tercapai — jeda dulu agar akun aman', { statusCode: 429 });
|
|
151
|
+
}
|
|
152
|
+
return orig(jid, participants, action, inviteMessage);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (typeof sock.groupCreate === 'function') {
|
|
156
|
+
const origCreate = sock.groupCreate.bind(sock);
|
|
157
|
+
sock.groupCreate = async (subject, participants) => {
|
|
158
|
+
if (!self.groupCreate.tryAcquire('__create__')) {
|
|
159
|
+
throw new Boom('AunuAntiBan: batas buat grup tercapai — jeda dulu agar akun aman', { statusCode: 429 });
|
|
160
|
+
}
|
|
161
|
+
return origCreate(subject, participants);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
sock.aunuAntiBan = self;
|
|
165
|
+
return sock;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Bungkus socket dengan AunuAntiBan (jalan pintas). */
|
|
170
|
+
export const wrapAntiBan = (sock, options) => new AunuAntiBan(options).wrap(sock);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aunubaileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.12",
|
|
5
5
|
"description": "AunuBaileys \u2014 a WhatsApp Web API for bots that never sleep. Full upstream power with the beloved in-memory store restored and pairing that just works. Forged by AunuCore.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"homepage": "https://docsjs.aunucore.eu.cc",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/
|
|
25
|
+
"url": "git+https://github.com/AunuCore/AunuBaileys-lib.git"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "AunuCore Creative Network",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"node": ">=20.0.0"
|
|
102
102
|
},
|
|
103
103
|
"bugs": {
|
|
104
|
-
"url": "https://github.com/
|
|
104
|
+
"url": "https://github.com/AunuCore/AunuBaileys-lib/issues"
|
|
105
105
|
},
|
|
106
106
|
"publishConfig": {
|
|
107
107
|
"access": "public"
|