@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 +65 -25
- package/lib/Utils/use-multi-file-auth-state.js +110 -67
- package/lib/index.js +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
360
|
-
cleanupThreshold:
|
|
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
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
const
|
|
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
|
-
|
|
448
|
+
#### Custom collection/table name
|
|
423
449
|
|
|
424
|
-
By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv
|
|
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',
|
|
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
|
-
|
|
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
|
-
//
|
|
440
|
-
await clearSession()
|
|
483
|
+
await clearSession() // wipes all keys for this session only — other sessions untouched
|
|
441
484
|
```
|
|
442
485
|
|
|
443
|
-
`clearSession()`
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
// ──
|
|
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
|
|
23
|
+
? (() => { try { return JSON.parse(raw) } catch { return raw } })() // ← return raw string on parse fail, don't warn
|
|
27
24
|
: raw
|
|
28
|
-
//
|
|
29
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
148
|
+
let cleanupPromise = null, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
|
|
104
149
|
|
|
105
150
|
const cleanOldPreKeys = async () => {
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
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
|
|
158
|
-
|
|
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
|
|
218
|
+
if (ids.length) await bmap.get('pre-key').deleteMany(ids)
|
|
179
219
|
lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
|
|
180
|
-
} catch (e) {
|
|
181
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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)
|
|
199
|
-
else
|
|
241
|
+
if (v != null) bucket.sets.push({ key: k, value: ser(v) })
|
|
242
|
+
else bucket.dels.push(k)
|
|
200
243
|
}
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
226
|
-
const
|
|
227
|
-
const parsed = deser(await
|
|
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.
|
|
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.
|
|
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",
|