@zervonex/baileys 3.0.9 → 4.0.0
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 +1 -0
- package/lib/Defaults/index.js +4 -0
- package/lib/Utils/auth-utils.js +19 -2
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/use-keyv-auth-state.js +169 -0
- package/lib/Utils/use-multi-file-auth-state.js +3 -194
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/lib/Defaults/index.js
CHANGED
|
@@ -208,6 +208,10 @@ export const INITIAL_PREKEY_COUNT = 95;
|
|
|
208
208
|
export const UPLOAD_TIMEOUT = 30000;
|
|
209
209
|
export const MIN_UPLOAD_INTERVAL = 5000;
|
|
210
210
|
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120000;
|
|
211
|
+
export const DEFAULT_PREKEY_RETENTION = 150
|
|
212
|
+
export const DEFAULT_CLEANUP_THRESHOLD = 50
|
|
213
|
+
export const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
|
|
214
|
+
export const DEFAULT_COLLECTION_NAME = 'zervonex'
|
|
211
215
|
export const DEFAULT_CACHE_TTLS = {
|
|
212
216
|
SIGNAL_STORE: 5 * 60,
|
|
213
217
|
MSG_RETRY: 60 * 60,
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -6,8 +6,9 @@ import PQueue from 'p-queue'
|
|
|
6
6
|
import { v4 } from 'uuid'
|
|
7
7
|
import { DEFAULT_CACHE_TTLS } from '../Defaults/index.js'
|
|
8
8
|
import { Curve, signedKeyPair } from './crypto.js'
|
|
9
|
-
import { delay, generateRegistrationId } from './generics.js'
|
|
9
|
+
import { delay, generateRegistrationId, BufferJSON } from './generics.js'
|
|
10
10
|
import { PreKeyManager } from './pre-key-manager.js'
|
|
11
|
+
import { proto } from '../../WAProto/index.js'
|
|
11
12
|
|
|
12
13
|
export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
13
14
|
const cache = _cache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, useClones: false, deleteOnExpire: true })
|
|
@@ -139,4 +140,20 @@ export const initAuthCreds = () => {
|
|
|
139
140
|
routingInfo: undefined,
|
|
140
141
|
additionalData: undefined
|
|
141
142
|
}
|
|
142
|
-
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
export const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
147
|
+
export const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
148
|
+
export const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
149
|
+
export const deser = raw => {
|
|
150
|
+
if (raw == null) return null
|
|
151
|
+
let p = typeof raw === 'string'
|
|
152
|
+
? (() => { try { return JSON.parse(raw) } catch { return raw } })()
|
|
153
|
+
: raw
|
|
154
|
+
let depth = 0
|
|
155
|
+
while (depth++ < 3 && p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
|
|
156
|
+
if (p == null) return null
|
|
157
|
+
if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
|
|
158
|
+
return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
|
|
159
|
+
}
|
package/lib/Utils/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './lt-hash.js'
|
|
|
14
14
|
export * from './auth-utils.js'
|
|
15
15
|
export * from './baileys-event-stream.js'
|
|
16
16
|
export * from './use-multi-file-auth-state.js'
|
|
17
|
+
export * from './use-keyv-auth-state.js'
|
|
17
18
|
export * from './link-preview.js'
|
|
18
19
|
export * from './event-buffer.js'
|
|
19
20
|
export * from './process-message.js'
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Keyv, detectKeyvStorage } from 'keyv'
|
|
2
|
+
import { DEFAULT_PREKEY_RETENTION, DEFAULT_CLEANUP_THRESHOLD, CLEANUP_INTERVAL_MS, DEFAULT_COLLECTION_NAME } from '../Defaults/index.js'
|
|
3
|
+
import { bootstrapCreds, patchAppStateKey, ser, deser } from './auth-utils.js'
|
|
4
|
+
import { PROTOCOL_ADAPTERS } from '../WABinary/index.js'
|
|
5
|
+
import logger from './logger.js'
|
|
6
|
+
|
|
7
|
+
// ── Adapter resolution ────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
// Only these adapters accept a named collection/table — redis, valkey, etcd etc do not
|
|
10
|
+
const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
|
|
11
|
+
|
|
12
|
+
// Cached per conn::col::namespace — each session gets its own adapter instance so
|
|
13
|
+
// clear() never bleeds across sessions. Underlying drivers manage their own connection
|
|
14
|
+
// pools so multiple adapters on the same URI still share one real connection.
|
|
15
|
+
const _adapterCache = new Map()
|
|
16
|
+
export const clearAdapterCache = () => _adapterCache.clear()
|
|
17
|
+
|
|
18
|
+
const resolveAdapter = async (conn, col, namespace) => {
|
|
19
|
+
const ck = `${conn}::${col ?? ''}::${namespace ?? ''}`
|
|
20
|
+
if (_adapterCache.has(ck)) return _adapterCache.get(ck)
|
|
21
|
+
let protocol
|
|
22
|
+
try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
|
|
23
|
+
const pkg = PROTOCOL_ADAPTERS[protocol]
|
|
24
|
+
if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: ${Object.keys(PROTOCOL_ADAPTERS).map(p => p.replace(':', '')).join(', ')}`)
|
|
25
|
+
let mod
|
|
26
|
+
try { mod = await import(pkg) } catch (e) {
|
|
27
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
|
|
28
|
+
throw e
|
|
29
|
+
}
|
|
30
|
+
const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
|
|
31
|
+
const optKey = COLLECTION_KEY[pkg]
|
|
32
|
+
const adapter = col && optKey ? new Adapter({ url: conn, uri: conn, [optKey]: col }) : new Adapter(conn)
|
|
33
|
+
adapter.setMaxListeners?.(0)
|
|
34
|
+
if (namespace) adapter.namespace = namespace
|
|
35
|
+
_adapterCache.set(ck, adapter)
|
|
36
|
+
return adapter
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolveStore = async (backend, col, namespace) => {
|
|
40
|
+
if (backend == null) return null
|
|
41
|
+
if (typeof backend === 'string') return resolveAdapter(backend, col, namespace)
|
|
42
|
+
if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
|
|
43
|
+
const { compatible } = detectKeyvStorage(backend)
|
|
44
|
+
if (!compatible) throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
|
|
45
|
+
// scope namespace via prototype so we don't mutate the original store instance
|
|
46
|
+
const scoped = Object.create(backend)
|
|
47
|
+
if (namespace) scoped.namespace = namespace
|
|
48
|
+
return scoped
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const makeKeyv = (store, namespace) => {
|
|
52
|
+
const keyv = store ? new Keyv({ store, namespace }) : new Keyv({ namespace })
|
|
53
|
+
keyv.setMaxListeners?.(0)
|
|
54
|
+
return keyv
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Multi-backend support ─────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// Multi-backend map: { 'creds': pgBackend, 'pre-key': mongoBackend, ... }
|
|
60
|
+
// Anything else (string, Keyv, raw store adapter) is treated as single backend
|
|
61
|
+
const isMultiBackend = backend =>
|
|
62
|
+
backend !== null
|
|
63
|
+
&& typeof backend === 'object'
|
|
64
|
+
&& !Array.isArray(backend)
|
|
65
|
+
&& !(backend instanceof Keyv)
|
|
66
|
+
&& !detectKeyvStorage(backend).compatible
|
|
67
|
+
|
|
68
|
+
// Returns { get(type) → Keyv, all → Keyv[] }
|
|
69
|
+
// get(type) falls back to first entry if type not in map
|
|
70
|
+
// all is deduplicated — shared backends only clear once
|
|
71
|
+
const resolveBackendMap = async (backend, sessionId, collectionName) => {
|
|
72
|
+
if (!isMultiBackend(backend)) {
|
|
73
|
+
const store = await resolveStore(backend, collectionName, sessionId)
|
|
74
|
+
const keyv = makeKeyv(store, sessionId)
|
|
75
|
+
return { get: () => keyv, all: [keyv] }
|
|
76
|
+
}
|
|
77
|
+
const entries = Object.entries(backend)
|
|
78
|
+
if (!entries.length) throw new Error('[Auth] backend map must have at least one entry')
|
|
79
|
+
const resolved = new Map()
|
|
80
|
+
for (const [, b] of entries) {
|
|
81
|
+
if (!resolved.has(b)) {
|
|
82
|
+
const store = await resolveStore(b, collectionName, sessionId)
|
|
83
|
+
resolved.set(b, makeKeyv(store, sessionId))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const typeMap = Object.fromEntries(entries.map(([type, b]) => [type, resolved.get(b)]))
|
|
87
|
+
const defaultKeyv = resolved.get(entries[0][1])
|
|
88
|
+
return { get: type => typeMap[type] ?? defaultKeyv, all: [...new Set(resolved.values())] }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Keyv-backed auth state ────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
94
|
+
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName = DEFAULT_COLLECTION_NAME } = opts
|
|
95
|
+
const bmap = await resolveBackendMap(backend, sessionId, collectionName)
|
|
96
|
+
const credsKeyv = bmap.get('creds')
|
|
97
|
+
credsKeyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
|
|
98
|
+
|
|
99
|
+
const kn = (type, id) => `${type}-${id}`
|
|
100
|
+
const credsKey = 'creds'
|
|
101
|
+
let creds = bootstrapCreds(deser(await credsKeyv.get(credsKey)))
|
|
102
|
+
let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
|
|
103
|
+
|
|
104
|
+
const cleanOldPreKeys = async () => {
|
|
105
|
+
if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
106
|
+
cleanupRunning = true
|
|
107
|
+
try {
|
|
108
|
+
const minId = creds.nextPreKeyId - preKeyRetention
|
|
109
|
+
if (minId <= 0) return
|
|
110
|
+
const ids = []
|
|
111
|
+
for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
|
|
112
|
+
if (ids.length) await bmap.get('pre-key').deleteMany(ids)
|
|
113
|
+
lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
|
|
114
|
+
} catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
|
|
115
|
+
finally { cleanupRunning = false }
|
|
116
|
+
}
|
|
117
|
+
cleanOldPreKeys().catch(() => { })
|
|
118
|
+
|
|
119
|
+
const keys = {
|
|
120
|
+
get: async (type, ids) => {
|
|
121
|
+
if (!ids.length) return {}
|
|
122
|
+
const keyv = bmap.get(type)
|
|
123
|
+
const raws = await keyv.getMany(ids.map(id => kn(type, id)))
|
|
124
|
+
return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(raws[i]))]))
|
|
125
|
+
},
|
|
126
|
+
set: async data => {
|
|
127
|
+
// group by Keyv instance to batch writes per backend
|
|
128
|
+
const byKeyv = new Map()
|
|
129
|
+
for (const type in data) {
|
|
130
|
+
const keyv = bmap.get(type)
|
|
131
|
+
if (!byKeyv.has(keyv)) byKeyv.set(keyv, { sets: [], dels: [] })
|
|
132
|
+
const bucket = byKeyv.get(keyv)
|
|
133
|
+
for (const id in data[type]) {
|
|
134
|
+
const v = data[type][id], k = kn(type, id)
|
|
135
|
+
if (v != null) bucket.sets.push({ key: k, value: ser(v) })
|
|
136
|
+
else bucket.dels.push(k)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await Promise.all([...byKeyv.entries()].map(([keyv, { sets, dels }]) =>
|
|
140
|
+
Promise.all([sets.length ? keyv.setMany(sets) : null, dels.length ? keyv.deleteMany(dels) : null])
|
|
141
|
+
))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const saveCreds = async () => {
|
|
146
|
+
if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
147
|
+
await credsKeyv.set(credsKey, ser(creds))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const clearSession = async () => {
|
|
151
|
+
try { await Promise.all(bmap.all.map(keyv => keyv.clear())) }
|
|
152
|
+
catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] clearSession failed') }
|
|
153
|
+
bmap.all.forEach(keyv => keyv.removeAllListeners?.())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { state: { creds, keys }, saveCreds, clearSession }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Lightweight check — reads only the creds key without full state init
|
|
160
|
+
export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
|
|
161
|
+
try {
|
|
162
|
+
const b = isMultiBackend(backend) ? Object.values(backend)[0] : backend
|
|
163
|
+
const bmap = await resolveBackendMap(b, sessionId, opts.collectionName ?? DEFAULT_COLLECTION_NAME)
|
|
164
|
+
const parsed = deser(await bmap.get('creds').get('creds'))
|
|
165
|
+
return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
|
|
166
|
+
} catch { return false }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { Keyv }
|
|
@@ -1,121 +1,10 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, stat, unlink, writeFile, readdir } from 'fs/promises'
|
|
2
2
|
import { join } from 'path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { initAuthCreds } from './auth-utils.js'
|
|
3
|
+
import { DEFAULT_PREKEY_RETENTION, DEFAULT_CLEANUP_THRESHOLD, CLEANUP_INTERVAL_MS } from '../Defaults/index.js'
|
|
4
|
+
import { bootstrapCreds, patchAppStateKey } from './auth-utils.js'
|
|
6
5
|
import { BufferJSON } from './generics.js'
|
|
7
|
-
import { PROTOCOL_ADAPTERS } from '../WABinary/index.js'
|
|
8
6
|
import logger from './logger.js'
|
|
9
7
|
|
|
10
|
-
const DEFAULT_PREKEY_RETENTION = 150
|
|
11
|
-
const DEFAULT_CLEANUP_THRESHOLD = 50
|
|
12
|
-
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
|
|
13
|
-
const DEFAULT_COLLECTION_NAME = 'zervonex'
|
|
14
|
-
|
|
15
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
18
|
-
const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
19
|
-
const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
20
|
-
const deser = raw => {
|
|
21
|
-
if (raw == null) return null
|
|
22
|
-
let p = typeof raw === 'string'
|
|
23
|
-
? (() => { try { return JSON.parse(raw) } catch { return raw } })() // ← return raw string on parse fail, don't warn
|
|
24
|
-
: raw
|
|
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
|
|
28
|
-
if (p == null) return null
|
|
29
|
-
if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
|
|
30
|
-
return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── Adapter resolution ────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
// Only these adapters accept a named collection/table — redis, valkey, etcd etc do not
|
|
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.
|
|
41
|
-
const _adapterCache = new Map()
|
|
42
|
-
export const clearAdapterCache = () => _adapterCache.clear()
|
|
43
|
-
|
|
44
|
-
const resolveAdapter = async (conn, col, namespace) => {
|
|
45
|
-
const ck = `${conn}::${col ?? ''}::${namespace ?? ''}`
|
|
46
|
-
if (_adapterCache.has(ck)) return _adapterCache.get(ck)
|
|
47
|
-
let protocol
|
|
48
|
-
try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
|
|
49
|
-
const pkg = PROTOCOL_ADAPTERS[protocol]
|
|
50
|
-
if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: ${Object.keys(PROTOCOL_ADAPTERS).map(p => p.replace(':', '')).join(', ')}`)
|
|
51
|
-
let mod
|
|
52
|
-
try { mod = await import(pkg) } catch (e) {
|
|
53
|
-
if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
|
|
54
|
-
throw e
|
|
55
|
-
}
|
|
56
|
-
const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
|
|
57
|
-
const optKey = COLLECTION_KEY[pkg]
|
|
58
|
-
const adapter = col && optKey ? new Adapter({ url: conn, uri: conn, [optKey]: col }) : new Adapter(conn)
|
|
59
|
-
adapter.setMaxListeners?.(0)
|
|
60
|
-
if (namespace) adapter.namespace = namespace
|
|
61
|
-
_adapterCache.set(ck, adapter)
|
|
62
|
-
return adapter
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const resolveStore = async (backend, col, namespace) => {
|
|
66
|
-
if (backend == null) return null
|
|
67
|
-
if (typeof backend === 'string') return resolveAdapter(backend, col, namespace)
|
|
68
|
-
if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
|
|
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())] }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── File-based auth state ─────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
8
|
export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
120
9
|
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD } = opts
|
|
121
10
|
const fix = f => f?.replace(/\//g, '__')?.replace(/:/g, '-')
|
|
@@ -192,84 +81,4 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
|
192
81
|
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 }
|
|
193
82
|
}
|
|
194
83
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ── Keyv-backed auth state ────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
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'))
|
|
204
|
-
|
|
205
|
-
const kn = (type, id) => `${type}-${id}`
|
|
206
|
-
const credsKey = 'creds'
|
|
207
|
-
let creds = bootstrapCreds(deser(await credsKeyv.get(credsKey)))
|
|
208
|
-
let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
|
|
209
|
-
|
|
210
|
-
const cleanOldPreKeys = async () => {
|
|
211
|
-
if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
212
|
-
cleanupRunning = true
|
|
213
|
-
try {
|
|
214
|
-
const minId = creds.nextPreKeyId - preKeyRetention
|
|
215
|
-
if (minId <= 0) return
|
|
216
|
-
const ids = []
|
|
217
|
-
for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
|
|
218
|
-
if (ids.length) await bmap.get('pre-key').deleteMany(ids)
|
|
219
|
-
lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
|
|
220
|
-
} catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
|
|
221
|
-
finally { cleanupRunning = false }
|
|
222
|
-
}
|
|
223
|
-
cleanOldPreKeys().catch(() => { })
|
|
224
|
-
|
|
225
|
-
const keys = {
|
|
226
|
-
get: async (type, ids) => {
|
|
227
|
-
if (!ids.length) return {}
|
|
228
|
-
const keyv = bmap.get(type)
|
|
229
|
-
const raws = await keyv.getMany(ids.map(id => kn(type, id)))
|
|
230
|
-
return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(raws[i]))]))
|
|
231
|
-
},
|
|
232
|
-
set: async 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)
|
|
239
|
-
for (const id in data[type]) {
|
|
240
|
-
const v = data[type][id], k = kn(type, id)
|
|
241
|
-
if (v != null) bucket.sets.push({ key: k, value: ser(v) })
|
|
242
|
-
else bucket.dels.push(k)
|
|
243
|
-
}
|
|
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
|
-
))
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const saveCreds = async () => {
|
|
252
|
-
if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
253
|
-
await credsKeyv.set(credsKey, ser(creds))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const clearSession = async () => {
|
|
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?.())
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return { state: { creds, keys }, saveCreds, clearSession }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Lightweight check — reads only the creds key without full state init
|
|
266
|
-
export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
|
|
267
|
-
try {
|
|
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'))
|
|
271
|
-
return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
|
|
272
|
-
} catch { return false }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export { Keyv }
|
|
84
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const info = [
|
|
|
29
29
|
'╭──────────────────────────────────────────────────────────────╮',
|
|
30
30
|
'│ │',
|
|
31
31
|
'│ ◆ Package › @zervonex/baileys │',
|
|
32
|
-
'│ ◆ Version ›
|
|
32
|
+
'│ ◆ Version › 4.0.0 │',
|
|
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": "
|
|
5
|
+
"version": "4.0.0",
|
|
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",
|