@zervonex/baileys 3.0.5 → 3.0.7

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 CHANGED
@@ -350,15 +350,14 @@ Two adapters are available. They do **not** share an identical return shape —
350
350
 
351
351
  ### File-based (default)
352
352
 
353
- Atomic writes, sha256 checksum integrity, per-file mutex locking, corrupt file recovery, and legacy file upgrade. Best for single-instance bots or local development.
353
+ Atomic writes (tmp → rename with fallback), corrupt file self-healing, and automatic old pre-key cleanup. Best for single-instance bots or local development.
354
354
 
355
355
  ```javascript
356
356
  import { useMultiFileAuthState } from '@zervonex/baileys'
357
357
 
358
358
  const { state, saveCreds, getStats } = await useMultiFileAuthState('auth_session', {
359
- preKeyRetention: 150, // how many recent prekeys to keep (default: 150)
360
- cleanupThreshold: 50, // new prekeys before a cleanup runs (default: 50)
361
- logger, // optional — logs checksum mismatches and cleanup events
359
+ preKeyRetention: 150, // how many recent prekeys to keep (default: 150)
360
+ cleanupThreshold: 50, // new prekeys before a cleanup runs (default: 50)
362
361
  })
363
362
 
364
363
  const sock = makeWASocket({ auth: state })
@@ -369,15 +368,20 @@ const stats = await getStats()
369
368
  // { totalFiles: 42, preKeyCount: 150, nextPreKeyId: 3200, lastCleanupAt: '2026-07-05T...' }
370
369
  ```
371
370
 
372
- To fully log out and delete a session's files, remove the session folder directly (e.g. `fs.rm(folder, { recursive: true, force: true })`) — there is no `clearSession`/`close` method on this adapter.
371
+ To fully log out and delete a session, remove the session folder directly — there is no `clearSession` method on this adapter:
372
+
373
+ ```javascript
374
+ import { rm } from 'fs/promises'
375
+ await rm('auth_session', { recursive: true, force: true })
376
+ ```
373
377
 
374
378
  ### Keyv-based (production)
375
379
 
376
- Universal adapter for any database backend via [Keyv](https://github.com/jaredwray/keyv). Pass a connection string — the matching backend driver is resolved and imported automatically, no manual adapter imports needed.
380
+ Universal adapter for any database backend via [Keyv](https://www.npmjs.com/package/keyv). Pass a connection string — the matching backend driver is resolved and cached automatically, no manual adapter imports needed.
377
381
 
378
- Storage format matches `useMultiFileAuthState` exactly: every value is wrapped as `{ data, __checksum }` with a sha256 integrity check, so corruption is detected and self-healed the same way on both adapters. Reads also auto-detect and repair values that were written by an older/buggy version of this adapter (including arbitrarily nested `{ value: { value: ... } }` wrapping) — no manual data-fixing needed if you're upgrading from an earlier release.
382
+ Both adapters use the same `BufferJSON` serialization format and auto-detect legacy `{ value: { value: ... } }` double-wrapping from older versions — no manual data migration needed when upgrading.
379
383
 
380
- The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It's used as a key prefix (`sessionId::key`), so multiple bots can safely share one backend. The second argument is a connection string, a pre-built `Keyv` instance, or a raw `KeyvStoreAdapter`:
384
+ The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It is used as a Keyv namespace so multiple bots can safely share one backend without key collisions. The second argument is a connection string, a pre-built `Keyv` instance, or a raw `KeyvStoreAdapter`:
381
385
 
382
386
  ```javascript
383
387
  import { useKeyvAuthState } from '@zervonex/baileys'
@@ -407,52 +411,56 @@ const { state, saveCreds } = await useKeyvAuthState('bot1', 'memcache://localhos
407
411
  const { state, saveCreds } = await useKeyvAuthState('test')
408
412
  ```
409
413
 
410
- Supported connection string protocols: `redis:`/`rediss:`, `mongodb:`/`mongodb+srv:`, `postgres:`/`postgresql:`, `mysql:`, `sqlite:`, `etcd:`, `memcache:`/`memcached:`. All matching driver packages (`@keyv/redis`, `@keyv/mongo`, `@keyv/postgres`, `@keyv/mysql`, `@keyv/sqlite`, `@keyv/etcd`, `@keyv/memcache`) ship as regular dependencies of this package — nothing extra to install.
414
+ Supported protocols: `redis:`/`rediss:`, `mongodb:`/`mongodb+srv:`, `postgres:`/`postgresql:`, `mysql:`, `sqlite:`, `etcd:`, `memcache:`/`memcached:`. All matching driver packages ship as dependencies — nothing extra to install.
411
415
 
412
- For backends without a connection string (e.g. DynamoDB), or to reuse one connection across multiple `useKeyvAuthState` calls yourself, pass a pre-built adapter or `Keyv` instance instead of a string:
416
+ For backends without a connection string (e.g. DynamoDB), or to share one connection across multiple sessions, pass a pre-built adapter or `Keyv` instance:
413
417
 
414
418
  ```javascript
415
- import { useKeyvAuthState } from '@zervonex/baileys'
416
419
  import KeyvMongo from '@keyv/mongo'
420
+ import KeyvDynamo from '@keyv/dynamo'
421
+
422
+ // share one connection across sessions
423
+ const mongo = new KeyvMongo('mongodb://localhost/baileys')
424
+ const { state: s1 } = await useKeyvAuthState('bot1', mongo)
425
+ const { state: s2 } = await useKeyvAuthState('bot2', mongo)
417
426
 
418
- const backend = new KeyvMongo('mongodb://localhost/baileys') // build once, share across sessions
419
- const { state, saveCreds } = await useKeyvAuthState('bot1', backend)
427
+ // DynamoDB (no connection string protocol)
428
+ const dynamo = new KeyvDynamo({ region: 'us-east-1', table: 'baileys_auth' })
429
+ const { state, saveCreds } = await useKeyvAuthState('bot1', dynamo)
420
430
  ```
421
431
 
422
- Same connection string passed as a plain string is automatically cached and reused across every `useKeyvAuthState` call in the process — you don't need to build the instance yourself just to avoid opening duplicate connections, but you can if you want the reference for other purposes.
432
+ Connection strings passed as plain strings are automatically cached and reused across every `useKeyvAuthState` call in the process — no need to build the instance yourself just to avoid duplicate connections.
423
433
 
424
- By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv` (each adapter's own default). Set `collectionName` to use your own name instead — it's mapped to whatever each backend actually calls it (`collection` for Mongo, `table` for Postgres/MySQL/SQLite):
434
+ By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv`. Pass `collectionName` to use your own:
425
435
 
426
436
  ```javascript
427
437
  const { state, saveCreds } = await useKeyvAuthState('bot1', 'mongodb://localhost/baileys', {
428
- collectionName: 'baileys_auth', // instead of the default "keyv" collection
438
+ collectionName: 'baileys_auth',
429
439
  })
430
440
  ```
431
441
 
432
- > Per-type backend routing (e.g. prekeys on SQLite, sessions on Postgres) is not currently supported — one `useKeyvAuthState` call uses one backend for every key type.
442
+ > Per-type backend routing (e.g. prekeys on SQLite, sessions on Postgres) is not currently supported — one `useKeyvAuthState` call uses one backend for all key types.
433
443
 
434
444
  #### Clearing a session (logout)
435
445
 
436
446
  ```javascript
437
447
  const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', 'redis://localhost:6379')
438
448
 
439
- // on logout / when you want to wipe this session's auth entirely:
440
- await clearSession()
449
+ await clearSession() // wipes all keys for this session, disconnects the store
441
450
  ```
442
451
 
443
- `clearSession()` wipes every key belonging to that `sessionId` — creds, prekeys, sessions, sender keys, everything — by iterating the store filtering by the `sessionId::` prefix and calling `deleteMany`. Only this session's keys are removed; other sessions sharing the same backend are untouched. There is no equivalent method on `useMultiFileAuthState` — delete the session folder instead.
452
+ `clearSession()` removes every key scoped to that session's namespace — creds, prekeys, sessions, sender keys, everything. Other sessions sharing the same backend are untouched.
444
453
 
445
454
  #### Checking auth without loading full state
446
455
 
447
456
  ```javascript
448
457
  import { hasKeyvAuth } from '@zervonex/baileys'
449
458
 
450
- // Check if a session has valid auth (lightweight — no full state load)
451
459
  const valid = await hasKeyvAuth('bot1', backend)
452
460
  // true if creds exist and contain noiseKey + signedIdentityKey
453
461
  ```
454
462
 
455
- `hasKeyvAuth()` queries the backend using the correct key prefix — the same way `useKeyvAuthState` does internally — so it always returns the right answer regardless of your backend. Useful for checking session ownership before deciding whether to restore a connection, without the overhead of initializing a full auth state.
463
+ Lightweight — opens no full auth state, just checks for valid creds under the session's namespace.
456
464
 
457
465
  #### Options
458
466
 
@@ -460,8 +468,7 @@ const valid = await hasKeyvAuth('bot1', backend)
460
468
  |--------|------|---------|-------------|
461
469
  | `preKeyRetention` | `number` | `150` | Number of recent prekeys to keep |
462
470
  | `cleanupThreshold` | `number` | `50` | New prekeys generated before cleanup runs |
463
- | `logger` | `object` | `undefined` | Logger with `.info` / `.warn` methods |
464
- | `collectionName` | `string` | adapter's own default (`"keyv"`) | Custom collection/table name — Keyv-based adapter only, ignored for connection-string-less (in-memory) and pre-built adapter/`Keyv` instances |
471
+ | `collectionName` | `string` | `"keyv"` | Custom collection/table name (Keyv adapter only, ignored for in-memory and pre-built instances) |
465
472
 
466
473
  #### Return values
467
474
 
@@ -471,7 +478,6 @@ const valid = await hasKeyvAuth('bot1', backend)
471
478
  | `saveCreds` | ✅ | ✅ |
472
479
  | `getStats` | ✅ | ❌ |
473
480
  | `clearSession` | ❌ (delete the folder instead) | ✅ |
474
- | `close` | ❌ | ✅ (drops local listeners; safe to call, does not disconnect a shared backend) |
475
481
 
476
482
  ---
477
483
 
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, rename, stat, unlink, writeFile, readdir } from 'fs/promises'
2
2
  import { join } from 'path'
3
- import { Keyv } from 'keyv'
3
+ import { Keyv, detectKeyvStorage } from 'keyv'
4
4
  import { proto } from '../../WAProto/index.js'
5
5
  import { initAuthCreds } from './auth-utils.js'
6
6
  import { BufferJSON } from './generics.js'
@@ -11,21 +11,16 @@ const DEFAULT_PREKEY_RETENTION = 150
11
11
  const DEFAULT_CLEANUP_THRESHOLD = 50
12
12
  const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
13
13
 
14
- // ── Shared helpers ────────────────────────────────────────────────────────────
14
+ // ── Helpers ───────────────────────────────────────────────────────────────────
15
15
 
16
16
  const bootstrapCreds = raw => raw ?? initAuthCreds()
17
-
18
- const patchAppStateKey = (type, v) =>
19
- type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
20
-
17
+ const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
21
18
  const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
22
-
23
19
  const deser = raw => {
24
20
  if (raw == null) return null
25
21
  let p = typeof raw === 'string'
26
- ? (() => { try { return JSON.parse(raw) } catch (e) { logger.warn({ err: e.message }, '[Auth] failed to parse stored value — treating as missing'); return null } })()
22
+ ? (() => { try { return JSON.parse(raw) } catch (e) { logger.warn({ err: e.message }, '[Auth] failed to parse stored value'); return null } })()
27
23
  : raw
28
- // unwrap adapter double-wrapping e.g. { value: { value: actual } }
29
24
  while (p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
30
25
  if (p == null) return null
31
26
  if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
@@ -34,7 +29,9 @@ const deser = raw => {
34
29
 
35
30
  // ── Adapter resolution ────────────────────────────────────────────────────────
36
31
 
32
+ // Only adapters that accept a named collection/table — redis, valkey, etcd, memcache, dynamo do not
37
33
  const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
34
+
38
35
  const _adapterCache = new Map()
39
36
  export const clearAdapterCache = () => _adapterCache.clear()
40
37
 
@@ -44,7 +41,7 @@ const resolveAdapter = async (conn, col) => {
44
41
  let protocol
45
42
  try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
46
43
  const pkg = PROTOCOL_ADAPTERS[protocol]
47
- if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: redis, mongodb, postgresql, mysql, sqlite`)
44
+ if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: ${Object.keys(PROTOCOL_ADAPTERS).map(p => p.replace(':', '')).join(', ')}`)
48
45
  let mod
49
46
  try { mod = await import(pkg) } catch (e) {
50
47
  if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
@@ -58,12 +55,25 @@ const resolveAdapter = async (conn, col) => {
58
55
  return adapter
59
56
  }
60
57
 
61
- const resolveStore = async (backend, col) => {
58
+ // Object.create scoping only needed for mapLike stores — keyvStorage and asyncMap have namespace managed by Keyv/bridge
59
+ const resolveStore = async (backend, col, namespace) => {
62
60
  if (backend == null) return null
63
61
  if (typeof backend === 'string') return resolveAdapter(backend, col)
64
62
  if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
65
- if (backend != null && typeof backend === 'object' && ['get', 'set', 'delete', 'clear'].every(m => typeof backend[m] === 'function')) return backend
66
- throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
63
+ const { compatible, store } = detectKeyvStorage(backend)
64
+ if (!compatible) throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
65
+ if (store === 'mapLike') {
66
+ const scoped = Object.create(backend)
67
+ if (namespace) scoped.namespace = namespace
68
+ return scoped
69
+ }
70
+ return backend
71
+ }
72
+
73
+ const makeKeyv = (store, namespace) => {
74
+ const keyv = store ? new Keyv({ store, namespace }) : new Keyv({ namespace })
75
+ keyv.setMaxListeners?.(0)
76
+ return keyv
67
77
  }
68
78
 
69
79
  // ── File-based auth state ─────────────────────────────────────────────────────
@@ -77,13 +87,12 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
77
87
  if (info) { if (!info.isDirectory()) throw new Error(`Not a directory: ${folder}`) }
78
88
  else await mkdir(folder, { recursive: true })
79
89
 
80
- // atomic write: tmp → rename, fallback to direct write
90
+ // Atomic write: tmp → rename, fallback to direct write
81
91
  const write = async (data, file) => {
82
92
  const [p, t, payload] = [fp(file), fp(file) + '.tmp', JSON.stringify(data, BufferJSON.replacer)]
83
93
  await writeFile(t, payload)
84
94
  try { await rename(t, p) } catch { await writeFile(p, payload); await unlink(t).catch(() => { }) }
85
95
  }
86
-
87
96
  const read = async file => {
88
97
  const raw = await readFile(fp(file), 'utf-8').catch(() => null)
89
98
  if (!raw) return null
@@ -96,7 +105,6 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
96
105
  return null
97
106
  }
98
107
  }
99
-
100
108
  const remove = f => unlink(fp(f)).catch(() => { })
101
109
 
102
110
  let creds = bootstrapCreds(await read('creds.json'))
@@ -114,11 +122,9 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
114
122
  .map(m => join(folder, m[0]))
115
123
  if (targets.length) await Promise.all(targets.map(f => unlink(f).catch(() => { })))
116
124
  lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
117
- } catch (e) {
118
- logger.warn({ err: e.message }, 'prekey cleanup failed')
119
- } finally { cleanupRunning = false }
125
+ } catch (e) { logger.warn({ err: e.message }, 'prekey cleanup failed') }
126
+ finally { cleanupRunning = false }
120
127
  }
121
-
122
128
  cleanOldPreKeys().catch(() => { })
123
129
 
124
130
  return {
@@ -153,20 +159,14 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
153
159
 
154
160
  export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
155
161
  const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName } = opts
156
-
157
- const store = await resolveStore(backend, collectionName)
158
- const keyv = store ? new Keyv({ store }) : new Keyv()
159
- keyv.setMaxListeners?.(0)
162
+ const store = await resolveStore(backend, collectionName, sessionId)
163
+ const keyv = makeKeyv(store, sessionId)
160
164
  keyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
161
165
 
162
- // all keys prefixed sessionId:: to isolate sessions on a shared store
163
- const kn = (type, id) => `${sessionId}::${type}-${id}`
164
- const credsKey = `${sessionId}::creds`
165
-
166
+ const kn = (type, id) => `${type}-${id}`
167
+ const credsKey = 'creds'
166
168
  let creds = bootstrapCreds(deser(await keyv.get(credsKey)))
167
169
  let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
168
- // tracks every key ever written — fallback clearSession uses this to wipe all types including JID-based IDs
169
- const _writtenKeys = new Set([credsKey])
170
170
 
171
171
  const cleanOldPreKeys = async () => {
172
172
  if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
@@ -176,13 +176,11 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
176
176
  if (minId <= 0) return
177
177
  const ids = []
178
178
  for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
179
- if (ids.length) await Promise.all(ids.map(k => keyv.delete(k).catch(() => { })))
179
+ if (ids.length) await keyv.deleteMany(ids)
180
180
  lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
181
- } catch (e) {
182
- logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed')
183
- } finally { cleanupRunning = false }
181
+ } catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
182
+ finally { cleanupRunning = false }
184
183
  }
185
-
186
184
  cleanOldPreKeys().catch(() => { })
187
185
 
188
186
  const keys = {
@@ -196,58 +194,35 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
196
194
  for (const type in data)
197
195
  for (const id in data[type]) {
198
196
  const v = data[type][id], k = kn(type, id)
199
- if (v != null) { sets.push({ key: k, value: ser(v) }); _writtenKeys.add(k) }
200
- else { dels.push(k); _writtenKeys.delete(k) }
197
+ if (v != null) sets.push({ key: k, value: ser(v) })
198
+ else dels.push(k)
201
199
  }
202
- await Promise.all([
203
- sets.length ? keyv.setMany(sets) : null,
204
- dels.length ? keyv.deleteMany(dels) : null
205
- ])
200
+ await Promise.all([sets.length ? keyv.setMany(sets) : null, dels.length ? keyv.deleteMany(dels) : null])
206
201
  }
207
202
  }
208
203
 
209
204
  const saveCreds = async () => {
210
205
  if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
211
- _writtenKeys.add(credsKey)
212
206
  await keyv.set(credsKey, ser(creds))
213
207
  }
214
208
 
215
209
  const clearSession = async () => {
216
- const prefix = `${sessionId}::`
217
- let done = false
218
- if (store) {
219
- // Redis: KEYS pattern scan
220
- const rc = store.client
221
- if (rc && typeof rc.keys === 'function') {
222
- try { await store.set('__probe__', 1); const m = await rc.keys(`${prefix}*`); if (m.length) await rc.del(m); done = true } catch { }
223
- }
224
- // Mongo/Postgres/SQLite: iterator scan
225
- if (!done && typeof store.iterator === 'function') {
226
- try {
227
- const targets = []
228
- for await (const [k] of store.iterator(prefix)) if (k.startsWith(prefix)) targets.push(k)
229
- if (targets.length) await store.deleteMany(targets)
230
- done = true
231
- } catch { }
232
- }
233
- }
234
- if (!done) {
235
- // fallback (in-memory): delete every key ever written — covers all types + JID-based IDs
236
- await Promise.all([..._writtenKeys].map(k => keyv.delete(k).catch(() => { })))
237
- _writtenKeys.clear()
238
- }
210
+ try {
211
+ const isolated = makeKeyv(store, sessionId)
212
+ await isolated.clear()
213
+ isolated.removeAllListeners?.()
214
+ } catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] clearSession failed') }
239
215
  keyv.removeAllListeners?.()
240
216
  }
241
217
 
242
218
  return { state: { creds, keys }, saveCreds, clearSession }
243
219
  }
244
220
 
245
- // check if valid creds exist for a session without fully loading it
221
+ // Checks if valid creds exist for a session without fully loading it
246
222
  export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
247
223
  try {
248
- const store = await resolveStore(backend, opts.collectionName)
249
- const keyv = store ? new Keyv({ store }) : new Keyv()
250
- const parsed = deser(await keyv.get(`${sessionId}::creds`))
224
+ const keyv = makeKeyv(await resolveStore(backend, opts.collectionName, sessionId), sessionId)
225
+ const parsed = deser(await keyv.get('creds'))
251
226
  return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
252
227
  } catch { return false }
253
228
  }
package/lib/index.js CHANGED
@@ -29,7 +29,7 @@ const info = [
29
29
  '╭──────────────────────────────────────────────────────────────╮',
30
30
  '│ │',
31
31
  '│ ◆ Package › @zervonex/baileys │',
32
- '│ ◆ Version › 3.0.5 │',
32
+ '│ ◆ Version › 3.0.7 │',
33
33
  '│ ◆ Status › Production Ready │',
34
34
  '│ │',
35
35
  '├──────────────────────────── ◇◆◇ ─────────────────────────────┤',
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@zervonex/baileys",
4
4
  "type": "module",
5
- "version": "3.0.5",
5
+ "version": "3.0.7",
6
6
  "description": "Advanced WhatsApp Web API built on Baileys — interactive messages, buttons, carousels, products, events, payments, polls, albums, sticker packs and more.",
7
7
  "keywords": [
8
8
  "baileys",