@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.
@@ -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
- const bootstrapCreds = (raw) => {
15
- const c = raw ?? initAuthCreds()
16
- if (!c.__version || c.__version < CURRENT_VERSION) c.__version = CURRENT_VERSION
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
- // ── File-based ────────────────────────────────────────────────────────────────
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
- export const useMultiFileAuthState = async (folder, options = {}) => {
26
- const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger } = options
35
+ // ── Adapter resolution ────────────────────────────────────────────────────────
27
36
 
28
- const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
29
- const filePath = (file) => join(folder, fixFileName(file))
30
- const tmpPath = (file) => filePath(file) + '.tmp'
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
- const folderInfo = await stat(folder).catch(() => null)
33
- if (folderInfo) {
34
- if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`)
35
- } else {
36
- await mkdir(folder, { recursive: true })
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
- const writeData = async (data, file) => {
40
- const fp = filePath(file)
41
- const tp = tmpPath(file)
42
- const payload = JSON.stringify(data, BufferJSON.replacer)
43
- await writeFile(tp, payload)
44
- try {
45
- await rename(tp, fp)
46
- } catch {
47
- await writeFile(fp, payload)
48
- await unlink(tp).catch(() => { })
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 readData = async (file) => {
53
- const raw = await readFile(filePath(file), { encoding: 'utf-8' }).catch(() => null)
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 parsed = JSON.parse(raw, BufferJSON.reviver)
57
- if (parsed && typeof parsed === 'object' && '__checksum' in parsed && 'data' in parsed) return parsed.data
58
- return parsed
59
- } catch (err) {
60
- logger?.warn({ file, err: err.message }, 'failed to read auth file — reinitializing')
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 removeData = (file) => unlink(filePath(file)).catch(() => { })
100
+ const remove = f => unlink(fp(f)).catch(() => { })
67
101
 
68
- let creds = bootstrapCreds(await readData('creds.json'))
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
- const now = Date.now()
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 (!targets.length) return
85
- await Promise.all(targets.map(f => unlink(f).catch(() => { })))
86
- lastCleanupAt = Date.now()
87
- lastCleanedPreKeyId = creds.nextPreKeyId
88
- } catch (err) {
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 (data) => {
133
+ set: async data => {
122
134
  const tasks = []
123
- for (const category in data) {
124
- for (const id in data[category]) {
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 - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
135
- return writeData(creds, 'creds.json')
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
- const COLLECTION_OPTION_KEY = {
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, logger, collectionName } = opts
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, namespace: sessionId }) : new Keyv({ namespace: sessionId })
225
- keyv.on('error', (err) => logger?.warn?.({ err: err?.message }, '[Auth] store error'))
158
+ const keyv = store ? new Keyv({ store }) : new Keyv()
159
+ keyv.setMaxListeners?.(0)
160
+ keyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
226
161
 
227
- const keyName = (type, id) => `${type}-${id}`
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('creds'), logger))
230
- let cleanupRunning = false
231
- let lastCleanupAt = 0
232
- let lastCleanedPreKeyId = creds.nextPreKeyId
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
- const now = Date.now()
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, lastCleanedPreKeyId - preKeyRetention); id < minId; id++) ids.push(keyName('pre-key', id))
243
- if (!ids.length) return
244
- await Promise.all(ids.map(k => keyv.delete(k).catch(() => { })))
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?.warn?.({ err: e.message }, '[Auth] prekey cleanup failed')
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
- const data = {}
259
- if (!ids.length) return data
260
- const rawKeys = ids.map(id => keyName(type, id))
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 (data) => {
270
- const setEntries = []
271
- const delKeys = []
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 value = data[type][id]
275
- const key = keyName(type, id)
276
- if (value != null) setEntries.push({ key, value: ser(value) })
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
- setEntries.length ? keyv.setMany(setEntries) : null,
282
- delKeys.length ? keyv.deleteMany(delKeys) : null,
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 - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
289
- await keyv.set('creds', ser(creds))
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 s = keyv.opts?.store ?? keyv.store
294
- if (s && typeof s.deleteMany === 'function' && typeof s.iterator === 'function') {
295
- const toDelete = []
296
- for await (const [key] of s.iterator(sessionId)) toDelete.push(key)
297
- if (toDelete.length) await s.deleteMany(toDelete)
298
- } else {
299
- await keyv.clear()
216
+ const prefix = `${sessionId}::`
217
+ let done = false
218
+ if (store) {
219
+ // Redis: KEYS pattern scan
220
+ const rc = store.client
221
+ if (rc && typeof rc.keys === 'function') {
222
+ try { await store.set('__probe__', 1); const m = await rc.keys(`${prefix}*`); if (m.length) await rc.del(m); done = true } catch { }
223
+ }
224
+ // Mongo/Postgres/SQLite: iterator scan
225
+ if (!done && typeof store.iterator === 'function') {
226
+ try {
227
+ const targets = []
228
+ for await (const [k] of store.iterator(prefix)) if (k.startsWith(prefix)) targets.push(k)
229
+ if (targets.length) await store.deleteMany(targets)
230
+ done = true
231
+ } catch { }
232
+ }
233
+ }
234
+ if (!done) {
235
+ // fallback (in-memory): delete every key ever written — covers all types + JID-based IDs
236
+ await Promise.all([..._writtenKeys].map(k => keyv.delete(k).catch(() => { })))
237
+ _writtenKeys.clear()
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.WIN32,
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: config.browser[1].toLocaleLowerCase().includes('android')
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 getPlatformType = (platform) => {
52
- const platformType = platform.toUpperCase()
53
- if (platformType === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE
54
- return proto.DeviceProps.PlatformType[platformType] ?? proto.DeviceProps.PlatformType.CHROME
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
- storageQuotaMb: 569150,
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
  }