@nexustechpro/baileys 1.0.2 → 1.0.3

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.
@@ -4,96 +4,79 @@ import { URL } from "url"
4
4
  import { promisify } from "util"
5
5
  import { proto } from "../../WAProto/index.js"
6
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,
7
+ DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT,
8
+ MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, UPLOAD_TIMEOUT
14
9
  } from "../Defaults/index.js"
15
10
  import { DisconnectReason } from "../Types/index.js"
16
11
  import {
17
- addTransactionCapability,
18
- aesEncryptCTR,
19
- bindWaitForConnectionUpdate,
20
- bytesToCrockford,
21
- configureSuccessfulPairing,
22
- Curve,
23
- derivePairingCodeKey,
24
- generateLoginNode,
25
- generateMdTagPrefix,
26
- generateRegistrationNode,
27
- getCodeFromWSError,
28
- getErrorCodeFromStreamError,
29
- getNextPreKeysNode,
30
- makeEventBuffer,
31
- makeNoiseHandler,
32
- promiseTimeout,
12
+ addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford,
13
+ configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix,
14
+ generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode,
15
+ makeEventBuffer, makeNoiseHandler, promiseTimeout
33
16
  } from "../Utils/index.js"
34
17
  import { getPlatformId } from "../Utils/browser-utils.js"
35
18
  import {
36
- assertNodeErrorFree,
37
- binaryNodeToString,
38
- encodeBinaryNode,
39
- getBinaryNodeChild,
40
- getBinaryNodeChildren,
41
- isLidUser,
42
- jidDecode,
43
- jidEncode,
44
- S_WHATSAPP_NET,
19
+ assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild,
20
+ getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET
45
21
  } from "../WABinary/index.js"
46
22
  import { BinaryInfo } from "../WAM/BinaryInfo.js"
47
23
  import { USyncQuery, USyncUser } from "../WAUSync/index.js"
48
24
  import { WebSocketClient } from "./Client/index.js"
49
- /**
50
- * Connects to WA servers and performs:
51
- * - simple queries (no retry mechanism, wait for connection establishment)
52
- * - listen to messages and emit events
53
- * - query phone connection
54
- */
25
+
55
26
  export const makeSocket = (config) => {
56
27
  const {
57
- waWebSocketUrl,
58
- connectTimeoutMs,
59
- logger,
60
- keepAliveIntervalMs,
61
- browser,
62
- auth: authState,
63
- printQRInTerminal,
64
- defaultQueryTimeoutMs,
65
- transactionOpts,
66
- qrTimeout,
67
- makeSignalRepository,
28
+ waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser,
29
+ auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts,
30
+ qrTimeout, makeSignalRepository
68
31
  } = config
32
+
69
33
  const publicWAMBuffer = new BinaryInfo()
70
34
  const uqTagId = generateMdTagPrefix()
35
+ let epoch = 1
71
36
  const generateMessageTag = () => `${uqTagId}${epoch++}`
37
+
72
38
  if (printQRInTerminal) {
73
- console.warn(
74
- "⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.",
75
- )
39
+ logger?.warn("printQRInTerminal deprecated - handle QR via connection.update event")
76
40
  }
41
+
77
42
  const url = typeof waWebSocketUrl === "string" ? new URL(waWebSocketUrl) : waWebSocketUrl
78
43
  if (config.mobile || url.protocol === "tcp:") {
79
- throw new Boom("Mobile API is not supported anymore", { statusCode: DisconnectReason.loggedOut })
44
+ throw new Boom("Mobile API not supported", { statusCode: DisconnectReason.loggedOut })
80
45
  }
81
46
  if (url.protocol === "wss" && authState?.creds?.routingInfo) {
82
47
  url.searchParams.append("ED", authState.creds.routingInfo.toString("base64url"))
83
48
  }
84
- /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
49
+
85
50
  const ephemeralKeyPair = Curve.generateKeyPair()
86
- /** WA noise protocol wrapper */
87
51
  const noise = makeNoiseHandler({
88
52
  keyPair: ephemeralKeyPair,
89
53
  NOISE_HEADER: NOISE_WA_HEADER,
90
54
  logger,
91
- routingInfo: authState?.creds?.routingInfo,
55
+ routingInfo: authState?.creds?.routingInfo
92
56
  })
57
+
93
58
  const ws = new WebSocketClient(url, config)
94
59
  ws.connect()
60
+
61
+ const ev = makeEventBuffer(logger)
62
+ const { creds } = authState
63
+ const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
64
+ const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync)
65
+
66
+ let lastDateRecv, keepAliveReq, qrTimer, sessionHealthCheck, preKeyMonitorInterval
67
+ let closed = false, isUploadingPreKeys = false
68
+ let lastPreKeyCheck = 0, lastUploadTime = 0, uploadPreKeysPromise = null
69
+ let lastMessageTime = Date.now()
70
+ let preKeyCheckQueue = []
71
+ let reconnectAttempts = 0
72
+ const MAX_RECONNECT_ATTEMPTS = 5
73
+
74
+ const PREKEY_CHECK_INTERVAL = 30 * 60 * 1000 // 30 mins
75
+ const PREKEY_MIN_INTERVAL = 5 * 60 * 1000 // 5 mins
76
+ const PREKEY_CRITICAL_THRESHOLD = 3
77
+
95
78
  const sendPromise = promisify(ws.send)
96
- /** send a raw buffer */
79
+
97
80
  const sendRawMessage = async (data) => {
98
81
  if (!ws.isOpen) {
99
82
  throw new Boom("Connection Closed", { statusCode: DisconnectReason.connectionClosed })
@@ -108,7 +91,7 @@ export const makeSocket = (config) => {
108
91
  }
109
92
  })
110
93
  }
111
- /** send a binary node */
94
+
112
95
  const sendNode = (frame) => {
113
96
  if (logger.level === "trace") {
114
97
  logger.trace({ xml: binaryNodeToString(frame), msg: "xml send" })
@@ -116,27 +99,15 @@ export const makeSocket = (config) => {
116
99
  const buff = encodeBinaryNode(frame)
117
100
  return sendRawMessage(buff)
118
101
  }
119
- /**
120
- * Wait for a message with a certain tag to be received
121
- * @param msgId the message tag to await
122
- * @param timeoutMs timeout after which the promise will reject
123
- */
102
+
124
103
  const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
125
- let onRecv
126
- let onErr
104
+ let onRecv, onErr
127
105
  try {
128
106
  const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
129
- onRecv = (data) => {
130
- resolve(data)
131
- }
132
- onErr = (err) => {
133
- reject(
134
- err ||
135
- new Boom("Connection Closed", {
136
- statusCode: DisconnectReason.connectionClosed,
137
- }),
138
- )
139
- }
107
+ onRecv = (data) => resolve(data)
108
+ onErr = (err) => reject(err || new Boom("Connection Closed", {
109
+ statusCode: DisconnectReason.connectionClosed
110
+ }))
140
111
  ws.on(`TAG:${msgId}`, onRecv)
141
112
  ws.on("close", onErr)
142
113
  ws.on("error", onErr)
@@ -144,7 +115,6 @@ export const makeSocket = (config) => {
144
115
  })
145
116
  return result
146
117
  } catch (error) {
147
- // Catch timeout and return undefined instead of throwing
148
118
  if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
149
119
  logger?.warn?.({ msgId }, "timed out waiting for message")
150
120
  return undefined
@@ -158,79 +128,55 @@ export const makeSocket = (config) => {
158
128
  }
159
129
  }
160
130
  }
161
- /** send a query, and wait for its response. auto-generates message ID if not provided */
131
+
162
132
  const query = async (node, timeoutMs) => {
163
- if (!node.attrs.id) {
164
- node.attrs.id = generateMessageTag()
165
- }
133
+ if (!node.attrs.id) node.attrs.id = generateMessageTag()
166
134
  const msgId = node.attrs.id
167
135
  const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
168
136
  const result = waitForMessage(msgId, timeoutMs).catch(reject)
169
- sendNode(node)
170
- .then(async () => resolve(await result))
171
- .catch(reject)
137
+ sendNode(node).then(async () => resolve(await result)).catch(reject)
172
138
  })
173
139
  if (result && "tag" in result) {
174
140
  assertNodeErrorFree(result)
175
141
  }
176
142
  return result
177
143
  }
144
+
178
145
  const executeUSyncQuery = async (usyncQuery) => {
179
146
  if (usyncQuery.protocols.length === 0) {
180
147
  throw new Boom("USyncQuery must have at least one protocol")
181
148
  }
182
- // todo: validate users, throw WARNING on no valid users
183
- // variable below has only validated users
184
149
  const validUsers = usyncQuery.users
185
- const userNodes = validUsers.map((user) => {
186
- return {
187
- tag: "user",
188
- attrs: {
189
- jid: !user.phone ? user.id : undefined,
190
- },
191
- content: usyncQuery.protocols.map((a) => a.getUserElement(user)).filter((a) => a !== null),
192
- }
193
- })
194
- const listNode = {
195
- tag: "list",
196
- attrs: {},
197
- content: userNodes,
198
- }
199
- const queryNode = {
200
- tag: "query",
201
- attrs: {},
202
- content: usyncQuery.protocols.map((a) => a.getQueryElement()),
203
- }
150
+ const userNodes = validUsers.map((user) => ({
151
+ tag: "user",
152
+ attrs: { jid: !user.phone ? user.id : undefined },
153
+ content: usyncQuery.protocols.map((a) => a.getUserElement(user)).filter((a) => a !== null)
154
+ }))
204
155
  const iq = {
205
156
  tag: "iq",
206
- attrs: {
207
- to: S_WHATSAPP_NET,
208
- type: "get",
209
- xmlns: "usync",
210
- },
211
- content: [
212
- {
213
- tag: "usync",
214
- attrs: {
215
- context: usyncQuery.context,
216
- mode: usyncQuery.mode,
217
- sid: generateMessageTag(),
218
- last: "true",
219
- index: "0",
220
- },
221
- content: [queryNode, listNode],
157
+ attrs: { to: S_WHATSAPP_NET, type: "get", xmlns: "usync" },
158
+ content: [{
159
+ tag: "usync",
160
+ attrs: {
161
+ context: usyncQuery.context, mode: usyncQuery.mode,
162
+ sid: generateMessageTag(), last: "true", index: "0"
222
163
  },
223
- ],
164
+ content: [
165
+ { tag: "query", attrs: {}, content: usyncQuery.protocols.map((a) => a.getQueryElement()) },
166
+ { tag: "list", attrs: {}, content: userNodes }
167
+ ]
168
+ }]
224
169
  }
225
170
  const result = await query(iq)
226
171
  return usyncQuery.parseUSyncQueryResult(result)
227
172
  }
173
+
228
174
  const onWhatsApp = async (...phoneNumber) => {
229
175
  let usyncQuery = new USyncQuery()
230
176
  let contactEnabled = false
231
177
  for (const jid of phoneNumber) {
232
178
  if (isLidUser(jid)) {
233
- logger?.warn("LIDs are not supported with onWhatsApp")
179
+ logger?.warn("LIDs not supported with onWhatsApp")
234
180
  continue
235
181
  } else {
236
182
  if (!contactEnabled) {
@@ -241,15 +187,16 @@ export const makeSocket = (config) => {
241
187
  usyncQuery.withUser(new USyncUser().withPhone(phone))
242
188
  }
243
189
  }
244
- if (usyncQuery.users.length === 0) {
245
- return [] // return early without forcing an empty query
246
- }
190
+ if (usyncQuery.users.length === 0) return []
247
191
  const results = await executeUSyncQuery(usyncQuery)
248
192
  if (results) {
249
- return results.list.filter((a) => !!a.contact).map(({ contact, id }) => ({ jid: id, exists: contact }))
193
+ return results.list.filter((a) => !!a.contact).map(({ contact, id }) => ({
194
+ jid: id, exists: contact
195
+ }))
250
196
  }
251
197
  }
252
- const pnFromLIDUSync = async (jids) => {
198
+
199
+ async function pnFromLIDUSync(jids) {
253
200
  const usyncQuery = new USyncQuery().withLIDProtocol().withContext("background")
254
201
  for (const jid of jids) {
255
202
  if (isLidUser(jid)) {
@@ -259,43 +206,32 @@ export const makeSocket = (config) => {
259
206
  usyncQuery.withUser(new USyncUser().withId(jid))
260
207
  }
261
208
  }
262
- if (usyncQuery.users.length === 0) {
263
- return [] // return early without forcing an empty query
264
- }
209
+ if (usyncQuery.users.length === 0) return []
265
210
  const results = await executeUSyncQuery(usyncQuery)
266
211
  if (results) {
267
212
  return results.list.filter((a) => !!a.lid).map(({ lid, id }) => ({ pn: id, lid: lid }))
268
213
  }
269
214
  return []
270
215
  }
271
- const ev = makeEventBuffer(logger)
272
- const { creds } = authState
273
- // add transaction capability
274
- const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
275
- const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync)
276
- let lastDateRecv
277
- let epoch = 1
278
- let preKeyMonitorInterval = null
279
- let lastPreKeyCheck = 0
280
- let isUploadingPreKeys = false
281
- const PREKEY_CHECK_INTERVAL = 2 * 60 * 60 * 1000 // Check every 2 hours
282
- const PREKEY_MIN_INTERVAL = 30 * 60 * 1000 // Minimum 30 mins between uploads
283
- let keepAliveReq
284
- let qrTimer
285
- let closed = false
286
- /** log & process any unexpected errors */
216
+
287
217
  const onUnexpectedError = (err, msg) => {
288
218
  logger.error({ err }, `unexpected error in '${msg}'`)
219
+ const message = (err && ((err.stack || err.message) || String(err))).toLowerCase()
220
+
221
+ // Trigger critical pre-key check on crypto errors
222
+ if (message.includes('bad mac') || (message.includes('mac') && message.includes('invalid'))) {
223
+ triggerPreKeyCheck("bad-mac", "critical")
224
+ }
225
+ if (message.includes('session') && message.includes('corrupt')) {
226
+ triggerPreKeyCheck("session-corruption", "critical")
227
+ }
289
228
  }
290
- /** await the next incoming message */
229
+
291
230
  const awaitNextMessage = async (sendMsg) => {
292
231
  if (!ws.isOpen) {
293
- throw new Boom("Connection Closed", {
294
- statusCode: DisconnectReason.connectionClosed,
295
- })
232
+ throw new Boom("Connection Closed", { statusCode: DisconnectReason.connectionClosed })
296
233
  }
297
- let onOpen
298
- let onClose
234
+ let onOpen, onClose
299
235
  const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
300
236
  onOpen = resolve
301
237
  onClose = mapWebSocketError(reject)
@@ -312,11 +248,9 @@ export const makeSocket = (config) => {
312
248
  }
313
249
  return result
314
250
  }
315
- /** connection handshake */
251
+
316
252
  const validateConnection = async () => {
317
- let helloMsg = {
318
- clientHello: { ephemeral: ephemeralKeyPair.public },
319
- }
253
+ let helloMsg = { clientHello: { ephemeral: ephemeralKeyPair.public } }
320
254
  helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
321
255
  logger.info({ browser, helloMsg }, "connected to WA")
322
256
  const init = proto.HandshakeMessage.encode(helloMsg).finish()
@@ -333,67 +267,52 @@ export const makeSocket = (config) => {
333
267
  logger.info({ node }, "logging in...")
334
268
  }
335
269
  const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
336
- await sendRawMessage(
337
- proto.HandshakeMessage.encode({
338
- clientFinish: {
339
- static: keyEnc,
340
- payload: payloadEnc,
341
- },
342
- }).finish(),
343
- )
270
+ await sendRawMessage(proto.HandshakeMessage.encode({
271
+ clientFinish: { static: keyEnc, payload: payloadEnc }
272
+ }).finish())
344
273
  noise.finishInit()
345
274
  startKeepAliveRequest()
346
275
  }
276
+
347
277
  const getAvailablePreKeysOnServer = async () => {
348
278
  const result = await query({
349
279
  tag: "iq",
350
- attrs: {
351
- id: generateMessageTag(),
352
- xmlns: "encrypt",
353
- type: "get",
354
- to: S_WHATSAPP_NET,
355
- },
356
- content: [{ tag: "count", attrs: {} }],
280
+ attrs: { id: generateMessageTag(), xmlns: "encrypt", type: "get", to: S_WHATSAPP_NET },
281
+ content: [{ tag: "count", attrs: {} }]
357
282
  })
358
283
  const countChild = getBinaryNodeChild(result, "count")
359
284
  return +countChild.attrs.value
360
285
  }
361
- // Pre-key upload state management
362
- let uploadPreKeysPromise = null
363
- let lastUploadTime = 0
364
- /** generates and uploads a set of pre-keys to the server */
286
+
365
287
  const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
366
- // Check minimum interval (except for retries)
367
288
  if (retryCount === 0) {
368
289
  const timeSinceLastUpload = Date.now() - lastUploadTime
369
290
  if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
370
- logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`)
291
+ logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last`)
371
292
  return
372
293
  }
373
294
  }
374
- // Prevent multiple concurrent uploads
375
295
  if (uploadPreKeysPromise) {
376
- logger.debug("Pre-key upload already in progress, waiting for completion")
296
+ logger.debug("Pre-key upload in progress, waiting")
377
297
  await uploadPreKeysPromise
298
+ return
378
299
  }
300
+
379
301
  const uploadLogic = async () => {
380
302
  logger.info({ count, retryCount }, "uploading pre-keys")
381
- // Generate and save pre-keys atomically (prevents ID collisions on retry)
382
303
  const node = await keys.transaction(async () => {
383
- logger.debug({ requestedCount: count }, "generating pre-keys with requested count")
304
+ logger.debug({ requestedCount: count }, "generating pre-keys")
384
305
  const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
385
- // Update credentials immediately to prevent duplicate IDs on retry
386
306
  ev.emit("creds.update", update)
387
- return node // Only return node since update is already used
307
+ return node
388
308
  }, creds?.me?.id || "upload-pre-keys")
389
- // Upload to server (outside transaction, can fail without affecting local keys)
309
+
390
310
  try {
391
311
  await query(node)
392
312
  logger.info({ count }, "uploaded pre-keys successfully")
393
313
  lastUploadTime = Date.now()
394
314
  } catch (uploadError) {
395
- logger.error({ uploadError: uploadError.toString(), count }, "Failed to upload pre-keys to server")
396
- // Exponential backoff retry (max 3 retries)
315
+ logger.error({ uploadError: uploadError.toString(), count }, "Failed to upload pre-keys")
397
316
  if (retryCount < 3) {
398
317
  const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000)
399
318
  logger.info(`Retrying pre-key upload in ${backoffDelay}ms`)
@@ -403,93 +322,127 @@ export const makeSocket = (config) => {
403
322
  throw uploadError
404
323
  }
405
324
  }
406
- // Add timeout protection
325
+
407
326
  uploadPreKeysPromise = Promise.race([
408
327
  uploadLogic(),
409
328
  new Promise((_, reject) =>
410
- setTimeout(() => reject(new Boom("Pre-key upload timeout", { statusCode: 408 })), UPLOAD_TIMEOUT),
411
- ),
329
+ setTimeout(() => reject(new Boom("Pre-key upload timeout", { statusCode: 408 })), UPLOAD_TIMEOUT)
330
+ )
412
331
  ])
332
+
413
333
  try {
414
334
  await uploadPreKeysPromise
415
335
  } finally {
416
336
  uploadPreKeysPromise = null
417
337
  }
418
338
  }
419
- // Add after the uploadPreKeys function:
420
- const smartPreKeyMonitor = async (reason = "scheduled") => {
421
- const now = Date.now()
422
- const timeSinceLastCheck = now - lastPreKeyCheck
423
-
424
- // Rate limiting - prevent spam
425
- if (timeSinceLastCheck < PREKEY_MIN_INTERVAL && reason !== "critical") {
426
- logger.debug({
427
- timeSinceLastCheck,
428
- reason
429
- }, "Skipping pre-key check - too recent")
430
- return
431
- }
432
-
433
- // Prevent concurrent uploads
434
- if (isUploadingPreKeys) {
435
- logger.debug({ reason }, "Pre-key upload already in progress")
436
- return
437
- }
438
-
439
- lastPreKeyCheck = now
440
-
441
- try {
442
- logger.debug({ reason }, "Checking pre-key status")
443
- const preKeyCount = await getAvailablePreKeysOnServer()
339
+
340
+ const smartPreKeyMonitor = async (reason = "scheduled", priority = "normal") => {
341
+ const now = Date.now()
342
+ const timeSinceLastCheck = now - lastPreKeyCheck
444
343
 
445
- logger.info({ preKeyCount, reason }, "Pre-key check result")
344
+ if (priority !== "critical") {
345
+ if (timeSinceLastCheck < PREKEY_MIN_INTERVAL) {
346
+ logger.debug({ timeSinceLastCheck, reason }, "Skipping pre-key check - too recent")
347
+ return
348
+ }
349
+ }
446
350
 
447
- // Determine if upload is needed
448
- const criticalThreshold = 3
449
- const lowThreshold = MIN_PREKEY_COUNT
351
+ if (isUploadingPreKeys) {
352
+ logger.debug({ reason, priority }, "Pre-key upload in progress")
353
+ if (priority === "critical") {
354
+ preKeyCheckQueue.push({ reason, priority, timestamp: now })
355
+ logger.info("Critical pre-key check queued")
356
+ }
357
+ return
358
+ }
450
359
 
451
- if (preKeyCount <= criticalThreshold) {
452
- logger.warn({ preKeyCount }, "CRITICAL: Very low pre-keys, uploading immediately")
453
- isUploadingPreKeys = true
454
- await uploadPreKeys(INITIAL_PREKEY_COUNT) // Upload more when critical
455
- } else if (preKeyCount < lowThreshold) {
456
- logger.info({ preKeyCount }, "Low pre-keys detected, topping up")
457
- isUploadingPreKeys = true
458
- await uploadPreKeys(lowThreshold - preKeyCount + 5) // Top up with buffer
459
- } else {
460
- logger.debug({ preKeyCount }, "Pre-key count is healthy")
360
+ lastPreKeyCheck = now
361
+
362
+ try {
363
+ logger.debug({ reason, priority }, "Checking pre-key status")
364
+ const preKeyCount = await getAvailablePreKeysOnServer()
365
+ logger.info({ preKeyCount, reason, priority }, "Pre-key check result")
366
+
367
+ let shouldUpload = false, uploadCount = 0
368
+
369
+ if (preKeyCount <= PREKEY_CRITICAL_THRESHOLD) {
370
+ logger.warn({ preKeyCount }, "🚨 CRITICAL: Very low pre-keys!")
371
+ shouldUpload = true
372
+ uploadCount = INITIAL_PREKEY_COUNT
373
+ priority = "critical"
374
+ } else if (preKeyCount < MIN_PREKEY_COUNT) {
375
+ logger.info({ preKeyCount }, "⚠️ Low pre-keys detected")
376
+ shouldUpload = true
377
+ uploadCount = Math.max(10, MIN_PREKEY_COUNT - preKeyCount + 5)
378
+ } else if (priority === "critical") {
379
+ logger.info({ preKeyCount }, "Uploading pre-keys for critical recovery")
380
+ shouldUpload = true
381
+ uploadCount = MIN_PREKEY_COUNT
382
+ } else {
383
+ logger.debug({ preKeyCount }, "✅ Pre-key count healthy")
384
+ }
385
+
386
+ if (shouldUpload) {
387
+ isUploadingPreKeys = true
388
+ await uploadPreKeys(uploadCount)
389
+ if (preKeyCheckQueue.length > 0) {
390
+ logger.info(`Processing ${preKeyCheckQueue.length} queued checks`)
391
+ preKeyCheckQueue = []
392
+ }
393
+ }
394
+ } catch (error) {
395
+ logger.error({ error, reason, priority }, "Pre-key check failed")
396
+ if (priority === "critical") {
397
+ setTimeout(() => {
398
+ smartPreKeyMonitor(reason, "critical").catch(err =>
399
+ logger.error({ err }, "Critical pre-key retry failed")
400
+ )
401
+ }, 10000)
402
+ }
403
+ } finally {
404
+ isUploadingPreKeys = false
461
405
  }
462
- } catch (error) {
463
- logger.error({ error, reason }, "Pre-key check failed")
464
- } finally {
465
- isUploadingPreKeys = false
466
406
  }
467
- }
468
407
 
469
- // Start background monitor
470
- const startPreKeyBackgroundMonitor = () => {
471
- if (preKeyMonitorInterval) {
472
- clearInterval(preKeyMonitorInterval)
473
- }
474
-
475
- preKeyMonitorInterval = setInterval(() => {
476
- smartPreKeyMonitor("background-check").catch(err => {
477
- logger.error({ err }, "Background pre-key monitor failed")
408
+ const triggerPreKeyCheck = (event, priority = "normal") => {
409
+ const triggers = {
410
+ "signal-error": "critical", "bad-mac": "critical", "session-corruption": "critical",
411
+ "auth-failure": "critical", "connection-established": "high", "connection-restored": "high",
412
+ "device-paired": "high", "message-send-error": "normal", "message-received": "normal",
413
+ "scheduled": "low", "keep-alive": "low"
414
+ }
415
+ const effectivePriority = triggers[event] || priority
416
+ logger.debug({ event, priority: effectivePriority }, "Pre-key check triggered")
417
+ smartPreKeyMonitor(event, effectivePriority).catch(err => {
418
+ logger.error({ err, event }, "Triggered pre-key check failed")
478
419
  })
479
- }, PREKEY_CHECK_INTERVAL)
480
-
481
- logger.info({ intervalHours: PREKEY_CHECK_INTERVAL / (60 * 60 * 1000) },
482
- "Started pre-key background monitor")
483
- }
420
+ }
421
+
422
+ const startPreKeyBackgroundMonitor = () => {
423
+ if (preKeyMonitorInterval) clearInterval(preKeyMonitorInterval)
424
+ preKeyMonitorInterval = setInterval(() => {
425
+ triggerPreKeyCheck("scheduled", "low")
426
+ }, PREKEY_CHECK_INTERVAL)
427
+ logger.info({ intervalMinutes: PREKEY_CHECK_INTERVAL / (60 * 1000) }, "Started pre-key monitor")
428
+ }
429
+
430
+ const stopPreKeyBackgroundMonitor = () => {
431
+ if (preKeyMonitorInterval) {
432
+ clearInterval(preKeyMonitorInterval)
433
+ preKeyMonitorInterval = null
434
+ logger.debug("Stopped pre-key monitor")
435
+ }
436
+ }
437
+
484
438
  const verifyCurrentPreKeyExists = async () => {
485
439
  const currentPreKeyId = creds.nextPreKeyId - 1
486
- if (currentPreKeyId <= 0) {
487
- return { exists: false, currentPreKeyId: 0 }
488
- }
440
+ if (currentPreKeyId <= 0) return { exists: false, currentPreKeyId: 0 }
489
441
  const preKeys = await keys.get("pre-key", [currentPreKeyId.toString()])
490
442
  const exists = !!preKeys[currentPreKeyId.toString()]
491
443
  return { exists, currentPreKeyId }
492
444
  }
445
+
493
446
  const uploadPreKeysToServerIfRequired = async () => {
494
447
  try {
495
448
  let count = 0
@@ -498,42 +451,37 @@ const startPreKeyBackgroundMonitor = () => {
498
451
  else count = MIN_PREKEY_COUNT
499
452
  const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
500
453
  logger.info(`${preKeyCount} pre-keys found on server`)
501
- logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
454
+ logger.info(`Current prekey ID: ${currentPreKeyId}, exists: ${currentPreKeyExists}`)
502
455
  const lowServerCount = preKeyCount <= count
503
456
  const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
504
457
  const shouldUpload = lowServerCount || missingCurrentPreKey
505
458
  if (shouldUpload) {
506
459
  const reasons = []
507
460
  if (lowServerCount) reasons.push(`server count low (${preKeyCount})`)
508
- if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing from storage`)
461
+ if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing`)
509
462
  logger.info(`Uploading PreKeys due to: ${reasons.join(", ")}`)
510
463
  await uploadPreKeys(count)
511
464
  } else {
512
- logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`)
465
+ logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current ${currentPreKeyId} exists`)
513
466
  }
514
467
  } catch (error) {
515
- logger.error({ error }, "Failed to check/upload pre-keys during initialization")
516
- // Don't throw - allow connection to continue even if pre-key check fails
468
+ logger.error({ error }, "Failed to check/upload pre-keys during init")
517
469
  }
518
470
  }
471
+
519
472
  const onMessageReceived = (data) => {
520
473
  noise.decodeFrame(data, (frame) => {
521
- // reset ping timeout
522
474
  lastDateRecv = new Date()
523
- updateLastMessageTime() // Update session health monitor
524
- let anyTriggered = false
525
- anyTriggered = ws.emit("frame", frame)
526
- // if it's a binary node
475
+ lastMessageTime = Date.now()
476
+ reconnectAttempts = 0 // Reset on successful message
477
+ let anyTriggered = ws.emit("frame", frame)
527
478
  if (!(frame instanceof Uint8Array)) {
528
479
  const msgId = frame.attrs.id
529
480
  if (logger.level === "trace") {
530
481
  logger.trace({ xml: binaryNodeToString(frame), msg: "recv xml" })
531
482
  }
532
- /* Check if this is a response to a message we sent */
533
483
  anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered
534
- /* Check if this is a response to a message we are expecting */
535
- const l0 = frame.tag
536
- const l1 = frame.attrs || {}
484
+ const l0 = frame.tag, l1 = frame.attrs || {}
537
485
  const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ""
538
486
  for (const key of Object.keys(l1)) {
539
487
  anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
@@ -548,48 +496,52 @@ const startPreKeyBackgroundMonitor = () => {
548
496
  }
549
497
  })
550
498
  }
499
+
551
500
  const end = (error) => {
552
501
  if (closed) {
553
502
  logger.trace({ trace: error?.stack }, "connection already closed")
554
503
  return
555
504
  }
556
505
  closed = true
557
- logger.info({ trace: error?.stack }, error ? "connection errored" : "connection closed")
506
+
507
+ // Only log and end if it's a real error, not just Connection Terminated
508
+ const shouldLogError = error && error.message !== "Connection Terminated"
509
+ if (shouldLogError) {
510
+ logger.info({ trace: error?.stack }, "connection errored")
511
+ } else {
512
+ logger.debug("connection closed gracefully")
513
+ }
514
+
558
515
  clearInterval(keepAliveReq)
559
- clearInterval(sessionHealthCheck) // Clear health monitor
516
+ clearInterval(sessionHealthCheck)
560
517
  clearTimeout(qrTimer)
518
+ stopPreKeyBackgroundMonitor()
519
+
561
520
  ws.removeAllListeners("close")
562
521
  ws.removeAllListeners("open")
563
522
  ws.removeAllListeners("message")
564
- // Clean up pre-key monitor
565
- if (preKeyMonitorInterval) {
566
- clearInterval(preKeyMonitorInterval)
567
- preKeyMonitorInterval = null
568
- logger.debug("Stopped pre-key background monitor")
569
- }
523
+
570
524
  if (!ws.isClosed && !ws.isClosing) {
571
- try {
572
- ws.close()
573
- } catch {}
525
+ try { ws.close() } catch {}
574
526
  }
575
- ev.emit("connection.update", {
576
- connection: "close",
577
- lastDisconnect: {
578
- error,
579
- date: new Date(),
580
- },
581
- })
527
+
528
+ // Don't emit if it's just a normal close
529
+ if (shouldLogError || (error && error.output?.statusCode !== DisconnectReason.connectionClosed)) {
530
+ ev.emit("connection.update", {
531
+ connection: "close",
532
+ lastDisconnect: { error, date: new Date() }
533
+ })
534
+ }
535
+
582
536
  ev.removeAllListeners("connection.update")
583
537
  }
538
+
584
539
  const waitForSocketOpen = async () => {
585
- if (ws.isOpen) {
586
- return
587
- }
540
+ if (ws.isOpen) return
588
541
  if (ws.isClosed || ws.isClosing) {
589
542
  throw new Boom("Connection Closed", { statusCode: DisconnectReason.connectionClosed })
590
543
  }
591
- let onOpen
592
- let onClose
544
+ let onOpen, onClose
593
545
  await new Promise((resolve, reject) => {
594
546
  onOpen = () => resolve(undefined)
595
547
  onClose = mapWebSocketError(reject)
@@ -602,180 +554,114 @@ const startPreKeyBackgroundMonitor = () => {
602
554
  ws.off("error", onClose)
603
555
  })
604
556
  }
605
- const startKeepAliveRequest = () =>
606
- (keepAliveReq = setInterval(() => {
607
- if (!lastDateRecv) {
608
- lastDateRecv = new Date()
609
- }
610
- const diff = Date.now() - lastDateRecv.getTime()
611
- /*
612
- check if it's been a suspicious amount of time since the server responded with our last seen
613
- it could be that the network is down
614
- */
615
- if (diff > keepAliveIntervalMs + 5000) {
616
- end(new Boom("Connection was lost", { statusCode: DisconnectReason.connectionLost }))
617
- } else if (ws.isOpen) {
618
- // if its all good, send a keep alive request
619
- query({
557
+
558
+ const startKeepAliveRequest = () => {
559
+ let consecutiveFailedPings = 0
560
+ const MAX_FAILED_PINGS = 3 // Allow 3 failed pings before giving up
561
+
562
+ keepAliveReq = setInterval(async () => {
563
+ if (!lastDateRecv) lastDateRecv = new Date()
564
+
565
+ if (ws.isOpen) {
566
+ try {
567
+ // Send ping and WAIT for response
568
+ await query({
620
569
  tag: "iq",
621
- attrs: {
622
- id: generateMessageTag(),
623
- to: S_WHATSAPP_NET,
624
- type: "get",
625
- xmlns: "w:p",
626
- },
627
- content: [{ tag: "ping", attrs: {} }],
628
- }).catch((err) => {
629
- logger.error({ trace: err.stack }, "error in sending keep alive")
570
+ attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: "get", xmlns: "w:p" },
571
+ content: [{ tag: "ping", attrs: {} }]
630
572
  })
631
- } else {
632
- logger.warn("keep alive called when WS not open")
573
+
574
+ // Ping succeeded - reset failure counter
575
+ consecutiveFailedPings = 0
576
+ logger.debug("Keep-alive ping successful")
577
+
578
+ } catch (err) {
579
+ // ❌ Ping failed
580
+ consecutiveFailedPings++
581
+ logger.warn({
582
+ consecutiveFailures: consecutiveFailedPings,
583
+ maxAllowed: MAX_FAILED_PINGS
584
+ }, "Keep-alive ping failed")
585
+
586
+ // Only kill connection after multiple consecutive failures
587
+ if (consecutiveFailedPings >= MAX_FAILED_PINGS) {
588
+ logger.error("Multiple consecutive ping failures - connection lost")
589
+ end(new Boom("Connection was lost", { statusCode: DisconnectReason.connectionLost }))
590
+ }
633
591
  }
634
- }, keepAliveIntervalMs))
635
-
636
- // The keepalive mechanism above already handles connection health
637
- let lastMessageTime = Date.now()
638
- let sessionHealthCheck
592
+ } else {
593
+ logger.warn("keep alive called when WS not open")
594
+ }
595
+ }, keepAliveIntervalMs)
596
+ }
639
597
 
640
598
  const startSessionHealthMonitor = () => {
641
599
  sessionHealthCheck = setInterval(() => {
642
600
  const timeSinceLastMsg = Date.now() - lastMessageTime
643
- // Increased threshold to 5 minutes (was 90 seconds)
644
- const healthCheckIntervalMs = keepAliveIntervalMs * 10 // 5 minutes with default 30s keep-alive
645
-
646
- // Only log warning, don't force disconnect - let keepalive handle it
601
+ const healthCheckIntervalMs = keepAliveIntervalMs * 10
647
602
  if (timeSinceLastMsg > healthCheckIntervalMs && ws.isOpen) {
648
- logger.warn(
649
- { timeSinceLastMsg, threshold: healthCheckIntervalMs },
650
- "Session health check: extended inactivity detected",
651
- )
652
- // The keepalive mechanism will naturally detect and handle dead connections
603
+ logger.warn({ timeSinceLastMsg, threshold: healthCheckIntervalMs }, "Extended inactivity detected")
653
604
  }
654
- }, keepAliveIntervalMs * 4) // Check every ~2 minutes with default 30s keep-alive
605
+ }, keepAliveIntervalMs * 4)
655
606
  }
656
607
 
657
- const updateLastMessageTime = () => {
658
- lastMessageTime = Date.now()
659
- }
660
- /** i have no idea why this exists. pls enlighten me */
661
- const sendPassiveIq = (tag) =>
662
- query({
663
- tag: "iq",
664
- attrs: {
665
- to: S_WHATSAPP_NET,
666
- xmlns: "passive",
667
- type: "set",
668
- },
669
- content: [{ tag, attrs: {} }],
670
- })
671
- /** logout & invalidate connection */
608
+ const sendPassiveIq = (tag) => query({
609
+ tag: "iq",
610
+ attrs: { to: S_WHATSAPP_NET, xmlns: "passive", type: "set" },
611
+ content: [{ tag, attrs: {} }]
612
+ })
613
+
672
614
  const logout = async (msg) => {
673
615
  const jid = authState.creds.me?.id
674
616
  if (jid) {
675
617
  await sendNode({
676
618
  tag: "iq",
677
- attrs: {
678
- to: S_WHATSAPP_NET,
679
- type: "set",
680
- id: generateMessageTag(),
681
- xmlns: "md",
682
- },
683
- content: [
684
- {
685
- tag: "remove-companion-device",
686
- attrs: {
687
- jid,
688
- reason: "user_initiated",
689
- },
690
- },
691
- ],
619
+ attrs: { to: S_WHATSAPP_NET, type: "set", id: generateMessageTag(), xmlns: "md" },
620
+ content: [{ tag: "remove-companion-device", attrs: { jid, reason: "user_initiated" } }]
692
621
  })
693
622
  }
694
623
  end(new Boom(msg || "Intentional Logout", { statusCode: DisconnectReason.loggedOut }))
695
624
  }
625
+
696
626
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
697
627
  const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
698
628
  if (customPairingCode && customPairingCode?.length !== 8) {
699
629
  throw new Error("Custom pairing code must be exactly 8 chars")
700
630
  }
701
631
  authState.creds.pairingCode = pairingCode
702
- authState.creds.me = {
703
- id: jidEncode(phoneNumber, "s.whatsapp.net"),
704
- name: "~",
705
- }
632
+ authState.creds.me = { id: jidEncode(phoneNumber, "s.whatsapp.net"), name: "~" }
706
633
  ev.emit("creds.update", authState.creds)
707
634
  await sendNode({
708
635
  tag: "iq",
709
- attrs: {
710
- to: S_WHATSAPP_NET,
711
- type: "set",
712
- id: generateMessageTag(),
713
- xmlns: "md",
714
- },
715
- content: [
716
- {
717
- tag: "link_code_companion_reg",
718
- attrs: {
719
- jid: authState.creds.me.id,
720
- stage: "companion_hello",
721
- should_show_push_notification: "true",
722
- },
723
- content: [
724
- {
725
- tag: "link_code_pairing_wrapped_companion_ephemeral_pub",
726
- attrs: {},
727
- content: await generatePairingKey(),
728
- },
729
- {
730
- tag: "companion_server_auth_key_pub",
731
- attrs: {},
732
- content: authState.creds.noiseKey.public,
733
- },
734
- {
735
- tag: "companion_platform_id",
736
- attrs: {},
737
- content: getPlatformId(browser[1]),
738
- },
739
- {
740
- tag: "companion_platform_display",
741
- attrs: {},
742
- content: `${browser[1]} (${browser[0]})`,
743
- },
744
- {
745
- tag: "link_code_pairing_nonce",
746
- attrs: {},
747
- content: "0",
748
- },
749
- ],
750
- },
751
- ],
636
+ attrs: { to: S_WHATSAPP_NET, type: "set", id: generateMessageTag(), xmlns: "md" },
637
+ content: [{
638
+ tag: "link_code_companion_reg",
639
+ attrs: { jid: authState.creds.me.id, stage: "companion_hello", should_show_push_notification: "true" },
640
+ content: [
641
+ { tag: "link_code_pairing_wrapped_companion_ephemeral_pub", attrs: {}, content: await generatePairingKey() },
642
+ { tag: "companion_server_auth_key_pub", attrs: {}, content: authState.creds.noiseKey.public },
643
+ { tag: "companion_platform_id", attrs: {}, content: getPlatformId(browser[1]) },
644
+ { tag: "companion_platform_display", attrs: {}, content: `${browser[1]} (${browser[0]})` },
645
+ { tag: "link_code_pairing_nonce", attrs: {}, content: "0" }
646
+ ]
647
+ }]
752
648
  })
753
649
  return authState.creds.pairingCode
754
650
  }
651
+
755
652
  async function generatePairingKey() {
756
- const salt = randomBytes(32)
757
- const randomIv = randomBytes(16)
653
+ const salt = randomBytes(32), randomIv = randomBytes(16)
758
654
  const key = await derivePairingCodeKey(authState.creds.pairingCode, salt)
759
655
  const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv)
760
656
  return Buffer.concat([salt, randomIv, ciphered])
761
657
  }
762
- const sendWAMBuffer = (wamBuffer) => {
763
- return query({
764
- tag: "iq",
765
- attrs: {
766
- to: S_WHATSAPP_NET,
767
- id: generateMessageTag(),
768
- xmlns: "w:stats",
769
- },
770
- content: [
771
- {
772
- tag: "add",
773
- attrs: { t: Math.round(Date.now() / 1000) + "" },
774
- content: wamBuffer,
775
- },
776
- ],
777
- })
778
- }
658
+
659
+ const sendWAMBuffer = (wamBuffer) => query({
660
+ tag: "iq",
661
+ attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: "w:stats" },
662
+ content: [{ tag: "add", attrs: { t: Math.round(Date.now() / 1000) + "" }, content: wamBuffer }]
663
+ })
664
+
779
665
  ws.on("message", onMessageReceived)
780
666
  ws.on("open", async () => {
781
667
  try {
@@ -785,33 +671,46 @@ const startPreKeyBackgroundMonitor = () => {
785
671
  end(err)
786
672
  }
787
673
  })
788
- ws.on("error", mapWebSocketError(end))
789
- ws.on("close", () => end(new Boom("Connection Terminated", { statusCode: DisconnectReason.connectionClosed })))
790
- // the server terminated the connection
791
- ws.on("CB:xmlstreamend", () =>
792
- end(new Boom("Connection Terminated by Server", { statusCode: DisconnectReason.connectionClosed })),
793
- )
794
- // QR gen
795
- ws.on("CB:iq,type:set,pair-device", async (stanza) => {
796
- const iq = {
797
- tag: "iq",
798
- attrs: {
799
- to: S_WHATSAPP_NET,
800
- type: "result",
801
- id: stanza.attrs.id,
802
- },
674
+
675
+ ws.on("error", (err) => {
676
+ logger.warn({ err: err.message }, "WebSocket error occurred")
677
+ // Don't immediately end - let other mechanisms handle it
678
+ })
679
+
680
+ ws.on("close", (code, reason) => {
681
+ logger.debug({ code, reason: reason?.toString() }, "WebSocket closed")
682
+ // Graceful close, don't throw error
683
+ if (!closed) {
684
+ setTimeout(() => {
685
+ if (!closed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
686
+ reconnectAttempts++
687
+ logger.info({ attempt: reconnectAttempts }, "Attempting to reconnect...")
688
+ ws.restart().catch(err => logger.error({ err }, "Reconnect failed"))
689
+ } else if (!closed) {
690
+ end(new Boom("Connection Terminated", { statusCode: DisconnectReason.connectionClosed }))
691
+ }
692
+ }, Math.min(1000 * Math.pow(2, reconnectAttempts), 10000)) // Exponential backoff
693
+ }
694
+ })
695
+
696
+ ws.on("CB:xmlstreamend", () => {
697
+ logger.info("Stream ended by server")
698
+ if (!closed) {
699
+ end(new Boom("Connection Terminated by Server", { statusCode: DisconnectReason.connectionClosed }))
803
700
  }
701
+ })
702
+
703
+ ws.on("CB:iq,type:set,pair-device", async (stanza) => {
704
+ const iq = { tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "result", id: stanza.attrs.id } }
804
705
  await sendNode(iq)
805
706
  const pairDeviceNode = getBinaryNodeChild(stanza, "pair-device")
806
707
  const refNodes = getBinaryNodeChildren(pairDeviceNode, "ref")
807
708
  const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString("base64")
808
709
  const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString("base64")
809
710
  const advB64 = creds.advSecretKey
810
- let qrMs = qrTimeout || 60000 // time to let a QR live
711
+ let qrMs = qrTimeout || 60000
811
712
  const genPairQR = () => {
812
- if (!ws.isOpen) {
813
- return
814
- }
713
+ if (!ws.isOpen) return
815
714
  const refNode = refNodes.shift()
816
715
  if (!refNode) {
817
716
  end(new Boom("QR refs attempts ended", { statusCode: DisconnectReason.timedOut }))
@@ -821,29 +720,26 @@ const startPreKeyBackgroundMonitor = () => {
821
720
  const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(",")
822
721
  ev.emit("connection.update", { qr })
823
722
  qrTimer = setTimeout(genPairQR, qrMs)
824
- qrMs = qrTimeout || 20000 // shorter subsequent qrs
723
+ qrMs = qrTimeout || 20000
825
724
  }
826
725
  genPairQR()
827
726
  })
828
- // device paired for the first time
829
- // if device pairs successfully, the server asks to restart the connection
727
+
830
728
  ws.on("CB:iq,,pair-success", async (stanza) => {
831
729
  logger.debug("pair success recv")
832
730
  try {
833
731
  const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
834
- logger.info(
835
- { me: updatedCreds.me, platform: updatedCreds.platform },
836
- "pairing configured successfully, expect to restart the connection...",
837
- )
732
+ logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, "pairing configured successfully")
838
733
  ev.emit("creds.update", updatedCreds)
839
734
  ev.emit("connection.update", { isNewLogin: true, qr: undefined })
735
+ triggerPreKeyCheck("device-paired", "high")
840
736
  await sendNode(reply)
841
737
  } catch (error) {
842
738
  logger.info({ trace: error.stack }, "error in pairing")
843
739
  end(error)
844
740
  }
845
741
  })
846
- // login complete
742
+
847
743
  ws.on("CB:success", async (node) => {
848
744
  try {
849
745
  await uploadPreKeysToServerIfRequired()
@@ -852,33 +748,21 @@ const startPreKeyBackgroundMonitor = () => {
852
748
  logger.warn({ err }, "failed to send initial passive iq")
853
749
  }
854
750
  logger.info("opened connection to WA")
855
- clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try
856
- // Immediate check on connect
857
- smartPreKeyMonitor("connection-established").catch(err => {
858
- logger.warn({ err }, "Initial pre-key check failed")
859
- })
860
-
861
- // Start background monitor
862
- startPreKeyBackgroundMonitor()
751
+ clearTimeout(qrTimer)
752
+ triggerPreKeyCheck("connection-established", "high")
753
+ startPreKeyBackgroundMonitor()
863
754
  ev.emit("creds.update", { me: { ...authState.creds.me, lid: node.attrs.lid } })
864
755
  ev.emit("connection.update", { connection: "open" })
865
- // Start monitoring session health after successful connection
866
756
  startSessionHealthMonitor()
757
+ reconnectAttempts = 0
867
758
  if (node.attrs.lid && authState.creds.me?.id) {
868
759
  const myLID = node.attrs.lid
869
760
  process.nextTick(async () => {
870
761
  try {
871
762
  const myPN = authState.creds.me.id
872
- // Store our own LID-PN mapping
873
763
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
874
- // Create device list for our own user (needed for bulk migration)
875
764
  const { user, device } = jidDecode(myPN)
876
- await authState.keys.set({
877
- "device-list": {
878
- [user]: [device?.toString() || "0"],
879
- },
880
- })
881
- // migrate our own session
765
+ await authState.keys.set({ "device-list": { [user]: [device?.toString() || "0"] } })
882
766
  await signalRepository.migrateSession(myPN, myLID)
883
767
  logger.info({ myPN, myLID }, "Own LID session created successfully")
884
768
  } catch (error) {
@@ -887,27 +771,27 @@ const startPreKeyBackgroundMonitor = () => {
887
771
  })
888
772
  }
889
773
  })
774
+
890
775
  ws.on("CB:stream:error", (node) => {
891
776
  logger.error({ node }, "stream errored out")
892
777
  const { reason, statusCode } = getErrorCodeFromStreamError(node)
893
778
  end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }))
894
779
  })
895
- // stream fail, possible logout
780
+
896
781
  ws.on("CB:failure", (node) => {
897
782
  const reason = +(node.attrs.reason || 500)
898
783
  end(new Boom("Connection Failure", { statusCode: reason, data: node.attrs }))
899
784
  })
785
+
900
786
  ws.on("CB:ib,,downgrade_webclient", () => {
901
787
  end(new Boom("Multi-device beta not joined", { statusCode: DisconnectReason.multideviceMismatch }))
902
788
  })
789
+
903
790
  ws.on("CB:ib,,offline_preview", (node) => {
904
791
  logger.info("offline preview received", JSON.stringify(node))
905
- sendNode({
906
- tag: "ib",
907
- attrs: {},
908
- content: [{ tag: "offline_batch", attrs: { count: "100" } }],
909
- })
792
+ sendNode({ tag: "ib", attrs: {}, content: [{ tag: "offline_batch", attrs: { count: "100" } }] })
910
793
  })
794
+
911
795
  ws.on("CB:ib,,edge_routing", (node) => {
912
796
  const edgeRoutingNode = getBinaryNodeChild(node, "edge_routing")
913
797
  const routingInfo = getBinaryNodeChild(edgeRoutingNode, "routing_info")
@@ -916,17 +800,16 @@ const startPreKeyBackgroundMonitor = () => {
916
800
  ev.emit("creds.update", authState.creds)
917
801
  }
918
802
  })
803
+
919
804
  let didStartBuffer = false
920
805
  process.nextTick(() => {
921
806
  if (creds.me?.id) {
922
- // start buffering important events
923
- // if we're logged in
924
807
  ev.buffer()
925
808
  didStartBuffer = true
926
809
  }
927
810
  ev.emit("connection.update", { connection: "connecting", receivedPendingNotifications: false, qr: undefined })
928
811
  })
929
- // called when all offline notifs are handled
812
+
930
813
  ws.on("CB:ib,,offline", (node) => {
931
814
  const child = getBinaryNodeChild(node, "offline")
932
815
  const offlineNotifs = +(child?.attrs.count || 0)
@@ -937,29 +820,21 @@ const startPreKeyBackgroundMonitor = () => {
937
820
  }
938
821
  ev.emit("connection.update", { receivedPendingNotifications: true })
939
822
  })
940
- // update credentials when required
823
+
941
824
  ev.on("creds.update", (update) => {
942
825
  const name = update.me?.name
943
- // if name has just been received
944
826
  if (creds.me?.name !== name) {
945
827
  logger.debug({ name }, "updated pushName")
946
- sendNode({
947
- tag: "presence",
948
- attrs: { name: name },
949
- }).catch((err) => {
828
+ sendNode({ tag: "presence", attrs: { name: name } }).catch((err) => {
950
829
  logger.warn({ trace: err.stack }, "error in sending presence update on name change")
951
830
  })
952
831
  }
953
832
  Object.assign(creds, update)
954
833
  })
955
834
 
956
- /**
957
- * Regenerate pre-keys immediately when Signal protocol errors occur
958
- */
959
835
  const regeneratePreKeysForRecovery = async () => {
960
836
  try {
961
837
  logger.info("Initiating aggressive pre-key regeneration for signal recovery")
962
- // Upload 15 fresh pre-keys immediately for recovery
963
838
  const preKeyNodes = getNextPreKeysNode(authState, MIN_PREKEY_COUNT)
964
839
  if (preKeyNodes) {
965
840
  await sendNode(preKeyNodes)
@@ -970,7 +845,6 @@ const startPreKeyBackgroundMonitor = () => {
970
845
  }
971
846
  }
972
847
 
973
- // Handler for signal session corruption on connection issues
974
848
  const handleSignalSessionCorruption = async (reason) => {
975
849
  if (reason === "SIGNAL_SESSION_CORRUPTED" || reason === "BAD_MAC_ERROR") {
976
850
  logger.warn("Signal session corruption detected - regenerating pre-keys immediately")
@@ -979,52 +853,23 @@ const startPreKeyBackgroundMonitor = () => {
979
853
  }
980
854
 
981
855
  return {
982
- type: "md",
983
- ws,
984
- ev,
985
- authState: { creds, keys },
986
- signalRepository,
987
- get user() {
988
- return authState.creds.me
989
- },
990
- generateMessageTag,
991
- query,
992
- waitForMessage,
993
- waitForSocketOpen,
994
- sendRawMessage,
995
- sendNode,
996
- logout,
997
- end,
998
- onUnexpectedError,
999
- uploadPreKeys,
1000
- uploadPreKeysToServerIfRequired,
1001
- requestPairingCode,
1002
- wamBuffer: publicWAMBuffer,
1003
- /** Waits for the connection to WA to reach a state */
1004
- waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
1005
- sendWAMBuffer,
1006
- executeUSyncQuery,
1007
- onWhatsApp,
1008
- regeneratePreKeysForRecovery,
1009
- handleSignalSessionCorruption,
856
+ type: "md", ws, ev, authState: { creds, keys }, signalRepository,
857
+ get user() { return authState.creds.me },
858
+ generateMessageTag, query, waitForMessage, waitForSocketOpen, sendRawMessage, sendNode,
859
+ logout, end, onUnexpectedError, uploadPreKeys, uploadPreKeysToServerIfRequired,
860
+ requestPairingCode, wamBuffer: publicWAMBuffer,
861
+ waitForConnectionUpdate: bindWaitForConnectionUpdate(ev), sendWAMBuffer,
862
+ executeUSyncQuery, onWhatsApp, regeneratePreKeysForRecovery, handleSignalSessionCorruption,
1010
863
  listener: (eventName) => {
1011
- if (typeof ev.listenerCount === "function") {
1012
- return ev.listenerCount(eventName)
1013
- }
1014
- if (typeof ev.listeners === "function") {
1015
- return ev.listeners(eventName)?.length || 0
1016
- }
864
+ if (typeof ev.listenerCount === "function") return ev.listenerCount(eventName)
865
+ if (typeof ev.listeners === "function") return ev.listeners(eventName)?.length || 0
1017
866
  return 0
1018
- },
867
+ }
1019
868
  }
1020
869
  }
1021
- /**
1022
- * map the websocket error to the right type
1023
- * so it can be retried by the caller
1024
- * */
870
+
1025
871
  function mapWebSocketError(handler) {
1026
872
  return (error) => {
1027
873
  handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }))
1028
874
  }
1029
- }
1030
- //# sourceMappingURL=socket.js.map
875
+ }