@nexustechpro/baileys 2.0.5 → 2.1.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.
@@ -1,16 +1,60 @@
1
- import { Boom } from "@hapi/boom"
2
- import { randomBytes } from "crypto"
3
- import { URL } from "url"
4
- import { promisify } from "util"
5
- import { proto } from "../../WAProto/index.js"
6
- import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, UPLOAD_TIMEOUT, BATCH_SIZE, TimeMs } from "../Defaults/index.js"
7
- import { DisconnectReason } from "../Types/index.js"
8
- import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from "../Utils/index.js"
9
- import { getPlatformId, migrateIndexKey } from "../Utils/index.js"
10
- import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from "../WABinary/index.js"
11
- import { BinaryInfo } from "../WAM/BinaryInfo.js"
12
- import { USyncQuery, USyncUser } from "../WAUSync/index.js"
13
- import { WebSocketClient } from "./Client/index.js"
1
+ import { Boom } from '@hapi/boom'
2
+ import { randomBytes } from 'crypto'
3
+ import { URL } from 'url'
4
+ import { promisify } from 'util'
5
+ import { proto } from '../../WAProto/index.js'
6
+ import {
7
+ DEF_CALLBACK_PREFIX,
8
+ DEF_TAG_PREFIX,
9
+ INITIAL_PREKEY_COUNT,
10
+ MIN_PREKEY_COUNT,
11
+ MIN_UPLOAD_INTERVAL,
12
+ NOISE_WA_HEADER,
13
+ UPLOAD_TIMEOUT,
14
+ BATCH_SIZE,
15
+ TimeMs
16
+ } from '../Defaults/index.js'
17
+ import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js'
18
+ import { DisconnectReason, XWAPaths } from '../Types/index.js'
19
+ import {
20
+ addTransactionCapability,
21
+ aesEncryptCTR,
22
+ bindWaitForConnectionUpdate,
23
+ buildPairingQRData,
24
+ bytesToCrockford,
25
+ configureSuccessfulPairing,
26
+ Curve,
27
+ derivePairingCodeKey,
28
+ generateLoginNode,
29
+ generateMdTagPrefix,
30
+ generateRegistrationNode,
31
+ getCodeFromWSError,
32
+ getCompanionPlatformId,
33
+ getErrorCodeFromStreamError,
34
+ getNextPreKeysNode,
35
+ makeEventBuffer,
36
+ makeNoiseHandler,
37
+ promiseTimeout,
38
+ signedKeyPair,
39
+ xmppSignedPreKey
40
+ } from '../Utils/index.js'
41
+ import { getPlatformId, migrateIndexKey } from '../Utils/index.js'
42
+ import {
43
+ assertNodeErrorFree,
44
+ binaryNodeToString,
45
+ encodeBinaryNode,
46
+ getAllBinaryNodeChildren,
47
+ getBinaryNodeChild,
48
+ getBinaryNodeChildren,
49
+ isLidUser,
50
+ jidDecode,
51
+ jidEncode,
52
+ S_WHATSAPP_NET
53
+ } from '../WABinary/index.js'
54
+ import { BinaryInfo } from '../WAM/BinaryInfo.js'
55
+ import { USyncQuery, USyncUser } from '../WAUSync/index.js'
56
+ import { WebSocketClient } from './Client/index.js'
57
+ import { executeWMexQuery } from './mex.js'
14
58
 
15
59
  // ─── Module-scope helpers ──────────────────────────────────────────────────────
16
60
 
@@ -43,7 +87,7 @@ export const makeSocket = (config) => {
43
87
  makeSignalRepository
44
88
  } = config
45
89
 
46
- if (printQRInTerminal) logger?.warn("printQRInTerminal deprecated")
90
+
47
91
 
48
92
  const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
49
93
 
@@ -65,6 +109,14 @@ export const makeSocket = (config) => {
65
109
  logger.info({ url: url.toString() }, 'Initiating WebSocket connection')
66
110
  ws.connect()
67
111
 
112
+ if (printQRInTerminal) {
113
+ import('qrcode-terminal').then(({ default: qrcode }) => {
114
+ ev.on('connection.update', ({ qr }) => {
115
+ if (qr) qrcode.generate(qr, { small: true })
116
+ })
117
+ })
118
+ }
119
+
68
120
  const ev = makeEventBuffer(logger)
69
121
  const { creds } = authState
70
122
  const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
@@ -73,6 +125,7 @@ export const makeSocket = (config) => {
73
125
  const uqTagId = generateMdTagPrefix()
74
126
  const sendPromise = promisify(ws.send)
75
127
 
128
+ // State
76
129
  let epoch = 1
77
130
  let lastDateRecv
78
131
  let lastUploadTime = 0
@@ -81,6 +134,10 @@ export const makeSocket = (config) => {
81
134
  let keepAliveReq
82
135
  let qrTimer
83
136
  let serverTimeOffsetMs = 0
137
+ let didStartBuffer = false
138
+
139
+ /** Socket end handlers — registered via registerSocketEndHandler() */
140
+ const socketEndHandlers = []
84
141
 
85
142
  const generateMessageTag = () => `${uqTagId}${epoch++}`
86
143
 
@@ -153,7 +210,13 @@ export const makeSocket = (config) => {
153
210
  attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' },
154
211
  content: [{
155
212
  tag: 'usync',
156
- attrs: { context: usyncQuery.context, mode: usyncQuery.mode, sid: generateMessageTag(), last: 'true', index: '0' },
213
+ attrs: {
214
+ context: usyncQuery.context,
215
+ mode: usyncQuery.mode,
216
+ sid: generateMessageTag(),
217
+ last: 'true',
218
+ index: '0'
219
+ },
157
220
  content: [
158
221
  { tag: 'query', attrs: {}, content: usyncQuery.protocols.map((a) => a.getQueryElement()) },
159
222
  { tag: 'list', attrs: {}, content: userNodes }
@@ -166,14 +229,39 @@ export const makeSocket = (config) => {
166
229
  async function pnFromLIDUSync(jids) {
167
230
  const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background')
168
231
  for (const jid of jids) {
169
- if (!isLidUser(jid)) usyncQuery.withUser(new USyncUser().withId(jid))
170
- else logger?.warn('LID user found in LID fetch call')
232
+ if (isLidUser(jid)) { logger?.warn('LID user found in LID fetch call'); continue }
233
+ usyncQuery.withUser(new USyncUser().withId(jid))
171
234
  }
172
235
  if (usyncQuery.users.length === 0) return []
173
236
  const results = await executeUSyncQuery(usyncQuery)
174
237
  return results ? results.list.filter((a) => !!a.lid).map(({ lid, id }) => ({ pn: id, lid })) : []
175
238
  }
176
239
 
240
+ const onWhatsApp = async (...phoneNumbers) => {
241
+ let usyncQuery = new USyncQuery()
242
+ let contactEnabled = false
243
+ for (let jid of phoneNumbers) {
244
+ if (isLidUser(jid)) {
245
+ const pn = await signalRepository.lidMapping.getPNForLID(jid)
246
+ if (pn) {
247
+ jid = pn
248
+ } else {
249
+ if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
250
+ usyncQuery.withUser(new USyncUser().withId(jid))
251
+ continue
252
+ }
253
+ }
254
+ if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
255
+ const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`
256
+ usyncQuery.withUser(new USyncUser().withPhone(phone))
257
+ }
258
+ if (usyncQuery.users.length === 0) return []
259
+ const results = await executeUSyncQuery(usyncQuery)
260
+ return results
261
+ ? results.list.filter((a) => !!a.contact && a.id && a.id !== 'undefined').map(({ contact, id }) => ({ jid: id, exists: contact }))
262
+ : []
263
+ }
264
+
177
265
  // ─── Pre-keys ───────────────────────────────────────────────────────────────
178
266
 
179
267
  const getAvailablePreKeysOnServer = async () => {
@@ -185,11 +273,24 @@ export const makeSocket = (config) => {
185
273
  return +getBinaryNodeChild(result, 'count').attrs.value
186
274
  }
187
275
 
276
+ /**
277
+ * Verify that our current pre-key actually exists in local key storage.
278
+ * Catches the case where the server still has keys but our local store is missing them.
279
+ */
280
+ const verifyCurrentPreKeyExists = async () => {
281
+ const currentPreKeyId = creds.nextPreKeyId - 1
282
+ if (currentPreKeyId <= 0) return { exists: false, currentPreKeyId: 0 }
283
+ const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()])
284
+ return { exists: !!preKeys[currentPreKeyId.toString()], currentPreKeyId }
285
+ }
286
+
188
287
  const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
288
+ // Rate-limit guard — only on the first attempt, not retries
189
289
  if (retryCount === 0 && Date.now() - lastUploadTime < MIN_UPLOAD_INTERVAL) {
190
- logger.debug(`Skipping upload, only ${Date.now() - lastUploadTime}ms since last`)
290
+ logger.debug(`Skipping upload only ${Date.now() - lastUploadTime}ms since last upload`)
191
291
  return
192
292
  }
293
+ // Dedup: if an upload is already in-flight, wait for it
193
294
  if (uploadPreKeysPromise) {
194
295
  logger.debug('Pre-key upload in progress, waiting...')
195
296
  await uploadPreKeysPromise
@@ -197,6 +298,7 @@ export const makeSocket = (config) => {
197
298
  }
198
299
  const uploadLogic = async () => {
199
300
  logger.info({ count, retryCount }, 'Uploading pre-keys')
301
+ // Generate keys inside a transaction to prevent ID collisions on retry
200
302
  const node = await keys.transaction(async () => {
201
303
  const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
202
304
  ev.emit('creds.update', update)
@@ -230,15 +332,23 @@ export const makeSocket = (config) => {
230
332
  try {
231
333
  const preKeyCount = await getAvailablePreKeysOnServer()
232
334
  logger.info(`${preKeyCount} pre-keys found on server`)
233
- if (preKeyCount < MIN_PREKEY_COUNT) {
234
- const uploadCount = INITIAL_PREKEY_COUNT - preKeyCount
235
- logger.info(`Server pre-key count low (${preKeyCount}), uploading ${uploadCount}`)
335
+ const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
336
+ logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
337
+ const lowServerCount = preKeyCount <= MIN_PREKEY_COUNT
338
+ const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
339
+ if (lowServerCount || missingCurrentPreKey) {
340
+ const reasons = []
341
+ if (lowServerCount) reasons.push(`server count low (${preKeyCount})`)
342
+ if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing from storage`)
343
+ logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`)
344
+ const uploadCount = preKeyCount === 0 ? INITIAL_PREKEY_COUNT : MIN_PREKEY_COUNT
236
345
  await uploadPreKeys(uploadCount)
237
346
  } else {
238
- logger.info(`✅ PreKey validation passed - Server: ${preKeyCount} pre-keys`)
347
+ logger.info(`✅ PreKey validation passed Server: ${preKeyCount}, prekey ${currentPreKeyId} exists`)
239
348
  }
240
349
  } catch (error) {
241
350
  logger.error({ error }, 'Failed to check/upload pre-keys during init')
351
+ // Non-fatal — allow connection to continue
242
352
  }
243
353
  }
244
354
 
@@ -285,7 +395,7 @@ export const makeSocket = (config) => {
285
395
  const parsed = Number(tValue)
286
396
  if (Number.isNaN(parsed) || parsed <= 0) return
287
397
  serverTimeOffsetMs = parsed * 1000 - Date.now()
288
- logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset')
398
+ logger.debug({ offset: serverTimeOffsetMs }, 'Calculated server time offset')
289
399
  }
290
400
 
291
401
  // ─── Unified session telemetry ───────────────────────────────────────────────
@@ -305,14 +415,65 @@ export const makeSocket = (config) => {
305
415
  content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
306
416
  })
307
417
  } catch (error) {
308
- logger.debug({ error }, 'failed to send unified_session telemetry')
418
+ logger.debug({ error }, 'Failed to send unified_session telemetry')
309
419
  }
310
420
  }
311
421
 
422
+ // ─── WAM buffer ─────────────────────────────────────────────────────────────
423
+
424
+ const sendWAMBuffer = (wamBuffer) =>
425
+ query({
426
+ tag: 'iq',
427
+ attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
428
+ content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
429
+ })
430
+
431
+ // ─── WMex queries ────────────────────────────────────────────────────────────
432
+
433
+ /**
434
+ * Fetches account restriction / reachout timelock status.
435
+ */
436
+ const fetchAccountReachoutTimelock = async () => {
437
+ const queryResult = await executeWMexQuery(
438
+ {},
439
+ QueryIds.REACHOUT_TIMELOCK,
440
+ XWAPaths.xwa2_fetch_account_reachout_timelock,
441
+ query,
442
+ generateMessageTag
443
+ )
444
+ const result = {
445
+ isActive: !!queryResult?.is_active,
446
+ timeEnforcementEnds:
447
+ queryResult?.time_enforcement_ends && queryResult.time_enforcement_ends !== '0'
448
+ ? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
449
+ : undefined,
450
+ enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
451
+ }
452
+ ev.emit('connection.update', { reachoutTimeLock: result })
453
+ return result
454
+ }
455
+
456
+ /**
457
+ * Fetches new-chat message cap quota and usage.
458
+ */
459
+ const fetchNewChatMessageCap = async () =>
460
+ executeWMexQuery(
461
+ { input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } },
462
+ QueryIds.MESSAGE_CAPPING_INFO,
463
+ XWAPaths.xwa2_message_capping_info,
464
+ query,
465
+ generateMessageTag
466
+ )
467
+
312
468
  // ─── Connection lifecycle ────────────────────────────────────────────────────
313
469
 
314
470
  const onUnexpectedError = (err, msg) => {
315
- logger.error({ err }, `unexpected error in '${msg}'`)
471
+ const isClosed = err?.message?.includes('Connection Closed') || err?.output?.statusCode === 428
472
+ if (isClosed) {
473
+ logger.debug({ msg: err?.message }, `Connection closed during '${msg}'`)
474
+ } else {
475
+ logger.error({ err }, `unexpected error in '${msg}'`)
476
+ }
316
477
  }
317
478
 
318
479
  const awaitNextMessage = async (sendMsg) => {
@@ -335,7 +496,7 @@ export const makeSocket = (config) => {
335
496
  }
336
497
 
337
498
  const validateConnection = async () => {
338
- let helloMsg = proto.HandshakeMessage.fromObject({ clientHello: { ephemeral: ephemeralKeyPair.public } })
499
+ const helloMsg = proto.HandshakeMessage.fromObject({ clientHello: { ephemeral: ephemeralKeyPair.public } })
339
500
  logger.info({ browser, helloMsg }, 'Connected to WhatsApp')
340
501
  const init = proto.HandshakeMessage.encode(helloMsg).finish()
341
502
  const result = await awaitNextMessage(init)
@@ -346,9 +507,7 @@ export const makeSocket = (config) => {
346
507
  logger.info({ node }, !creds.me ? 'Attempting registration...' : 'Logging in...')
347
508
  const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
348
509
  await sendRawMessage(
349
- proto.HandshakeMessage.encode({
350
- clientFinish: { static: keyEnc, payload: payloadEnc }
351
- }).finish()
510
+ proto.HandshakeMessage.encode({ clientFinish: { static: keyEnc, payload: payloadEnc } }).finish()
352
511
  )
353
512
  await noise.finishInit()
354
513
  startKeepAliveRequest()
@@ -376,27 +535,32 @@ export const makeSocket = (config) => {
376
535
  * Keep-alive: ping WA every keepAliveIntervalMs.
377
536
  * If the server stops responding (diff > interval + 5s) the connection
378
537
  * is considered lost and we call end() — the consumer handles reconnection.
379
- * No internal reconnect loop — clean separation of concerns.
380
538
  */
381
539
  const startKeepAliveRequest = () => {
382
540
  keepAliveReq = setInterval(() => {
383
541
  if (!lastDateRecv) lastDateRecv = new Date()
384
542
  const diff = Date.now() - lastDateRecv.getTime()
385
543
  if (diff > keepAliveIntervalMs + 5000) {
386
- end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
544
+ void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
387
545
  } else if (ws.isOpen) {
388
546
  query({
389
547
  tag: 'iq',
390
548
  attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:p' },
391
549
  content: [{ tag: 'ping', attrs: {} }]
392
- }).catch((err) => logger.error({ trace: err.stack }, 'error in sending keep alive'))
550
+ }).catch((err) => logger.error({ trace: err.stack }, 'Error in sending keep alive'))
393
551
  } else {
394
- logger.warn('keep alive called when WS not open')
552
+ logger.warn('Keep alive called when WS not open')
395
553
  }
396
554
  }, keepAliveIntervalMs)
397
555
  }
398
556
 
399
- const end = (error) => {
557
+ /**
558
+ * Tear down the socket.
559
+ * Awaits ws.close() so cleanup is deterministic before emitting connection.update.
560
+ * Runs all registered socketEndHandlers in order.
561
+ * Calls signalRepository.close() and ev.destroy() to prevent leaks.
562
+ */
563
+ const end = async (error) => {
400
564
  if (closed) { logger.trace({ trace: error?.stack }, 'Connection already closed'); return }
401
565
  closed = true
402
566
  logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
@@ -405,9 +569,18 @@ export const makeSocket = (config) => {
405
569
  ws.removeAllListeners('close')
406
570
  ws.removeAllListeners('open')
407
571
  ws.removeAllListeners('message')
408
- if (!ws.isClosed && !ws.isClosing) { try { ws.close() } catch { } }
572
+ signalRepository.close?.()
573
+ if (!ws.isClosed && !ws.isClosing) {
574
+ try { await ws.close() } catch { }
575
+ }
576
+ for (const handler of socketEndHandlers) {
577
+ try { await handler(error) }
578
+ catch (err) { logger.error({ err }, 'error in socket end handler') }
579
+ }
409
580
  ev.emit('connection.update', { connection: 'close', lastDisconnect: { error, date: new Date() } })
410
581
  ev.removeAllListeners('connection.update')
582
+ ev.removeAllListeners()
583
+ ev.destroy?.()
411
584
  }
412
585
 
413
586
  const sendPassiveIq = (tag) =>
@@ -426,14 +599,21 @@ export const makeSocket = (config) => {
426
599
  content: [{ tag: 'remove-companion-device', attrs: { jid, reason: 'user_initiated' } }]
427
600
  })
428
601
  }
429
- end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
602
+ void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
430
603
  }
431
604
 
605
+ /**
606
+ * Register a cleanup handler that will be awaited during end().
607
+ * Use for releasing external resources tied to this socket's lifetime.
608
+ */
609
+ const registerSocketEndHandler = (handler) => socketEndHandlers.push(handler)
610
+
432
611
  // ─── Pairing ─────────────────────────────────────────────────────────────────
433
612
 
434
613
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
435
614
  await waitForSocketOpen()
436
- await new Promise(resolve => setTimeout(resolve, 500))
615
+ // Brief stabilisation delay ensures the WS open event has fully propagated
616
+ await new Promise((resolve) => setTimeout(resolve, 500))
437
617
  const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
438
618
  if (customPairingCode && customPairingCode?.length !== 8)
439
619
  throw new Error('Custom pairing code must be exactly 8 chars')
@@ -449,7 +629,7 @@ export const makeSocket = (config) => {
449
629
  content: [
450
630
  { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
451
631
  { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
452
- { tag: 'companion_platform_id', attrs: {}, content: getPlatformId(browser[1]) },
632
+ { tag: 'companion_platform_id', attrs: {}, content: getCompanionPlatformId(browser) },
453
633
  { tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
454
634
  { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
455
635
  ]
@@ -492,31 +672,22 @@ export const makeSocket = (config) => {
492
672
  })
493
673
  }
494
674
 
495
- const sendWAMBuffer = (wamBuffer) =>
496
- query({
497
- tag: 'iq',
498
- attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
499
- content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
500
- })
501
-
502
675
  // ─── WebSocket event bindings ────────────────────────────────────────────────
503
676
 
504
677
  ws.on('message', onMessageReceived)
505
678
 
506
679
  ws.on('open', async () => {
507
680
  try { await validateConnection() }
508
- catch (err) { logger.error({ err }, 'error in validating connection'); end(err) }
681
+ catch (err) { logger.error({ err }, 'error in validating connection'); void end(err) }
509
682
  })
510
683
 
511
- // Let mapWebSocketError convert the raw error then call end()
512
684
  ws.on('error', mapWebSocketError(end))
513
685
 
514
- // Any close end(), consumer decides whether to reconnect
515
- ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
686
+ ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
516
687
 
517
688
  ws.on('CB:xmlstreamend', () => {
518
689
  logger.info('Stream ended by server')
519
- if (!closed) end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
690
+ if (!closed) void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
520
691
  })
521
692
 
522
693
  // ─── QR pairing ─────────────────────────────────────────────────────────────
@@ -532,9 +703,10 @@ export const makeSocket = (config) => {
532
703
  const genPairQR = () => {
533
704
  if (!ws.isOpen) return
534
705
  const refNode = refNodes.shift()
535
- if (!refNode) { end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
706
+ if (!refNode) { void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
536
707
  const ref = refNode.content.toString('utf-8')
537
- const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',')
708
+ // Use buildPairingQRData so the browser tuple is included in the QR payload
709
+ const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser)
538
710
  ev.emit('connection.update', { qr })
539
711
  qrTimer = setTimeout(genPairQR, qrMs)
540
712
  qrMs = qrTimeout || 20000
@@ -554,7 +726,7 @@ export const makeSocket = (config) => {
554
726
  void sendUnifiedSession()
555
727
  } catch (error) {
556
728
  logger.info({ trace: error.stack }, 'Error in pairing')
557
- end(error)
729
+ void end(error)
558
730
  }
559
731
  })
560
732
 
@@ -565,15 +737,11 @@ export const makeSocket = (config) => {
565
737
  updateServerTimeOffset(node)
566
738
  await uploadPreKeysToServerIfRequired()
567
739
  await sendPassiveIq('active')
568
- try {
569
- await digestKeyBundle()
570
- } catch (e) {
571
- logger.warn({ e }, 'failed to run digest after login')
572
- }
740
+ try { await digestKeyBundle() }
741
+ catch (e) { logger.warn({ e }, 'failed to run digest after login') }
573
742
  } catch (err) {
574
743
  logger.warn({ err }, 'Failed to send initial passive IQ')
575
744
  }
576
-
577
745
  logger.info('✅ Opened connection to WhatsApp')
578
746
  clearTimeout(qrTimer)
579
747
  ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
@@ -585,18 +753,22 @@ export const makeSocket = (config) => {
585
753
  process.nextTick(async () => {
586
754
  try {
587
755
  const myPN = authState.creds.me.id
756
+ // Store own LID-PN mapping
588
757
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
758
+ // Build device-list using index-based batching to avoid unbounded key growth
589
759
  const { user, device } = jidDecode(myPN)
590
760
  const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
591
761
  currentBatch[user] = [device?.toString() || '0']
592
762
  const deviceKeys = Object.keys(currentBatch)
593
763
  if (deviceKeys.length > BATCH_SIZE) {
594
764
  deviceKeys.sort()
595
- deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach(k => delete currentBatch[k])
765
+ deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach((k) => delete currentBatch[k])
596
766
  }
597
- await authState.keys.set({ 'device-list': { 'index': currentBatch } })
767
+ await authState.keys.set({ 'device-list': { index: currentBatch } })
768
+ // Migrate own session from PN → LID
598
769
  await signalRepository.migrateSession(myPN, myLID)
599
770
  logger.info({ myPN, myLID }, 'Own LID session created successfully')
771
+ // Batch-migrate any remaining PN sessions to LID
600
772
  if (signalRepository.migrateAllPNSessionsToLID) {
601
773
  try {
602
774
  const migrated = await signalRepository.migrateAllPNSessionsToLID()
@@ -615,37 +787,37 @@ export const makeSocket = (config) => {
615
787
  // ─── Stream / connection error handlers ─────────────────────────────────────
616
788
 
617
789
  ws.on('CB:stream:error', (node) => {
618
- logger.error({ node }, 'Stream errored out')
790
+ const [reasonNode] = getAllBinaryNodeChildren(node)
791
+ logger.error({ reasonNode, fullErrorNode: node }, 'Stream errored out')
619
792
  const { reason, statusCode } = getErrorCodeFromStreamError(node)
620
- end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }))
793
+ void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }))
621
794
  })
622
795
 
623
796
  ws.on('CB:failure', (node) => {
624
797
  const reason = +(node.attrs.reason || 500)
625
- end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
798
+ void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
626
799
  })
627
800
 
628
801
  ws.on('CB:ib,,downgrade_webclient', () =>
629
- end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
802
+ void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
630
803
  )
631
804
 
632
- ws.on('CB:ib,,offline_preview', (node) => {
805
+ ws.on('CB:ib,,offline_preview', async (node) => {
633
806
  logger.info('Offline preview received', JSON.stringify(node))
634
- sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
807
+ await sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
635
808
  })
636
809
 
637
810
  ws.on('CB:ib,,edge_routing', (node) => {
638
811
  const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
639
812
  const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
640
813
  if (routingInfo?.content) {
641
- authState.creds.routingInfo = Buffer.from(routingInfo?.content)
814
+ authState.creds.routingInfo = Buffer.from(routingInfo.content)
642
815
  ev.emit('creds.update', authState.creds)
643
816
  }
644
817
  })
645
818
 
646
819
  // ─── Buffering & offline notifications ──────────────────────────────────────
647
820
 
648
- let didStartBuffer = false
649
821
  process.nextTick(() => {
650
822
  if (creds.me?.id) { ev.buffer(); didStartBuffer = true }
651
823
  ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
@@ -689,6 +861,7 @@ export const makeSocket = (config) => {
689
861
  sendNode,
690
862
  logout,
691
863
  end,
864
+ registerSocketEndHandler,
692
865
  onUnexpectedError,
693
866
  uploadPreKeys,
694
867
  uploadPreKeysToServerIfRequired,
@@ -701,6 +874,9 @@ export const makeSocket = (config) => {
701
874
  waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
702
875
  sendWAMBuffer,
703
876
  executeUSyncQuery,
877
+ onWhatsApp,
878
+ fetchAccountReachoutTimelock,
879
+ fetchNewChatMessageCap,
704
880
  listener: (eventName) => {
705
881
  if (typeof ev.listenerCount === 'function') return ev.listenerCount(eventName)
706
882
  if (typeof ev.listener === 'function') return ev.listener(eventName)?.length || 0