@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 +3 -4
- package/lib/Defaults/index.js +2 -3
- package/lib/Socket/newsletter.js +9 -7
- package/lib/Utils/logger.js +2 -1
- package/lib/Utils/use-multi-file-auth-state.js +177 -122
- package/lib/index.js +1 -1
- package/package.json +10 -10
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
|
|
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 —
|
|
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
|
|
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
|
|
package/lib/Defaults/index.js
CHANGED
|
@@ -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/
|
|
5
|
-
import { Browsers } from '../Utils/
|
|
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');
|
package/lib/Socket/newsletter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────
|
package/lib/Utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
14
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
try {
|
|
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
|
-
|
|
36
|
-
|
|
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 {
|
|
39
|
-
|
|
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
|
|
100
|
+
const remove = f => unlink(fp(f)).catch(() => { })
|
|
43
101
|
|
|
44
|
-
let creds = bootstrapCreds(await
|
|
45
|
-
let cleanupRunning = false, lastCleanupAt = 0,
|
|
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
|
-
|
|
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))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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) => {
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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,
|
|
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
|
|
134
|
-
keyv.
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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,
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
162
|
-
return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(
|
|
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 () => {
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
193
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
105
|
-
"cache-manager": "
|
|
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": "
|
|
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": "
|
|
123
|
+
"uuid": "^14.0.3",
|
|
124
124
|
"wa-sticker-formatter": "^4.4.4",
|
|
125
125
|
"whatsapp-rust-bridge": "^0.5.5",
|
|
126
|
-
"ws": "
|
|
126
|
+
"ws": "^8.22.0"
|
|
127
127
|
},
|
|
128
128
|
"overrides": {
|
|
129
129
|
"wa-sticker-formatter": {
|
|
130
130
|
"sharp": "^0.35.5",
|
|
131
|
-
"axios": "^1.
|
|
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.
|
|
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.
|
|
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.
|
|
170
|
+
"axios": "^1.20.0",
|
|
171
171
|
"file-type": "^22.2.0",
|
|
172
172
|
"sharp": "^0.35.5"
|
|
173
173
|
},
|