@zervonex/baileys 3.0.6 → 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
@@ -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,24 @@ 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
+ ```
377
+
378
+ ---
373
379
 
374
380
  ### Keyv-based (production)
375
381
 
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.
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.
383
+
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.
377
385
 
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.
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`.
379
387
 
380
- The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It's used as a Keyv namespace, so multiple bots can safely share one backend. The second argument is a connection string, a pre-built `Keyv` instance, or a raw `KeyvStoreAdapter`:
388
+ #### Basic usage
381
389
 
382
390
  ```javascript
383
391
  import { useKeyvAuthState } from '@zervonex/baileys'
@@ -407,52 +415,86 @@ const { state, saveCreds } = await useKeyvAuthState('bot1', 'memcache://localhos
407
415
  const { state, saveCreds } = await useKeyvAuthState('test')
408
416
  ```
409
417
 
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.
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.
411
419
 
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:
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:
413
433
 
414
434
  ```javascript
415
- import { useKeyvAuthState } from '@zervonex/baileys'
416
435
  import KeyvMongo from '@keyv/mongo'
436
+ import KeyvDynamo from '@keyv/dynamo'
437
+
438
+ // share one adapter instance across sessions
439
+ const mongo = new KeyvMongo('mongodb://localhost/baileys')
440
+ const { state: s1 } = await useKeyvAuthState('bot1', mongo)
441
+ const { state: s2 } = await useKeyvAuthState('bot2', mongo)
417
442
 
418
- const backend = new KeyvMongo('mongodb://localhost/baileys') // build once, share across sessions
419
- const { state, saveCreds } = await useKeyvAuthState('bot1', backend)
443
+ // DynamoDB (no connection string protocol)
444
+ const dynamo = new KeyvDynamo({ region: 'us-east-1', table: 'baileys_auth' })
445
+ const { state, saveCreds } = await useKeyvAuthState('bot1', dynamo)
420
446
  ```
421
447
 
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.
448
+ #### Custom collection/table name
423
449
 
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):
450
+ By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv`. Pass `collectionName` to use your own:
425
451
 
426
452
  ```javascript
427
453
  const { state, saveCreds } = await useKeyvAuthState('bot1', 'mongodb://localhost/baileys', {
428
- collectionName: 'baileys_auth', // instead of the default "keyv" collection
454
+ collectionName: 'baileys_auth',
455
+ })
456
+ ```
457
+
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',
429
471
  })
430
472
  ```
431
473
 
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.
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.
433
477
 
434
478
  #### Clearing a session (logout)
435
479
 
436
480
  ```javascript
437
481
  const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', 'redis://localhost:6379')
438
482
 
439
- // on logout / when you want to wipe this session's auth entirely:
440
- await clearSession()
483
+ await clearSession() // wipes all keys for this session only — other sessions untouched
441
484
  ```
442
485
 
443
- `clearSession()` wipes every key belonging to that `sessionId` — creds, prekeys, sessions, sender keys, everything — by calling `keyv.clear()` which removes all keys scoped to that session's namespace. 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.
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.
444
487
 
445
488
  #### Checking auth without loading full state
446
489
 
447
490
  ```javascript
448
491
  import { hasKeyvAuth } from '@zervonex/baileys'
449
492
 
450
- // Check if a session has valid auth (lightweight — no full state load)
451
493
  const valid = await hasKeyvAuth('bot1', backend)
452
494
  // true if creds exist and contain noiseKey + signedIdentityKey
453
495
  ```
454
496
 
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.
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).
456
498
 
457
499
  #### Options
458
500
 
@@ -460,8 +502,7 @@ const valid = await hasKeyvAuth('bot1', backend)
460
502
  |--------|------|---------|-------------|
461
503
  | `preKeyRetention` | `number` | `150` | Number of recent prekeys to keep |
462
504
  | `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 |
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) |
465
506
 
466
507
  #### Return values
467
508
 
@@ -471,7 +512,6 @@ const valid = await hasKeyvAuth('bot1', backend)
471
512
  | `saveCreds` | ✅ | ✅ |
472
513
  | `getStats` | ✅ | ❌ |
473
514
  | `clearSession` | ❌ (delete the folder instead) | ✅ |
474
- | `close` | ❌ | ✅ (drops local listeners; safe to call, does not disconnect a shared backend) |
475
515
 
476
516
  ---
477
517
 
@@ -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'
@@ -10,23 +10,21 @@ 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
- // ── Shared helpers ────────────────────────────────────────────────────────────
15
+ // ── Helpers ───────────────────────────────────────────────────────────────────
15
16
 
16
17
  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
-
18
+ const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
21
19
  const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
22
-
23
20
  const deser = raw => {
24
21
  if (raw == null) return null
25
22
  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 } })()
23
+ ? (() => { try { return JSON.parse(raw) } catch { return raw } })() // ← return raw string on parse fail, don't warn
27
24
  : raw
28
- // unwrap adapter double-wrapping e.g. { value: { value: actual } }
29
- 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
30
28
  if (p == null) return null
31
29
  if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
32
30
  return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
@@ -34,17 +32,22 @@ const deser = raw => {
34
32
 
35
33
  // ── Adapter resolution ────────────────────────────────────────────────────────
36
34
 
35
+ // Only these adapters accept a named collection/table — redis, valkey, etcd etc do not
37
36
  const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
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.
38
41
  const _adapterCache = new Map()
39
42
  export const clearAdapterCache = () => _adapterCache.clear()
40
43
 
41
- const resolveAdapter = async (conn, col) => {
42
- const ck = col ? `${conn}::${col}` : conn
44
+ const resolveAdapter = async (conn, col, namespace) => {
45
+ const ck = `${conn}::${col ?? ''}::${namespace ?? ''}`
43
46
  if (_adapterCache.has(ck)) return _adapterCache.get(ck)
44
47
  let protocol
45
48
  try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
46
49
  const pkg = PROTOCOL_ADAPTERS[protocol]
47
- if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: redis, mongodb, postgresql, mysql, sqlite`)
50
+ if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: ${Object.keys(PROTOCOL_ADAPTERS).map(p => p.replace(':', '')).join(', ')}`)
48
51
  let mod
49
52
  try { mod = await import(pkg) } catch (e) {
50
53
  if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
@@ -54,16 +57,61 @@ const resolveAdapter = async (conn, col) => {
54
57
  const optKey = COLLECTION_KEY[pkg]
55
58
  const adapter = col && optKey ? new Adapter({ url: conn, uri: conn, [optKey]: col }) : new Adapter(conn)
56
59
  adapter.setMaxListeners?.(0)
60
+ if (namespace) adapter.namespace = namespace
57
61
  _adapterCache.set(ck, adapter)
58
62
  return adapter
59
63
  }
60
64
 
61
- const resolveStore = async (backend, col) => {
65
+ const resolveStore = async (backend, col, namespace) => {
62
66
  if (backend == null) return null
63
- if (typeof backend === 'string') return resolveAdapter(backend, col)
67
+ if (typeof backend === 'string') return resolveAdapter(backend, col, namespace)
64
68
  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')
69
+ const { compatible } = detectKeyvStorage(backend)
70
+ if (!compatible) throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
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
75
+ }
76
+
77
+ const makeKeyv = (store, namespace) => {
78
+ const keyv = store ? new Keyv({ store, namespace }) : new Keyv({ namespace })
79
+ keyv.setMaxListeners?.(0)
80
+ return keyv
81
+ }
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())] }
67
115
  }
68
116
 
69
117
  // ── File-based auth state ─────────────────────────────────────────────────────
@@ -77,13 +125,11 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
77
125
  if (info) { if (!info.isDirectory()) throw new Error(`Not a directory: ${folder}`) }
78
126
  else await mkdir(folder, { recursive: true })
79
127
 
80
- // atomic write: tmp → rename, fallback to direct write
81
128
  const write = async (data, file) => {
82
129
  const [p, t, payload] = [fp(file), fp(file) + '.tmp', JSON.stringify(data, BufferJSON.replacer)]
83
130
  await writeFile(t, payload)
84
131
  try { await rename(t, p) } catch { await writeFile(p, payload); await unlink(t).catch(() => { }) }
85
132
  }
86
-
87
133
  const read = async file => {
88
134
  const raw = await readFile(fp(file), 'utf-8').catch(() => null)
89
135
  if (!raw) return null
@@ -96,29 +142,28 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
96
142
  return null
97
143
  }
98
144
  }
99
-
100
145
  const remove = f => unlink(fp(f)).catch(() => { })
101
146
 
102
147
  let creds = bootstrapCreds(await read('creds.json'))
103
- let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
148
+ let cleanupPromise = null, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
104
149
 
105
150
  const cleanOldPreKeys = async () => {
106
- if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
107
- cleanupRunning = true
108
- try {
109
- const minId = creds.nextPreKeyId - preKeyRetention
110
- if (minId <= 0) return
111
- const targets = (await readdir(folder))
112
- .map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
113
- .filter(m => m && parseInt(m[1], 10) < minId)
114
- .map(m => join(folder, m[0]))
115
- if (targets.length) await Promise.all(targets.map(f => unlink(f).catch(() => { })))
116
- lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
117
- } catch (e) {
118
- logger.warn({ err: e.message }, 'prekey cleanup failed')
119
- } 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
120
166
  }
121
-
122
167
  cleanOldPreKeys().catch(() => { })
123
168
 
124
169
  return {
@@ -152,20 +197,15 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
152
197
  // ── Keyv-backed auth state ────────────────────────────────────────────────────
153
198
 
154
199
  export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
155
- 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, namespace: sessionId }) : new Keyv({ namespace: sessionId })
159
- keyv.setMaxListeners?.(0)
160
- 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'))
161
204
 
162
205
  const kn = (type, id) => `${type}-${id}`
163
206
  const credsKey = 'creds'
164
-
165
- let creds = bootstrapCreds(deser(await keyv.get(credsKey)))
207
+ let creds = bootstrapCreds(deser(await credsKeyv.get(credsKey)))
166
208
  let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
167
- // tracks every key ever written — fallback clearSession uses this to wipe all types including JID-based IDs
168
- const _writtenKeys = new Set([credsKey])
169
209
 
170
210
  const cleanOldPreKeys = async () => {
171
211
  if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
@@ -175,56 +215,59 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
175
215
  if (minId <= 0) return
176
216
  const ids = []
177
217
  for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
178
- if (ids.length) await Promise.all(ids.map(k => keyv.delete(k).catch(() => { })))
218
+ if (ids.length) await bmap.get('pre-key').deleteMany(ids)
179
219
  lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
180
- } catch (e) {
181
- logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed')
182
- } finally { cleanupRunning = false }
220
+ } catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
221
+ finally { cleanupRunning = false }
183
222
  }
184
-
185
223
  cleanOldPreKeys().catch(() => { })
186
224
 
187
225
  const keys = {
188
226
  get: async (type, ids) => {
189
227
  if (!ids.length) return {}
228
+ const keyv = bmap.get(type)
190
229
  const raws = await keyv.getMany(ids.map(id => kn(type, id)))
191
230
  return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(raws[i]))]))
192
231
  },
193
232
  set: async data => {
194
- const sets = [], dels = []
195
- 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)
196
239
  for (const id in data[type]) {
197
240
  const v = data[type][id], k = kn(type, id)
198
- if (v != null) { sets.push({ key: k, value: ser(v) }); _writtenKeys.add(k) }
199
- else { dels.push(k); _writtenKeys.delete(k) }
241
+ if (v != null) bucket.sets.push({ key: k, value: ser(v) })
242
+ else bucket.dels.push(k)
200
243
  }
201
- await Promise.all([
202
- sets.length ? keyv.setMany(sets) : null,
203
- dels.length ? keyv.deleteMany(dels) : null
204
- ])
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
+ ))
205
248
  }
206
249
  }
207
250
 
208
251
  const saveCreds = async () => {
209
252
  if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
210
- _writtenKeys.add(credsKey)
211
- await keyv.set(credsKey, ser(creds))
253
+ await credsKeyv.set(credsKey, ser(creds))
212
254
  }
213
255
 
214
256
  const clearSession = async () => {
215
- await keyv.clear()
216
- 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?.())
217
260
  }
218
261
 
219
262
  return { state: { creds, keys }, saveCreds, clearSession }
220
263
  }
221
264
 
222
- // check if valid creds exist for a session without fully loading it
265
+ // Lightweight check — reads only the creds key without full state init
223
266
  export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
224
267
  try {
225
- const store = await resolveStore(backend, opts.collectionName)
226
- const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
227
- 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'))
228
271
  return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
229
272
  } catch { return false }
230
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.6 │',
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.6",
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",