@zervonex/baileys 3.0.3 → 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 +33 -3
- package/WAProto/WAProtoCompile.js +1 -0
- package/WAProto/fetcher.js +17 -33
- package/WAProto/index.js +34 -26
- package/WAProto/parser.js +333 -312
- package/lib/Builders/AIRich.js +14 -14
- package/lib/Builders/base.js +3 -3
- package/lib/Builders/builders.js +460 -448
- package/lib/Defaults/index.js +2 -3
- package/lib/Socket/messages-recv.js +1 -1
- package/lib/Socket/messages-send.js +1 -1
- package/lib/Socket/newsletter.js +9 -7
- package/lib/Socket/socket.js +4 -3
- package/lib/Socket/zervonex-handler.js +1 -1
- package/lib/Utils/browser-utils.js +8 -4
- package/lib/Utils/chat-utils.js +1 -1
- package/lib/Utils/logger.js +2 -1
- package/lib/Utils/messages.js +91 -40
- package/lib/Utils/use-multi-file-auth-state.js +155 -207
- package/lib/Utils/validate-connection.js +19 -28
- package/lib/WABinary/generic-utils.js +43 -19
- package/lib/index.js +1 -1
- package/package.json +62 -97
|
@@ -5,74 +5,105 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
return c
|
|
18
|
-
}
|
|
14
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
19
17
|
|
|
20
18
|
const patchAppStateKey = (type, v) =>
|
|
21
19
|
type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
22
|
+
|
|
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
|
+
}
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger } = options
|
|
35
|
+
// ── Adapter resolution ────────────────────────────────────────────────────────
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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()
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
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
|
+
}
|
|
38
60
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(() => { }) }
|
|
50
85
|
}
|
|
51
86
|
|
|
52
|
-
const
|
|
53
|
-
const raw = await readFile(
|
|
87
|
+
const read = async file => {
|
|
88
|
+
const raw = await readFile(fp(file), 'utf-8').catch(() => null)
|
|
54
89
|
if (!raw) return null
|
|
55
90
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await unlink(filePath(file)).catch(() => { })
|
|
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(() => { })
|
|
62
96
|
return null
|
|
63
97
|
}
|
|
64
98
|
}
|
|
65
99
|
|
|
66
|
-
const
|
|
100
|
+
const remove = f => unlink(fp(f)).catch(() => { })
|
|
67
101
|
|
|
68
|
-
let creds = bootstrapCreds(await
|
|
69
|
-
let cleanupRunning = false
|
|
70
|
-
let lastCleanupAt = 0
|
|
71
|
-
let lastCleanedPreKeyId = creds.nextPreKeyId
|
|
102
|
+
let creds = bootstrapCreds(await read('creds.json'))
|
|
103
|
+
let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
|
|
72
104
|
|
|
73
105
|
const cleanOldPreKeys = async () => {
|
|
74
|
-
|
|
75
|
-
if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
106
|
+
if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
76
107
|
cleanupRunning = true
|
|
77
108
|
try {
|
|
78
109
|
const minId = creds.nextPreKeyId - preKeyRetention
|
|
@@ -81,222 +112,129 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
81
112
|
.map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
|
|
82
113
|
.filter(m => m && parseInt(m[1], 10) < minId)
|
|
83
114
|
.map(m => join(folder, m[0]))
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
logger?.warn({ err }, 'prekey cleanup failed')
|
|
90
|
-
} finally {
|
|
91
|
-
cleanupRunning = false
|
|
92
|
-
}
|
|
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 }
|
|
93
120
|
}
|
|
94
121
|
|
|
95
122
|
cleanOldPreKeys().catch(() => { })
|
|
96
123
|
|
|
97
|
-
const getStats = async () => {
|
|
98
|
-
const files = await readdir(folder).catch(() => [])
|
|
99
|
-
const preKeyFiles = files.filter(f => /^pre-key-\d+\.json$/.test(f))
|
|
100
|
-
return {
|
|
101
|
-
totalFiles: files.length,
|
|
102
|
-
preKeyCount: preKeyFiles.length,
|
|
103
|
-
nextPreKeyId: creds.nextPreKeyId,
|
|
104
|
-
lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null,
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
124
|
return {
|
|
109
125
|
state: {
|
|
110
126
|
creds,
|
|
111
127
|
keys: {
|
|
112
128
|
get: async (type, ids) => {
|
|
113
129
|
const data = {}
|
|
114
|
-
await Promise.all(ids.map(async id => {
|
|
115
|
-
let value = await readData(`${type}-${id}.json`)
|
|
116
|
-
if (type === 'app-state-sync-key' && value) value = proto.Message.AppStateSyncKeyData.fromObject(value)
|
|
117
|
-
data[id] = value
|
|
118
|
-
}))
|
|
130
|
+
await Promise.all(ids.map(async id => { const v = await read(`${type}-${id}.json`); data[id] = patchAppStateKey(type, v) }))
|
|
119
131
|
return data
|
|
120
132
|
},
|
|
121
|
-
set: async
|
|
133
|
+
set: async data => {
|
|
122
134
|
const tasks = []
|
|
123
|
-
for (const
|
|
124
|
-
for (const id in data[
|
|
125
|
-
const value = data[category][id]
|
|
126
|
-
tasks.push(value ? writeData(value, `${category}-${id}.json`) : removeData(`${category}-${id}.json`))
|
|
127
|
-
}
|
|
128
|
-
}
|
|
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`)) }
|
|
129
137
|
await Promise.all(tasks)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
132
140
|
},
|
|
133
141
|
saveCreds: async () => {
|
|
134
|
-
if (creds.nextPreKeyId -
|
|
135
|
-
return
|
|
142
|
+
if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
143
|
+
return write(creds, 'creds.json')
|
|
136
144
|
},
|
|
137
|
-
getStats
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── Keyv-backed ───────────────────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
const ser = (v) => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
144
|
-
|
|
145
|
-
const unwrapNestedValue = (obj) => {
|
|
146
|
-
let cur = obj
|
|
147
|
-
while (cur && typeof cur === 'object' && !Array.isArray(cur) && Object.keys(cur).length === 1 && 'value' in cur) cur = cur.value
|
|
148
|
-
return cur
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const deser = (raw, logger) => {
|
|
152
|
-
if (raw == null) return null
|
|
153
|
-
let parsed
|
|
154
|
-
if (typeof raw === 'string') {
|
|
155
|
-
try { parsed = JSON.parse(raw) } catch (err) {
|
|
156
|
-
logger?.warn?.({ err: err.message }, '[Auth] failed to parse stored value — treating as missing')
|
|
157
|
-
return null
|
|
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 }
|
|
158
148
|
}
|
|
159
|
-
} else {
|
|
160
|
-
parsed = raw
|
|
161
149
|
}
|
|
162
|
-
parsed = unwrapNestedValue(parsed)
|
|
163
|
-
if (parsed == null) return null
|
|
164
|
-
if (typeof parsed === 'object' && '__checksum' in parsed && 'data' in parsed) parsed = parsed.data
|
|
165
|
-
return JSON.parse(JSON.stringify(parsed), BufferJSON.reviver)
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
|
|
169
|
-
'@keyv/mongo': 'collection',
|
|
170
|
-
'@keyv/postgres': 'table',
|
|
171
|
-
'@keyv/mysql': 'table',
|
|
172
|
-
'@keyv/sqlite': 'table',
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const _adapterCache = new Map()
|
|
176
|
-
export const clearAdapterCache = () => _adapterCache.clear()
|
|
177
|
-
|
|
178
|
-
const resolveAdapter = async (connectionString, collectionName) => {
|
|
179
|
-
const cacheKey = collectionName ? `${connectionString}::${collectionName}` : connectionString
|
|
180
|
-
if (_adapterCache.has(cacheKey)) return _adapterCache.get(cacheKey)
|
|
181
|
-
|
|
182
|
-
let protocol
|
|
183
|
-
try { protocol = new URL(connectionString).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${connectionString}"`) }
|
|
184
|
-
|
|
185
|
-
const pkg = PROTOCOL_ADAPTERS[protocol]
|
|
186
|
-
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.`)
|
|
187
|
-
|
|
188
|
-
let mod
|
|
189
|
-
try { mod = await import(pkg) } catch (e) {
|
|
190
|
-
if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" is required for this backend — install it: npm i ${pkg}`)
|
|
191
|
-
throw e
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
|
|
195
|
-
const optionKey = COLLECTION_OPTION_KEY[pkg]
|
|
196
|
-
const adapter = (collectionName && optionKey)
|
|
197
|
-
? new Adapter({ url: connectionString, uri: connectionString, [optionKey]: collectionName })
|
|
198
|
-
: new Adapter(connectionString)
|
|
199
|
-
adapter.setMaxListeners?.(0)
|
|
200
|
-
_adapterCache.set(cacheKey, adapter)
|
|
201
|
-
return adapter
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const isAdapterLike = (v) =>
|
|
205
|
-
v != null &&
|
|
206
|
-
typeof v === 'object' &&
|
|
207
|
-
typeof v.get === 'function' &&
|
|
208
|
-
typeof v.set === 'function' &&
|
|
209
|
-
typeof v.delete === 'function' &&
|
|
210
|
-
typeof v.clear === 'function'
|
|
211
|
-
|
|
212
|
-
const resolveStore = async (backend, collectionName) => {
|
|
213
|
-
if (backend == null) return null
|
|
214
|
-
if (typeof backend === 'string') return resolveAdapter(backend, collectionName)
|
|
215
|
-
if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
|
|
216
|
-
if (isAdapterLike(backend)) return backend
|
|
217
|
-
throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
|
|
218
|
-
}
|
|
152
|
+
// ── Keyv-backed auth state ────────────────────────────────────────────────────
|
|
219
153
|
|
|
220
154
|
export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
221
|
-
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD,
|
|
155
|
+
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName } = opts
|
|
222
156
|
|
|
223
157
|
const store = await resolveStore(backend, collectionName)
|
|
224
|
-
const keyv = store ? new Keyv({ store
|
|
225
|
-
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'))
|
|
226
161
|
|
|
227
|
-
|
|
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`
|
|
228
165
|
|
|
229
|
-
let creds = bootstrapCreds(deser(await keyv.get(
|
|
230
|
-
let cleanupRunning = false
|
|
231
|
-
|
|
232
|
-
|
|
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])
|
|
233
170
|
|
|
234
171
|
const cleanOldPreKeys = async () => {
|
|
235
|
-
|
|
236
|
-
if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
172
|
+
if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
237
173
|
cleanupRunning = true
|
|
238
174
|
try {
|
|
239
175
|
const minId = creds.nextPreKeyId - preKeyRetention
|
|
240
176
|
if (minId <= 0) return
|
|
241
177
|
const ids = []
|
|
242
|
-
for (let id = Math.max(0,
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
lastCleanupAt = Date.now()
|
|
246
|
-
lastCleanedPreKeyId = creds.nextPreKeyId
|
|
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
|
|
247
181
|
} catch (e) {
|
|
248
|
-
logger
|
|
249
|
-
} finally {
|
|
250
|
-
cleanupRunning = false
|
|
251
|
-
}
|
|
182
|
+
logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed')
|
|
183
|
+
} finally { cleanupRunning = false }
|
|
252
184
|
}
|
|
253
185
|
|
|
254
186
|
cleanOldPreKeys().catch(() => { })
|
|
255
187
|
|
|
256
188
|
const keys = {
|
|
257
189
|
get: async (type, ids) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const rawValues = await keyv.getMany(rawKeys)
|
|
262
|
-
const rewrites = []
|
|
263
|
-
ids.forEach((id, i) => {
|
|
264
|
-
data[id] = patchAppStateKey(type, deser(rawValues[i], logger))
|
|
265
|
-
})
|
|
266
|
-
if (rewrites.length) await Promise.all(rewrites).catch(() => { })
|
|
267
|
-
return data
|
|
190
|
+
if (!ids.length) return {}
|
|
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]))]))
|
|
268
193
|
},
|
|
269
|
-
set: async
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
for (const type in data) {
|
|
194
|
+
set: async data => {
|
|
195
|
+
const sets = [], dels = []
|
|
196
|
+
for (const type in data)
|
|
273
197
|
for (const id in data[type]) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
else delKeys.push(key)
|
|
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) }
|
|
278
201
|
}
|
|
279
|
-
}
|
|
280
202
|
await Promise.all([
|
|
281
|
-
|
|
282
|
-
|
|
203
|
+
sets.length ? keyv.setMany(sets) : null,
|
|
204
|
+
dels.length ? keyv.deleteMany(dels) : null
|
|
283
205
|
])
|
|
284
|
-
}
|
|
206
|
+
}
|
|
285
207
|
}
|
|
286
208
|
|
|
287
209
|
const saveCreds = async () => {
|
|
288
|
-
if (creds.nextPreKeyId -
|
|
289
|
-
|
|
210
|
+
if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
211
|
+
_writtenKeys.add(credsKey)
|
|
212
|
+
await keyv.set(credsKey, ser(creds))
|
|
290
213
|
}
|
|
291
214
|
|
|
292
215
|
const clearSession = async () => {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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()
|
|
300
238
|
}
|
|
301
239
|
keyv.removeAllListeners?.()
|
|
302
240
|
}
|
|
@@ -304,4 +242,14 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
|
304
242
|
return { state: { creds, keys }, saveCreds, clearSession }
|
|
305
243
|
}
|
|
306
244
|
|
|
245
|
+
// check if valid creds exist for a session without fully loading it
|
|
246
|
+
export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
|
|
247
|
+
try {
|
|
248
|
+
const store = await resolveStore(backend, opts.collectionName)
|
|
249
|
+
const keyv = store ? new Keyv({ store }) : new Keyv()
|
|
250
|
+
const parsed = deser(await keyv.get(`${sessionId}::creds`))
|
|
251
|
+
return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
|
|
252
|
+
} catch { return false }
|
|
253
|
+
}
|
|
254
|
+
|
|
307
255
|
export { Keyv }
|
|
@@ -6,10 +6,12 @@ import { getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary/index
|
|
|
6
6
|
import { Curve, hmacSign } from './crypto.js'
|
|
7
7
|
import { encodeBigEndian } from './generics.js'
|
|
8
8
|
import { createSignalIdentity } from './signal.js'
|
|
9
|
+
import { getPlatformType } from './browser-utils.js'
|
|
9
10
|
|
|
10
11
|
const PLATFORM_MAP = {
|
|
11
12
|
'Mac OS': proto.ClientPayload.WebInfo.WebSubPlatform.DARWIN,
|
|
12
|
-
Windows: proto.ClientPayload.WebInfo.WebSubPlatform.
|
|
13
|
+
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN_HYBRID,
|
|
14
|
+
'WindowsStore': proto.ClientPayload.WebInfo.WebSubPlatform.WIN_STORE,
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const getUserAgent = (config) => ({
|
|
@@ -18,12 +20,11 @@ const getUserAgent = (config) => ({
|
|
|
18
20
|
secondary: config.version[1],
|
|
19
21
|
tertiary: config.version[2],
|
|
20
22
|
},
|
|
21
|
-
platform:
|
|
22
|
-
? proto.ClientPayload.UserAgent.Platform.MACOS
|
|
23
|
-
: proto.ClientPayload.UserAgent.Platform.WEB,
|
|
23
|
+
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
|
24
24
|
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
|
25
25
|
osVersion: '0.1',
|
|
26
26
|
device: 'Desktop',
|
|
27
|
+
deviceType: proto.ClientPayload.UserAgent.DeviceType.DESKTOP,
|
|
27
28
|
osBuildNumber: '0.1',
|
|
28
29
|
localeLanguageIso6391: 'en',
|
|
29
30
|
mnc: '000',
|
|
@@ -48,10 +49,16 @@ const getClientPayload = (config) => {
|
|
|
48
49
|
return payload
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const buildHistorySyncConfig = () => {
|
|
53
|
+
const fields = proto.DeviceProps.HistorySyncConfig.fields
|
|
54
|
+
const config = {}
|
|
55
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
56
|
+
if (field.type === 'bool') config[name] = true
|
|
57
|
+
}
|
|
58
|
+
config.storageQuotaMb = 569150
|
|
59
|
+
config.supportCallLogHistory = false
|
|
60
|
+
config.supportGroupHistory = false
|
|
61
|
+
return config
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
export const generateLoginNode = (userJid, config) => {
|
|
@@ -72,24 +79,8 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
|
|
|
72
79
|
os: config.browser[0],
|
|
73
80
|
platformType: getPlatformType(config.browser[1]),
|
|
74
81
|
requireFullSync: config.syncFullHistory,
|
|
75
|
-
historySyncConfig:
|
|
76
|
-
|
|
77
|
-
inlineInitialPayloadInE2EeMsg: true,
|
|
78
|
-
recentSyncDaysLimit: undefined,
|
|
79
|
-
supportCallLogHistory: false,
|
|
80
|
-
supportBotUserAgentChatHistory: true,
|
|
81
|
-
supportCagReactionsAndPolls: true,
|
|
82
|
-
supportBizHostedMsg: true,
|
|
83
|
-
supportRecentSyncChunkMessageCountTuning: true,
|
|
84
|
-
supportHostedGroupMsg: true,
|
|
85
|
-
supportFbidBotChatHistory: true,
|
|
86
|
-
supportAddOnHistorySyncMigration: undefined,
|
|
87
|
-
supportMessageAssociation: true,
|
|
88
|
-
supportGroupHistory: false,
|
|
89
|
-
onDemandReady: undefined,
|
|
90
|
-
supportGuestChat: undefined,
|
|
91
|
-
},
|
|
92
|
-
version: { primary: 10, secondary: 15, tertiary: 7 },
|
|
82
|
+
historySyncConfig: buildHistorySyncConfig(),
|
|
83
|
+
version: { primary: config.version[0], secondary: config.version[1], tertiary: config.version[2] },
|
|
93
84
|
}).finish()
|
|
94
85
|
|
|
95
86
|
return proto.ClientPayload.fromObject({
|
|
@@ -97,14 +88,14 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
|
|
|
97
88
|
passive: false,
|
|
98
89
|
pull: false,
|
|
99
90
|
devicePairingData: {
|
|
100
|
-
buildHash: appVersionBuf,
|
|
101
|
-
deviceProps: companionProto,
|
|
102
91
|
eRegid: encodeBigEndian(registrationId),
|
|
103
92
|
eKeytype: KEY_BUNDLE_TYPE,
|
|
104
93
|
eIdent: signedIdentityKey.public,
|
|
105
94
|
eSkeyId: encodeBigEndian(signedPreKey.keyId, 3),
|
|
106
95
|
eSkeyVal: signedPreKey.keyPair.public,
|
|
107
96
|
eSkeySig: signedPreKey.signature,
|
|
97
|
+
buildHash: appVersionBuf,
|
|
98
|
+
deviceProps: companionProto,
|
|
108
99
|
},
|
|
109
100
|
})
|
|
110
101
|
}
|