@zervonex/baileys 3.0.7 → 3.0.8

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
@@ -375,13 +375,17 @@ import { rm } from 'fs/promises'
375
375
  await rm('auth_session', { recursive: true, force: true })
376
376
  ```
377
377
 
378
+ ---
379
+
378
380
  ### Keyv-based (production)
379
381
 
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.
382
+ Universal adapter for any database backend via [Keyv](https://www.npmjs.com/package/keyv). Pass a connection string — the matching backend driver is resolved, instantiated, and **cached automatically per session** — no manual adapter imports needed.
381
383
 
382
384
  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.
383
385
 
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`:
386
+ 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`.
387
+
388
+ #### Basic usage
385
389
 
386
390
  ```javascript
387
391
  import { useKeyvAuthState } from '@zervonex/baileys'
@@ -413,13 +417,25 @@ const { state, saveCreds } = await useKeyvAuthState('test')
413
417
 
414
418
  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.
415
419
 
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:
420
+ #### How adapter caching works
421
+
422
+ Connection strings are cached **per `conn::collection::sessionId`** — each session gets its own isolated adapter instance so `clearSession()` and namespace operations never bleed across sessions. Underlying database drivers (MongoDB, pg, mysql2, ioredis) manage their own internal connection pools, so multiple adapter instances pointing at the same URI still share one real connection — no connection explosion across 50+ sessions.
423
+
424
+ ```javascript
425
+ // these two calls resolve to separate adapter instances but share one MongoDB connection internally
426
+ const bot1 = await useKeyvAuthState('session_111', 'mongodb://localhost/baileys')
427
+ const bot2 = await useKeyvAuthState('session_222', 'mongodb://localhost/baileys')
428
+ ```
429
+
430
+ #### Sharing a pre-built adapter
431
+
432
+ For backends without a connection string (e.g. DynamoDB), or when you need full control over the connection, pass a pre-built adapter or `Keyv` instance directly:
417
433
 
418
434
  ```javascript
419
435
  import KeyvMongo from '@keyv/mongo'
420
436
  import KeyvDynamo from '@keyv/dynamo'
421
437
 
422
- // share one connection across sessions
438
+ // share one adapter instance across sessions
423
439
  const mongo = new KeyvMongo('mongodb://localhost/baileys')
424
440
  const { state: s1 } = await useKeyvAuthState('bot1', mongo)
425
441
  const { state: s2 } = await useKeyvAuthState('bot2', mongo)
@@ -429,7 +445,7 @@ const dynamo = new KeyvDynamo({ region: 'us-east-1', table: 'baileys_auth' })
429
445
  const { state, saveCreds } = await useKeyvAuthState('bot1', dynamo)
430
446
  ```
431
447
 
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.
448
+ #### Custom collection/table name
433
449
 
434
450
  By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv`. Pass `collectionName` to use your own:
435
451
 
@@ -439,17 +455,35 @@ const { state, saveCreds } = await useKeyvAuthState('bot1', 'mongodb://localhost
439
455
  })
440
456
  ```
441
457
 
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.
458
+ #### Multi-backend routing
459
+
460
+ Route different key types to different backends — e.g. keep creds on Postgres and prekeys on Redis:
461
+
462
+ ```javascript
463
+ const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', {
464
+ 'creds': 'postgresql://user:pass@localhost/db',
465
+ 'pre-key': 'redis://localhost:6379',
466
+ 'session': 'redis://localhost:6379',
467
+ 'app-state-sync-key': 'mongodb://localhost/baileys',
468
+ 'app-state-sync-version': 'mongodb://localhost/baileys',
469
+ 'sender-key': 'redis://localhost:6379',
470
+ 'sender-key-memory': 'redis://localhost:6379',
471
+ })
472
+ ```
473
+
474
+ If a key type isn't listed in the map, the **first entry's backend** is used as the default. Backends that appear more than once in the map are deduplicated — one adapter instance, one connection.
475
+
476
+ `clearSession()` on a multi-backend session clears all backends in the map, each scoped to that session's namespace only.
443
477
 
444
478
  #### Clearing a session (logout)
445
479
 
446
480
  ```javascript
447
481
  const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', 'redis://localhost:6379')
448
482
 
449
- await clearSession() // wipes all keys for this session, disconnects the store
483
+ await clearSession() // wipes all keys for this session only — other sessions untouched
450
484
  ```
451
485
 
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.
486
+ `clearSession()` removes every key scoped to that session's namespace — creds, prekeys, sessions, sender keys, app state, everything. Sessions sharing the same backend are never affected.
453
487
 
454
488
  #### Checking auth without loading full state
455
489
 
@@ -460,7 +494,7 @@ const valid = await hasKeyvAuth('bot1', backend)
460
494
  // true if creds exist and contain noiseKey + signedIdentityKey
461
495
  ```
462
496
 
463
- Lightweight — opens no full auth state, just checks for valid creds under the session's namespace.
497
+ Lightweight — does not initialize full auth state, just checks for valid creds under the session's namespace. Accepts the same backend types as `useKeyvAuthState` including multi-backend maps (checks the first entry where creds live).
464
498
 
465
499
  #### Options
466
500
 
@@ -468,7 +502,7 @@ Lightweight — opens no full auth state, just checks for valid creds under the
468
502
  |--------|------|---------|-------------|
469
503
  | `preKeyRetention` | `number` | `150` | Number of recent prekeys to keep |
470
504
  | `cleanupThreshold` | `number` | `50` | New prekeys generated before cleanup runs |
471
- | `collectionName` | `string` | `"keyv"` | Custom collection/table name (Keyv adapter only, ignored for in-memory and pre-built instances) |
505
+ | `collectionName` | `string` | `"zervonex"` | Custom collection/table name — ignored for in-memory, pre-built instances, and backends without collection support (redis, etcd, memcache) |support (redis, etcd, memcache) |
472
506
 
473
507
  #### Return values
474
508
 
@@ -10,6 +10,7 @@ import logger from './logger.js'
10
10
  const DEFAULT_PREKEY_RETENTION = 150
11
11
  const DEFAULT_CLEANUP_THRESHOLD = 50
12
12
  const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
13
+ const DEFAULT_COLLECTION_NAME = 'zervonex'
13
14
 
14
15
  // ── Helpers ───────────────────────────────────────────────────────────────────
15
16
 
@@ -19,9 +20,11 @@ const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.repl
19
20
  const deser = raw => {
20
21
  if (raw == null) return null
21
22
  let p = typeof raw === 'string'
22
- ? (() => { try { return JSON.parse(raw) } catch (e) { logger.warn({ err: e.message }, '[Auth] failed to parse stored value'); return null } })()
23
+ ? (() => { try { return JSON.parse(raw) } catch { return raw } })() // ← return raw string on parse fail, don't warn
23
24
  : raw
24
- while (p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
25
+ // some adapters double-wrap in { value: { value: ... } } — unwrap up to 3 levels
26
+ let depth = 0
27
+ while (depth++ < 3 && p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
25
28
  if (p == null) return null
26
29
  if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
27
30
  return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
@@ -29,14 +32,17 @@ const deser = raw => {
29
32
 
30
33
  // ── Adapter resolution ────────────────────────────────────────────────────────
31
34
 
32
- // Only adapters that accept a named collection/table — redis, valkey, etcd, memcache, dynamo do not
35
+ // Only these adapters accept a named collection/table — redis, valkey, etcd etc do not
33
36
  const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
34
37
 
38
+ // Cached per conn::col::namespace — each session gets its own adapter instance so
39
+ // clear() never bleeds across sessions. Underlying drivers manage their own connection
40
+ // pools so multiple adapters on the same URI still share one real connection.
35
41
  const _adapterCache = new Map()
36
42
  export const clearAdapterCache = () => _adapterCache.clear()
37
43
 
38
- const resolveAdapter = async (conn, col) => {
39
- const ck = col ? `${conn}::${col}` : conn
44
+ const resolveAdapter = async (conn, col, namespace) => {
45
+ const ck = `${conn}::${col ?? ''}::${namespace ?? ''}`
40
46
  if (_adapterCache.has(ck)) return _adapterCache.get(ck)
41
47
  let protocol
42
48
  try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
@@ -51,23 +57,21 @@ const resolveAdapter = async (conn, col) => {
51
57
  const optKey = COLLECTION_KEY[pkg]
52
58
  const adapter = col && optKey ? new Adapter({ url: conn, uri: conn, [optKey]: col }) : new Adapter(conn)
53
59
  adapter.setMaxListeners?.(0)
60
+ if (namespace) adapter.namespace = namespace
54
61
  _adapterCache.set(ck, adapter)
55
62
  return adapter
56
63
  }
57
64
 
58
- // Object.create scoping only needed for mapLike stores — keyvStorage and asyncMap have namespace managed by Keyv/bridge
59
65
  const resolveStore = async (backend, col, namespace) => {
60
66
  if (backend == null) return null
61
- if (typeof backend === 'string') return resolveAdapter(backend, col)
67
+ if (typeof backend === 'string') return resolveAdapter(backend, col, namespace)
62
68
  if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
63
- const { compatible, store } = detectKeyvStorage(backend)
69
+ const { compatible } = detectKeyvStorage(backend)
64
70
  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
+ // scope namespace via prototype so we don't mutate the original store instance
72
+ const scoped = Object.create(backend)
73
+ if (namespace) scoped.namespace = namespace
74
+ return scoped
71
75
  }
72
76
 
73
77
  const makeKeyv = (store, namespace) => {
@@ -76,6 +80,40 @@ const makeKeyv = (store, namespace) => {
76
80
  return keyv
77
81
  }
78
82
 
83
+ // ── Multi-backend support ─────────────────────────────────────────────────────
84
+
85
+ // Multi-backend map: { 'creds': pgBackend, 'pre-key': mongoBackend, ... }
86
+ // Anything else (string, Keyv, raw store adapter) is treated as single backend
87
+ const isMultiBackend = backend =>
88
+ backend !== null
89
+ && typeof backend === 'object'
90
+ && !Array.isArray(backend)
91
+ && !(backend instanceof Keyv)
92
+ && !detectKeyvStorage(backend).compatible
93
+
94
+ // Returns { get(type) → Keyv, all → Keyv[] }
95
+ // get(type) falls back to first entry if type not in map
96
+ // all is deduplicated — shared backends only clear once
97
+ const resolveBackendMap = async (backend, sessionId, collectionName) => {
98
+ if (!isMultiBackend(backend)) {
99
+ const store = await resolveStore(backend, collectionName, sessionId)
100
+ const keyv = makeKeyv(store, sessionId)
101
+ return { get: () => keyv, all: [keyv] }
102
+ }
103
+ const entries = Object.entries(backend)
104
+ if (!entries.length) throw new Error('[Auth] backend map must have at least one entry')
105
+ const resolved = new Map()
106
+ for (const [, b] of entries) {
107
+ if (!resolved.has(b)) {
108
+ const store = await resolveStore(b, collectionName, sessionId)
109
+ resolved.set(b, makeKeyv(store, sessionId))
110
+ }
111
+ }
112
+ const typeMap = Object.fromEntries(entries.map(([type, b]) => [type, resolved.get(b)]))
113
+ const defaultKeyv = resolved.get(entries[0][1])
114
+ return { get: type => typeMap[type] ?? defaultKeyv, all: [...new Set(resolved.values())] }
115
+ }
116
+
79
117
  // ── File-based auth state ─────────────────────────────────────────────────────
80
118
 
81
119
  export const useMultiFileAuthState = async (folder, opts = {}) => {
@@ -87,7 +125,6 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
87
125
  if (info) { if (!info.isDirectory()) throw new Error(`Not a directory: ${folder}`) }
88
126
  else await mkdir(folder, { recursive: true })
89
127
 
90
- // Atomic write: tmp → rename, fallback to direct write
91
128
  const write = async (data, file) => {
92
129
  const [p, t, payload] = [fp(file), fp(file) + '.tmp', JSON.stringify(data, BufferJSON.replacer)]
93
130
  await writeFile(t, payload)
@@ -108,22 +145,24 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
108
145
  const remove = f => unlink(fp(f)).catch(() => { })
109
146
 
110
147
  let creds = bootstrapCreds(await read('creds.json'))
111
- let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
148
+ let cleanupPromise = null, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
112
149
 
113
150
  const cleanOldPreKeys = async () => {
114
- if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
115
- cleanupRunning = true
116
- try {
117
- const minId = creds.nextPreKeyId - preKeyRetention
118
- if (minId <= 0) return
119
- const targets = (await readdir(folder))
120
- .map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
121
- .filter(m => m && parseInt(m[1], 10) < minId)
122
- .map(m => join(folder, m[0]))
123
- if (targets.length) await Promise.all(targets.map(f => unlink(f).catch(() => { })))
124
- lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
125
- } catch (e) { logger.warn({ err: e.message }, 'prekey cleanup failed') }
126
- finally { cleanupRunning = false }
151
+ if (cleanupPromise || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
152
+ cleanupPromise = (async () => {
153
+ try {
154
+ const minId = creds.nextPreKeyId - preKeyRetention
155
+ if (minId <= 0) return
156
+ const targets = (await readdir(folder))
157
+ .map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
158
+ .filter(m => m && parseInt(m[1], 10) < minId)
159
+ .map(m => join(folder, m[0]))
160
+ if (targets.length) await Promise.all(targets.map(f => unlink(f).catch(() => { })))
161
+ lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
162
+ } catch (e) { logger.warn({ err: e.message }, 'prekey cleanup failed') }
163
+ finally { cleanupPromise = null }
164
+ })()
165
+ await cleanupPromise
127
166
  }
128
167
  cleanOldPreKeys().catch(() => { })
129
168
 
@@ -158,14 +197,14 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
158
197
  // ── Keyv-backed auth state ────────────────────────────────────────────────────
159
198
 
160
199
  export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
161
- const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName } = opts
162
- const store = await resolveStore(backend, collectionName, sessionId)
163
- const keyv = makeKeyv(store, sessionId)
164
- keyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
200
+ const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName = DEFAULT_COLLECTION_NAME } = opts
201
+ const bmap = await resolveBackendMap(backend, sessionId, collectionName)
202
+ const credsKeyv = bmap.get('creds')
203
+ credsKeyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
165
204
 
166
205
  const kn = (type, id) => `${type}-${id}`
167
206
  const credsKey = 'creds'
168
- let creds = bootstrapCreds(deser(await keyv.get(credsKey)))
207
+ let creds = bootstrapCreds(deser(await credsKeyv.get(credsKey)))
169
208
  let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
170
209
 
171
210
  const cleanOldPreKeys = async () => {
@@ -176,7 +215,7 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
176
215
  if (minId <= 0) return
177
216
  const ids = []
178
217
  for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
179
- if (ids.length) await keyv.deleteMany(ids)
218
+ if (ids.length) await bmap.get('pre-key').deleteMany(ids)
180
219
  lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
181
220
  } catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
182
221
  finally { cleanupRunning = false }
@@ -186,43 +225,49 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
186
225
  const keys = {
187
226
  get: async (type, ids) => {
188
227
  if (!ids.length) return {}
228
+ const keyv = bmap.get(type)
189
229
  const raws = await keyv.getMany(ids.map(id => kn(type, id)))
190
230
  return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(raws[i]))]))
191
231
  },
192
232
  set: async data => {
193
- const sets = [], dels = []
194
- for (const type in data)
233
+ // group by Keyv instance to batch writes per backend
234
+ const byKeyv = new Map()
235
+ for (const type in data) {
236
+ const keyv = bmap.get(type)
237
+ if (!byKeyv.has(keyv)) byKeyv.set(keyv, { sets: [], dels: [] })
238
+ const bucket = byKeyv.get(keyv)
195
239
  for (const id in data[type]) {
196
240
  const v = data[type][id], k = kn(type, id)
197
- if (v != null) sets.push({ key: k, value: ser(v) })
198
- else dels.push(k)
241
+ if (v != null) bucket.sets.push({ key: k, value: ser(v) })
242
+ else bucket.dels.push(k)
199
243
  }
200
- await Promise.all([sets.length ? keyv.setMany(sets) : null, dels.length ? keyv.deleteMany(dels) : null])
244
+ }
245
+ await Promise.all([...byKeyv.entries()].map(([keyv, { sets, dels }]) =>
246
+ Promise.all([sets.length ? keyv.setMany(sets) : null, dels.length ? keyv.deleteMany(dels) : null])
247
+ ))
201
248
  }
202
249
  }
203
250
 
204
251
  const saveCreds = async () => {
205
252
  if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
206
- await keyv.set(credsKey, ser(creds))
253
+ await credsKeyv.set(credsKey, ser(creds))
207
254
  }
208
255
 
209
256
  const clearSession = async () => {
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') }
215
- keyv.removeAllListeners?.()
257
+ try { await Promise.all(bmap.all.map(keyv => keyv.clear())) }
258
+ catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] clearSession failed') }
259
+ bmap.all.forEach(keyv => keyv.removeAllListeners?.())
216
260
  }
217
261
 
218
262
  return { state: { creds, keys }, saveCreds, clearSession }
219
263
  }
220
264
 
221
- // Checks if valid creds exist for a session without fully loading it
265
+ // Lightweight check — reads only the creds key without full state init
222
266
  export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
223
267
  try {
224
- const keyv = makeKeyv(await resolveStore(backend, opts.collectionName, sessionId), sessionId)
225
- const parsed = deser(await keyv.get('creds'))
268
+ const b = isMultiBackend(backend) ? Object.values(backend)[0] : backend
269
+ const bmap = await resolveBackendMap(b, sessionId, opts.collectionName ?? DEFAULT_COLLECTION_NAME)
270
+ const parsed = deser(await bmap.get('creds').get('creds'))
226
271
  return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
227
272
  } catch { return false }
228
273
  }
package/lib/index.js CHANGED
@@ -29,7 +29,7 @@ const info = [
29
29
  '╭──────────────────────────────────────────────────────────────╮',
30
30
  '│ │',
31
31
  '│ ◆ Package › @zervonex/baileys │',
32
- '│ ◆ Version › 3.0.7 │',
32
+ '│ ◆ Version › 3.0.8 │',
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.7",
5
+ "version": "3.0.8",
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",