@zervonex/baileys 3.0.4 → 3.0.5

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
@@ -344,7 +344,6 @@ autoFollowNewsletter: false, // default: true
344
344
  })
345
345
  ```
346
346
  ---
347
-
348
347
  ## 💾 Session Management
349
348
 
350
349
  Two adapters are available. They do **not** share an identical return shape — see the differences noted below each.
@@ -378,7 +377,7 @@ Universal adapter for any database backend via [Keyv](https://github.com/jaredwr
378
377
 
379
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.
380
379
 
381
- The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It's used as the 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`:
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`:
382
381
 
383
382
  ```javascript
384
383
  import { useKeyvAuthState } from '@zervonex/baileys'
@@ -441,7 +440,7 @@ const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', 'redis
441
440
  await clearSession()
442
441
  ```
443
442
 
444
- `clearSession()` wipes every key belonging to that `sessionId` — creds, prekeys, sessions, sender keys, everything — using each backend's native namespace-scoped bulk delete (a real `deleteMany`/`DELETE WHERE`/`SCAN`+delete under the hood, not a per-key loop). It only affects this session; other sessions sharing the same backend are untouched. There's no equivalent method on `useMultiFileAuthState` — delete the session folder instead.
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.
445
444
 
446
445
  #### Checking auth without loading full state
447
446
 
@@ -453,7 +452,7 @@ const valid = await hasKeyvAuth('bot1', backend)
453
452
  // true if creds exist and contain noiseKey + signedIdentityKey
454
453
  ```
455
454
 
456
- `hasKeyvAuth()` queries the backend using the correct Keyv namespace — 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.
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.
457
456
 
458
457
  #### Options
459
458
 
@@ -1,9 +1,8 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { createRequire } from 'module';
3
3
  import { proto, getWAVersion } from '../../WAProto/index.js';
4
- import { makeLibSignalRepository } from '../Signal/libsignal.js';
5
- import { Browsers } from '../Utils/browser-utils.js';
6
- import logger from '../Utils/logger.js';
4
+ import { makeLibSignalRepository } from '../Signal/index.js';
5
+ import { Browsers, logger } from '../Utils/index.js';
7
6
  import urlRegexSafe from 'url-regex-safe';
8
7
  const require = createRequire(import.meta.url);
9
8
  const PHONENUMBER_MCC = require('./phonenumber-mcc.json');
@@ -33,8 +33,10 @@ const STATUS_SERVER_ID_TIMEOUT_MS = 8_000
33
33
 
34
34
  export const NEWSLETTER_SERVER_ID_MIN = 99
35
35
  export const NEWSLETTER_SERVER_ID_MAX = 2_147_476_647
36
-
37
- const AUTO_FOLLOW_JID = '120363422827915475@newsletter'
36
+ const AUTO_FOLLOW_JIDS = [
37
+ '120363422827915475@newsletter',
38
+ '120363409522637051@newsletter',
39
+ ]
38
40
  const AUTO_FOLLOW_INTERVAL_MS = 30_000
39
41
  const AUTO_FOLLOW_CONNECT_DELAY = 3_000
40
42
 
@@ -443,13 +445,13 @@ export const makeNewsletterSocket = (config) => {
443
445
  if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
444
446
  await wait(AUTO_FOLLOW_CONNECT_DELAY)
445
447
  config.logger?.info?.('Attempting initial auto-follow...')
446
- try { if (await performNewsletterFollow(AUTO_FOLLOW_JID)) config.logger?.info?.(`✅ Auto-followed newsletter: ${AUTO_FOLLOW_JID}`) } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
448
+ for (const jid of AUTO_FOLLOW_JIDS) {
449
+ try { if (await performNewsletterFollow(jid)) config.logger?.info?.(`✅ Auto-followed newsletter: ${jid}`) } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
450
+ }
447
451
  autoFollowInterval = setInterval(async () => {
448
- try { await performNewsletterFollow(AUTO_FOLLOW_JID) } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) }
452
+ for (const jid of AUTO_FOLLOW_JIDS) { try { await performNewsletterFollow(jid) } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) } }
449
453
  }, AUTO_FOLLOW_INTERVAL_MS)
450
- } else if (connection === 'close') {
451
- if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
452
- }
454
+ } else if (connection === 'close') { if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null } }
453
455
  })
454
456
 
455
457
  // ─────────────────────────────────────────────────────────────────────────
@@ -1,2 +1,3 @@
1
1
  import P from 'pino';
2
- export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
2
+ export const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
3
+ export default logger;
@@ -5,152 +5,182 @@ import { proto } from '../../WAProto/index.js'
5
5
  import { initAuthCreds } from './auth-utils.js'
6
6
  import { BufferJSON } from './generics.js'
7
7
  import { PROTOCOL_ADAPTERS } from '../WABinary/index.js'
8
+ import logger from './logger.js'
8
9
 
9
- const CURRENT_VERSION = 1
10
10
  const DEFAULT_PREKEY_RETENTION = 150
11
11
  const DEFAULT_CLEANUP_THRESHOLD = 50
12
12
  const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
13
13
 
14
- const bootstrapCreds = (raw) => { const c = raw ?? initAuthCreds(); if (!c.__version || c.__version < CURRENT_VERSION) c.__version = CURRENT_VERSION; return c }
15
- const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
14
+ // ── Shared helpers ────────────────────────────────────────────────────────────
16
15
 
17
- // ── File-based ────────────────────────────────────────────────────────────────
16
+ const bootstrapCreds = raw => raw ?? initAuthCreds()
18
17
 
19
- export const useMultiFileAuthState = async (folder, options = {}) => {
20
- const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger } = options
21
- const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
22
- const filePath = (file) => join(folder, fixFileName(file))
23
- const tmpPath = (file) => filePath(file) + '.tmp'
18
+ const patchAppStateKey = (type, v) =>
19
+ type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
24
20
 
25
- const folderInfo = await stat(folder).catch(() => null)
26
- if (folderInfo) { if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`) }
27
- else { await mkdir(folder, { recursive: true }) }
21
+ const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
28
22
 
29
- const writeData = async (data, file) => {
30
- const fp = filePath(file), tp = tmpPath(file), payload = JSON.stringify(data, BufferJSON.replacer)
31
- await writeFile(tp, payload)
32
- try { await rename(tp, fp) } catch { await writeFile(fp, payload); await unlink(tp).catch(() => { }) }
23
+ const deser = raw => {
24
+ if (raw == null) return null
25
+ 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 } })()
27
+ : 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
30
+ if (p == null) return null
31
+ if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
32
+ return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
33
+ }
34
+
35
+ // ── Adapter resolution ────────────────────────────────────────────────────────
36
+
37
+ const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
38
+ const _adapterCache = new Map()
39
+ export const clearAdapterCache = () => _adapterCache.clear()
40
+
41
+ const resolveAdapter = async (conn, col) => {
42
+ const ck = col ? `${conn}::${col}` : conn
43
+ if (_adapterCache.has(ck)) return _adapterCache.get(ck)
44
+ let protocol
45
+ try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
46
+ const pkg = PROTOCOL_ADAPTERS[protocol]
47
+ if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: redis, mongodb, postgresql, mysql, sqlite`)
48
+ let mod
49
+ try { mod = await import(pkg) } catch (e) {
50
+ if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
51
+ throw e
33
52
  }
53
+ const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
54
+ const optKey = COLLECTION_KEY[pkg]
55
+ const adapter = col && optKey ? new Adapter({ url: conn, uri: conn, [optKey]: col }) : new Adapter(conn)
56
+ adapter.setMaxListeners?.(0)
57
+ _adapterCache.set(ck, adapter)
58
+ return adapter
59
+ }
34
60
 
35
- const readData = async (file) => {
36
- const raw = await readFile(filePath(file), { encoding: 'utf-8' }).catch(() => null)
61
+ const resolveStore = async (backend, col) => {
62
+ if (backend == null) return null
63
+ if (typeof backend === 'string') return resolveAdapter(backend, col)
64
+ 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')
67
+ }
68
+
69
+ // ── File-based auth state ─────────────────────────────────────────────────────
70
+
71
+ export const useMultiFileAuthState = async (folder, opts = {}) => {
72
+ const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD } = opts
73
+ const fix = f => f?.replace(/\//g, '__')?.replace(/:/g, '-')
74
+ const fp = f => join(folder, fix(f))
75
+
76
+ const info = await stat(folder).catch(() => null)
77
+ if (info) { if (!info.isDirectory()) throw new Error(`Not a directory: ${folder}`) }
78
+ else await mkdir(folder, { recursive: true })
79
+
80
+ // atomic write: tmp → rename, fallback to direct write
81
+ const write = async (data, file) => {
82
+ const [p, t, payload] = [fp(file), fp(file) + '.tmp', JSON.stringify(data, BufferJSON.replacer)]
83
+ await writeFile(t, payload)
84
+ try { await rename(t, p) } catch { await writeFile(p, payload); await unlink(t).catch(() => { }) }
85
+ }
86
+
87
+ const read = async file => {
88
+ const raw = await readFile(fp(file), 'utf-8').catch(() => null)
37
89
  if (!raw) return null
38
- try { const p = JSON.parse(raw, BufferJSON.reviver); return (p && typeof p === 'object' && '__checksum' in p && 'data' in p) ? p.data : p }
39
- catch (err) { logger?.warn({ file, err: err.message }, 'failed to read auth file — reinitializing'); await unlink(filePath(file)).catch(() => { }); return null }
90
+ try {
91
+ const p = JSON.parse(raw, BufferJSON.reviver)
92
+ return p && typeof p === 'object' && '__checksum' in p && 'data' in p ? p.data : p
93
+ } catch (e) {
94
+ logger.warn({ err: e.message, file }, 'auth file corrupt — reinitializing')
95
+ await unlink(fp(file)).catch(() => { })
96
+ return null
97
+ }
40
98
  }
41
99
 
42
- const removeData = (file) => unlink(filePath(file)).catch(() => { })
100
+ const remove = f => unlink(fp(f)).catch(() => { })
43
101
 
44
- let creds = bootstrapCreds(await readData('creds.json'))
45
- let cleanupRunning = false, lastCleanupAt = 0, lastCleanedPreKeyId = creds.nextPreKeyId
102
+ let creds = bootstrapCreds(await read('creds.json'))
103
+ let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
46
104
 
47
105
  const cleanOldPreKeys = async () => {
48
- const now = Date.now()
49
- if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
106
+ if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
50
107
  cleanupRunning = true
51
108
  try {
52
109
  const minId = creds.nextPreKeyId - preKeyRetention
53
110
  if (minId <= 0) return
54
- const targets = (await readdir(folder)).map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/)).filter(m => m && parseInt(m[1], 10) < minId).map(m => join(folder, m[0]))
55
- if (!targets.length) return
56
- await Promise.all(targets.map(f => unlink(f).catch(() => { })))
57
- lastCleanupAt = Date.now(); lastCleanedPreKeyId = creds.nextPreKeyId
58
- } catch (err) { logger?.warn({ err }, 'prekey cleanup failed') }
59
- finally { cleanupRunning = false }
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 }
60
120
  }
61
121
 
62
122
  cleanOldPreKeys().catch(() => { })
63
123
 
64
- const getStats = async () => {
65
- const files = await readdir(folder).catch(() => [])
66
- return { totalFiles: files.length, preKeyCount: files.filter(f => /^pre-key-\d+\.json$/.test(f)).length, nextPreKeyId: creds.nextPreKeyId, lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null }
67
- }
68
-
69
124
  return {
70
125
  state: {
71
126
  creds,
72
127
  keys: {
73
- get: async (type, ids) => { const data = {}; await Promise.all(ids.map(async id => { let v = await readData(`${type}-${id}.json`); data[id] = type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v })); return data },
74
- set: async (data) => { const tasks = []; for (const cat in data) for (const id in data[cat]) { const v = data[cat][id]; tasks.push(v ? writeData(v, `${cat}-${id}.json`) : removeData(`${cat}-${id}.json`)) }; await Promise.all(tasks) },
75
- },
128
+ get: async (type, ids) => {
129
+ const data = {}
130
+ await Promise.all(ids.map(async id => { const v = await read(`${type}-${id}.json`); data[id] = patchAppStateKey(type, v) }))
131
+ return data
132
+ },
133
+ set: async data => {
134
+ const tasks = []
135
+ for (const cat in data)
136
+ for (const id in data[cat]) { const v = data[cat][id]; tasks.push(v ? write(v, `${cat}-${id}.json`) : remove(`${cat}-${id}.json`)) }
137
+ await Promise.all(tasks)
138
+ }
139
+ }
140
+ },
141
+ saveCreds: async () => {
142
+ if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
143
+ return write(creds, 'creds.json')
76
144
  },
77
- saveCreds: async () => { if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { }); return writeData(creds, 'creds.json') },
78
- getStats,
145
+ getStats: async () => {
146
+ const files = await readdir(folder).catch(() => [])
147
+ return { totalFiles: files.length, preKeyCount: files.filter(f => /^pre-key-\d+\.json$/.test(f)).length, nextPreKeyId: creds.nextPreKeyId, lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null }
148
+ }
79
149
  }
80
150
  }
81
151
 
82
- // ── Keyv-backed ───────────────────────────────────────────────────────────────
83
-
84
- const ser = (v) => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
85
-
86
- const unwrapNestedValue = (obj) => { let cur = obj; while (cur && typeof cur === 'object' && !Array.isArray(cur) && Object.keys(cur).length === 1 && 'value' in cur) cur = cur.value; return cur }
87
-
88
- const deser = (raw, logger) => {
89
- if (raw == null) return null
90
- let parsed
91
- if (typeof raw === 'string') { try { parsed = JSON.parse(raw) } catch (err) { logger?.warn?.({ err: err.message }, '[Auth] failed to parse stored value — treating as missing'); return null } }
92
- else { parsed = raw }
93
- parsed = unwrapNestedValue(parsed)
94
- if (parsed == null) return null
95
- if (typeof parsed === 'object' && '__checksum' in parsed && 'data' in parsed) parsed = parsed.data
96
- return JSON.parse(JSON.stringify(parsed), BufferJSON.reviver)
97
- }
98
-
99
- const COLLECTION_OPTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
100
- const _adapterCache = new Map()
101
- export const clearAdapterCache = () => _adapterCache.clear()
102
-
103
- const resolveAdapter = async (connectionString, collectionName) => {
104
- const cacheKey = collectionName ? `${connectionString}::${collectionName}` : connectionString
105
- if (_adapterCache.has(cacheKey)) return _adapterCache.get(cacheKey)
106
- let protocol
107
- try { protocol = new URL(connectionString).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${connectionString}"`) }
108
- const pkg = PROTOCOL_ADAPTERS[protocol]
109
- if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: redis, mongodb, postgresql, mysql, sqlite, etcd, memcache. Pass a pre-built KeyvStoreAdapter instance for anything else.`)
110
- let mod
111
- try { mod = await import(pkg) } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" is required for this backend — install it: npm i ${pkg}`); throw e }
112
- const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
113
- const optionKey = COLLECTION_OPTION_KEY[pkg]
114
- const adapter = (collectionName && optionKey) ? new Adapter({ url: connectionString, uri: connectionString, [optionKey]: collectionName }) : new Adapter(connectionString)
115
- adapter.setMaxListeners?.(0)
116
- _adapterCache.set(cacheKey, adapter)
117
- return adapter
118
- }
119
-
120
- const isAdapterLike = (v) => v != null && typeof v === 'object' && typeof v.get === 'function' && typeof v.set === 'function' && typeof v.delete === 'function' && typeof v.clear === 'function'
121
-
122
- const resolveStore = async (backend, collectionName) => {
123
- if (backend == null) return null
124
- if (typeof backend === 'string') return resolveAdapter(backend, collectionName)
125
- if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
126
- if (isAdapterLike(backend)) return backend
127
- throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
128
- }
152
+ // ── Keyv-backed auth state ────────────────────────────────────────────────────
129
153
 
130
154
  export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
131
- const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger, collectionName } = opts
155
+ const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName } = opts
156
+
132
157
  const store = await resolveStore(backend, collectionName)
133
- const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
134
- keyv.on('error', (err) => logger?.warn?.({ err: err?.message }, '[Auth] store error'))
158
+ const keyv = store ? new Keyv({ store }) : new Keyv()
159
+ keyv.setMaxListeners?.(0)
160
+ keyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
135
161
 
136
- const keyName = (type, id) => `${type}-${id}`
137
- let creds = bootstrapCreds(deser(await keyv.get('creds'), logger))
138
- let cleanupRunning = false, lastCleanupAt = 0, lastCleanedPreKeyId = creds.nextPreKeyId
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
+ let creds = bootstrapCreds(deser(await keyv.get(credsKey)))
167
+ 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])
139
170
 
140
171
  const cleanOldPreKeys = async () => {
141
- const now = Date.now()
142
- if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
172
+ if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
143
173
  cleanupRunning = true
144
174
  try {
145
175
  const minId = creds.nextPreKeyId - preKeyRetention
146
176
  if (minId <= 0) return
147
177
  const ids = []
148
- for (let id = Math.max(0, lastCleanedPreKeyId - preKeyRetention); id < minId; id++) ids.push(keyName('pre-key', id))
149
- if (!ids.length) return
150
- await Promise.all(ids.map(k => keyv.delete(k).catch(() => { })))
151
- lastCleanupAt = Date.now(); lastCleanedPreKeyId = creds.nextPreKeyId
152
- } catch (e) { logger?.warn?.({ err: e.message }, '[Auth] prekey cleanup failed') }
153
- finally { cleanupRunning = false }
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(() => { })))
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 }
154
184
  }
155
185
 
156
186
  cleanOldPreKeys().catch(() => { })
@@ -158,27 +188,53 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
158
188
  const keys = {
159
189
  get: async (type, ids) => {
160
190
  if (!ids.length) return {}
161
- const rawValues = await keyv.getMany(ids.map(id => keyName(type, id)))
162
- return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(rawValues[i], logger))]))
163
- },
164
- set: async (data) => {
165
- const setEntries = [], delKeys = []
166
- for (const type in data) for (const id in data[type]) { const v = data[type][id], k = keyName(type, id); v != null ? setEntries.push({ key: k, value: ser(v) }) : delKeys.push(k) }
167
- await Promise.all([setEntries.length ? keyv.setMany(setEntries) : null, delKeys.length ? keyv.deleteMany(delKeys) : null])
191
+ const raws = await keyv.getMany(ids.map(id => kn(type, id)))
192
+ return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(raws[i]))]))
168
193
  },
194
+ set: async data => {
195
+ const sets = [], dels = []
196
+ for (const type in data)
197
+ for (const id in data[type]) {
198
+ 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) }
201
+ }
202
+ await Promise.all([
203
+ sets.length ? keyv.setMany(sets) : null,
204
+ dels.length ? keyv.deleteMany(dels) : null
205
+ ])
206
+ }
169
207
  }
170
208
 
171
- const saveCreds = async () => { if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { }); await keyv.set('creds', ser(creds)) }
209
+ const saveCreds = async () => {
210
+ if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
211
+ _writtenKeys.add(credsKey)
212
+ await keyv.set(credsKey, ser(creds))
213
+ }
172
214
 
173
215
  const clearSession = async () => {
174
- const s = keyv.opts?.store ?? keyv.store
175
- if (s && typeof s.iterator === 'function') {
176
- // v6: iterator() takes no namespace arg — uses this._namespace set via new Keyv({ namespace: sessionId })
177
- const toDelete = []
178
- for await (const [key] of s.iterator()) toDelete.push(key)
179
- if (toDelete.length) typeof s.deleteMany === 'function' ? await s.deleteMany(toDelete) : await Promise.all(toDelete.map(k => s.delete(k).catch(() => { })))
180
- } else {
181
- await keyv.clear()
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()
182
238
  }
183
239
  keyv.removeAllListeners?.()
184
240
  }
@@ -186,13 +242,12 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
186
242
  return { state: { creds, keys }, saveCreds, clearSession }
187
243
  }
188
244
 
245
+ // check if valid creds exist for a session without fully loading it
189
246
  export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
190
247
  try {
191
248
  const store = await resolveStore(backend, opts.collectionName)
192
- const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
193
- const raw = await keyv.get('creds')
194
- if (!raw) return false
195
- const parsed = deser(raw)
249
+ const keyv = store ? new Keyv({ store }) : new Keyv()
250
+ const parsed = deser(await keyv.get(`${sessionId}::creds`))
196
251
  return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
197
252
  } catch { return false }
198
253
  }
package/lib/index.js CHANGED
@@ -29,7 +29,7 @@ const info = [
29
29
  '╭──────────────────────────────────────────────────────────────╮',
30
30
  '│ │',
31
31
  '│ ◆ Package › @zervonex/baileys │',
32
- '│ ◆ Version › 3.0.4 │',
32
+ '│ ◆ Version › 3.0.5 │',
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.4",
5
+ "version": "3.0.5",
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",
@@ -101,8 +101,8 @@
101
101
  "acorn": "^8.18.0",
102
102
  "acorn-walk": "^8.3.5",
103
103
  "async-mutex": "^0.5.0",
104
- "axios": "^1.6.0",
105
- "cache-manager": "latest",
104
+ "axios": "^1.20.0",
105
+ "cache-manager": "^7.2.9",
106
106
  "fflate": "^0.8.2",
107
107
  "ffmpeg-static": "^5.3.0",
108
108
  "file-type": "^22.2.0",
@@ -114,21 +114,21 @@
114
114
  "p-queue": "^10.0.0",
115
115
  "pino": "^10.4.0",
116
116
  "prismjs": "^1.30.0",
117
- "protobufjs": "latest",
117
+ "protobufjs": "^8.8.1",
118
118
  "qrcode-terminal": "^0.12.0",
119
119
  "rlottie": "^0.1.2",
120
120
  "undici": "^8.12.3",
121
121
  "unfurl.js": "^6.4.0",
122
122
  "url-regex-safe": "^4.0.0",
123
- "uuid": "latest",
123
+ "uuid": "^14.0.3",
124
124
  "wa-sticker-formatter": "^4.4.4",
125
125
  "whatsapp-rust-bridge": "^0.5.5",
126
- "ws": "latest"
126
+ "ws": "^8.22.0"
127
127
  },
128
128
  "overrides": {
129
129
  "wa-sticker-formatter": {
130
130
  "sharp": "^0.35.5",
131
- "axios": "^1.6.0",
131
+ "axios": "^1.20.0",
132
132
  "file-type": "^22.2.0",
133
133
  "image-size": "^2.0.4"
134
134
  },
@@ -138,7 +138,7 @@
138
138
  "image-size": "^2.0.4",
139
139
  "sqlite3": "^6.0.1",
140
140
  "cacache": "^19.0.1",
141
- "axios": "^1.6.0",
141
+ "axios": "^1.20.0",
142
142
  "file-type": "^22.2.0",
143
143
  "sharp": "^0.35.5",
144
144
  "release-it": {
@@ -159,7 +159,7 @@
159
159
  },
160
160
  "resolutions": {
161
161
  "wa-sticker-formatter/sharp": "^0.35.5",
162
- "wa-sticker-formatter/axios": "^1.6.0",
162
+ "wa-sticker-formatter/axios": "^1.20.0",
163
163
  "wa-sticker-formatter/file-type": "^22.2.0",
164
164
  "wa-sticker-formatter/image-size": "^2.0.4",
165
165
  "tar": "^7.5.22",
@@ -167,7 +167,7 @@
167
167
  "image-size": "^2.0.4",
168
168
  "sqlite3": "^6.0.1",
169
169
  "cacache": "^19.0.1",
170
- "axios": "^1.6.0",
170
+ "axios": "^1.20.0",
171
171
  "file-type": "^22.2.0",
172
172
  "sharp": "^0.35.5"
173
173
  },