@nexustechpro/baileys 1.1.7 → 2.0.1

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.
@@ -122,7 +122,8 @@ export const makeMessagesSocket = (config) => {
122
122
  if (lidResults.length > 0) { logger.trace('Storing LID maps from device call'); await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id }))) }
123
123
  try {
124
124
  const lids = lidResults.map(a => a.lid)
125
- if (lids.length) await assertSessions(lids, true)
125
+ // Re-fetch sessions during device lookup to ensure fresh state
126
+ if (lids.length) await assertSessions(lids, false)
126
127
  } catch (e) {
127
128
  logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
128
129
  }
@@ -200,7 +201,7 @@ export const makeMessagesSocket = (config) => {
200
201
  for (const node of tokenNodes) {
201
202
  const jid = node.attrs.jid
202
203
  const token = node.content
203
- if (jid && token) tokens[jid] = { token }
204
+ if (jid && token) tokens[jid] = { token, timestamp: Number(unixTimestampSeconds()) }
204
205
  }
205
206
  }
206
207
  return tokens
@@ -361,8 +362,25 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
361
362
  if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
362
363
  additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
363
364
  }
365
+
366
+ // DEVICE 0 PRESERVATION FOR GROUPS: Initialize device 0 for all participants
367
+ const device0EntriesGroup = []
368
+ for (const jid of participantsList) {
369
+ const { user, server } = jidDecode(jid)
370
+ if (user) {
371
+ device0EntriesGroup.push({ user, device: 0, jid: jidEncode(user, server, 0) })
372
+ }
373
+ }
374
+
364
375
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
365
- devices.push(...additionalDevices)
376
+ // Combine device 0 entries with fetched devices, avoiding duplicates
377
+ const deviceMap = new Map()
378
+ for (const d of device0EntriesGroup) deviceMap.set(`${d.user}:${d.device}`, d)
379
+ for (const d of additionalDevices) {
380
+ const key = `${d.user}:${d.device}`
381
+ if (!deviceMap.has(key)) deviceMap.set(key, d)
382
+ }
383
+ devices.push(...Array.from(deviceMap.values()))
366
384
  }
367
385
 
368
386
  if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
@@ -383,6 +401,7 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
383
401
  if ((!hasKey || !!participant) && !isHostedLidUser(deviceJid) && !isHostedPnUser(deviceJid) && device.device !== 99) { senderKeyRecipients.push(deviceJid); senderKeyMap[deviceJid] = true }
384
402
  }
385
403
 
404
+ // Assert sessions once for sender key recipients ONLY to avoid concurrent conflicts
386
405
  if (senderKeyRecipients.length) {
387
406
  logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
388
407
  const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
@@ -416,10 +435,20 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
416
435
  }
417
436
 
418
437
  if (additionalAttributes?.category !== 'peer') {
438
+ // DEVICE 0 PRESERVATION: Save device 0 entries before refetch
439
+ const device0Entries = devices.filter(d => d.device === 0)
440
+ const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
419
441
  devices.length = 0
420
442
  const senderIdentity = isLid && meLid ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined) : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
443
+ // Fetch both sender and recipient devices to ensure complete enumeration
421
444
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
422
- devices.push(...sessionDevices)
445
+ devices.push(...device0Entries, ...sessionDevices)
446
+ // If sender devices weren't enumerated, explicitly fetch them
447
+ if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
448
+ const senderDevices = await getUSyncDevices([senderIdentity], true, false)
449
+ const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
450
+ if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
451
+ }
423
452
  }
424
453
  }
425
454
 
@@ -471,10 +500,24 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
471
500
 
472
501
  if (shouldIncludeDeviceIdentity) { stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) }); logger.debug({ jid }, 'adding device identity') }
473
502
  if (additionalNodes?.length > 0 && !additionalAlready) stanza.content.push(...additionalNodes)
474
- // Add TCToken support
475
- const contactTcTokenData = !isGroup && !isRetryResend && !isStatus ? await authState.keys.get('tctoken', [destinationJid]) : {}
476
- const tcTokenBuffer = contactTcTokenData[destinationJid]?.token
477
- if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
503
+ // Add TCToken support with expiration validation
504
+ if (!isGroup && !isRetryResend && !isStatus) {
505
+ const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
506
+ let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
507
+
508
+ // Check if token is expired
509
+ if (isTokenExpired(contactTcTokenData[destinationJid])) {
510
+ logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
511
+ try {
512
+ const freshTokens = await getPrivacyTokens([destinationJid])
513
+ tcTokenBuffer = freshTokens[destinationJid]?.token
514
+ } catch (err) {
515
+ logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
516
+ }
517
+ }
518
+
519
+ if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
520
+ }
478
521
 
479
522
  logger.debug({ msgId }, `sending message to ${participants.length} devices`)
480
523
  await sendNode(stanza)
@@ -484,7 +527,15 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
484
527
  return {key: {remoteJid: jid, fromMe: true, id: finalMsgId, participant: isGroup ? authState.creds.me.id : undefined}, messageId: finalMsgId}
485
528
  }
486
529
 
487
- const getPrivacyTokens = async (jids) => {
530
+ const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
531
+
532
+ const isTokenExpired = (tokenData) => {
533
+ if (!tokenData || !tokenData.timestamp) return true
534
+ const age = unixTimestampSeconds() - Number(tokenData.timestamp)
535
+ return age > TOKEN_EXPIRY_TTL
536
+ }
537
+
538
+ const getPrivacyTokens = async (jids) => {
488
539
  const t = unixTimestampSeconds().toString()
489
540
  const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' }, content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }] })
490
541
  const tokens = parseTCTokens(result)
@@ -277,7 +277,7 @@ class NexusHandler {
277
277
  eventMessage: {
278
278
  contextInfo: {
279
279
  mentionedJid: [jid], participant: jid, remoteJid: 'status@broadcast',
280
- forwardedNewsletterMessageInfo: { newsletterName: 'Nexus Events', newsletterJid: '120363421563597486@newsletter', serverMessageId: 1 }
280
+ forwardedNewsletterMessageInfo: { newsletterName: 'Nexus Events', newsletterJid: '120363422827915475@newsletter', serverMessageId: 1 }
281
281
  },
282
282
  isCanceled: e.isCanceled || false, name: e.name, description: e.description,
283
283
  location: e.location || { degreesLatitude: 0, degreesLongitude: 0, name: 'Location' }, joinLink: e.joinLink || '',
@@ -0,0 +1,197 @@
1
+ /* eslint-disable camelcase */
2
+ import axios from 'axios';
3
+ import { MOBILE_TOKEN, MOBILE_REGISTRATION_ENDPOINT, MOBILE_USERAGENT, REGISTRATION_PUBLIC_KEY } from '../Defaults/index.js';
4
+ import { md5, Curve, aesEncryptGCM } from '../Utils/crypto.js';
5
+ import { jidEncode } from '../WABinary/index.js';
6
+ import { makeCommunitiesSocket } from './communities.js';
7
+
8
+ function urlencode(str) {
9
+ return str.replace(/-/g, '%2d').replace(/_/g, '%5f').replace(/~/g, '%7e');
10
+ }
11
+
12
+ function convertBufferToUrlHex(buffer) {
13
+ let id = '';
14
+ buffer.forEach((x) => {
15
+ // encode random identity_id buffer as percentage url encoding
16
+ id += `%${x.toString(16).padStart(2, '0').toLowerCase()}`;
17
+ });
18
+ return id;
19
+ }
20
+
21
+ const validRegistrationOptions = (config) =>
22
+ config?.phoneNumberCountryCode &&
23
+ config.phoneNumberNationalNumber &&
24
+ config.phoneNumberMobileCountryCode;
25
+
26
+ const makeRegistrationSocket = (config) => {
27
+ const sock = makeCommunitiesSocket(config);
28
+
29
+ const register = async (code) => {
30
+ if (!validRegistrationOptions(config.auth.creds.registration)) {
31
+ throw new Error('please specify the registration options');
32
+ }
33
+
34
+ const result = await mobileRegister(
35
+ { ...sock.authState.creds, ...sock.authState.creds.registration, code },
36
+ config.options
37
+ );
38
+
39
+ sock.authState.creds.me = {
40
+ id: jidEncode(result.login, 's.whatsapp.net'),
41
+ name: '~'
42
+ };
43
+ sock.authState.creds.registered = true;
44
+ sock.ev.emit('creds.update', sock.authState.creds);
45
+
46
+ return result;
47
+ };
48
+
49
+ const requestRegistrationCode = async (registrationOptions) => {
50
+ registrationOptions = registrationOptions || config.auth.creds.registration;
51
+
52
+ if (!validRegistrationOptions(registrationOptions)) {
53
+ throw new Error('Invalid registration options');
54
+ }
55
+
56
+ sock.authState.creds.registration = registrationOptions;
57
+ sock.ev.emit('creds.update', sock.authState.creds);
58
+
59
+ return mobileRegisterCode(
60
+ { ...config.auth.creds, ...registrationOptions },
61
+ config.options
62
+ );
63
+ };
64
+
65
+ return {
66
+ ...sock,
67
+ register,
68
+ requestRegistrationCode,
69
+ };
70
+ };
71
+
72
+ function registrationParams(params) {
73
+ const e_regid = Buffer.alloc(4);
74
+ e_regid.writeInt32BE(params.registrationId);
75
+
76
+ const e_skey_id = Buffer.alloc(3);
77
+ e_skey_id.writeInt16BE(params.signedPreKey.keyId);
78
+
79
+ params.phoneNumberCountryCode = params.phoneNumberCountryCode.replace('+', '').trim();
80
+ params.phoneNumberNationalNumber = params.phoneNumberNationalNumber.replace(/[/\-\s)(]/g, '').trim();
81
+
82
+ return {
83
+ cc: params.phoneNumberCountryCode,
84
+ in: params.phoneNumberNationalNumber,
85
+ Rc: '0',
86
+ lg: 'en',
87
+ lc: 'GB',
88
+ mistyped: '6',
89
+ authkey: Buffer.from(params.noiseKey.public).toString('base64url'),
90
+ e_regid: e_regid.toString('base64url'),
91
+ e_keytype: 'BQ',
92
+ e_ident: Buffer.from(params.signedIdentityKey.public).toString('base64url'),
93
+ // e_skey_id: e_skey_id.toString('base64url'),
94
+ e_skey_id: 'AAAA',
95
+ e_skey_val: Buffer.from(params.signedPreKey.keyPair.public).toString('base64url'),
96
+ e_skey_sig: Buffer.from(params.signedPreKey.signature).toString('base64url'),
97
+ fdid: params.phoneId,
98
+ network_ratio_type: '1',
99
+ expid: params.deviceId,
100
+ simnum: '1',
101
+ hasinrc: '1',
102
+ pid: Math.floor(Math.random() * 1000).toString(),
103
+ id: convertBufferToUrlHex(params.identityId),
104
+ backup_token: convertBufferToUrlHex(params.backupToken),
105
+ token: md5(Buffer.concat([MOBILE_TOKEN, Buffer.from(params.phoneNumberNationalNumber)])).toString('hex'),
106
+ fraud_checkpoint_code: params.captcha,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Requests a registration code for the given phone number.
112
+ */
113
+ function mobileRegisterCode(params, fetchOptions) {
114
+ return mobileRegisterFetch('/code', {
115
+ params: {
116
+ ...registrationParams(params),
117
+ mcc: `${params.phoneNumberMobileCountryCode}`.padStart(3, '0'),
118
+ mnc: `${params.phoneNumberMobileNetworkCode || '001'}`.padStart(3, '0'),
119
+ sim_mcc: '000',
120
+ sim_mnc: '000',
121
+ method: params?.method || 'sms',
122
+ reason: '',
123
+ hasav: '1',
124
+ },
125
+ ...fetchOptions,
126
+ });
127
+ }
128
+
129
+ function mobileRegisterExists(params, fetchOptions) {
130
+ return mobileRegisterFetch('/exist', {
131
+ params: registrationParams(params),
132
+ ...fetchOptions,
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Registers the phone number on WhatsApp with the received OTP code.
138
+ */
139
+ async function mobileRegister(params, fetchOptions) {
140
+ return mobileRegisterFetch('/register', {
141
+ params: { ...registrationParams(params), code: params.code.replace('-', '') },
142
+ ...fetchOptions,
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Encrypts the given string as AEAD aes-256-gcm with the public WhatsApp key and a random keypair.
148
+ */
149
+ function mobileRegisterEncrypt(data) {
150
+ const keypair = Curve.generateKeyPair();
151
+ const key = Curve.sharedKey(keypair.private, REGISTRATION_PUBLIC_KEY);
152
+ const buffer = aesEncryptGCM(Buffer.from(data), new Uint8Array(key), Buffer.alloc(12), Buffer.alloc(0));
153
+
154
+ return Buffer.concat([Buffer.from(keypair.public), buffer]).toString('base64url');
155
+ }
156
+
157
+ async function mobileRegisterFetch(path, opts = {}) {
158
+ let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}`;
159
+
160
+ if (opts.params) {
161
+ const parameter = [];
162
+ for (const param in opts.params) {
163
+ if (opts.params[param] !== null && opts.params[param] !== undefined) {
164
+ parameter.push(param + '=' + urlencode(opts.params[param]));
165
+ }
166
+ }
167
+ url += `?${parameter.join('&')}`;
168
+ delete opts.params;
169
+ }
170
+
171
+ if (!opts.headers) {
172
+ opts.headers = {};
173
+ }
174
+ opts.headers['User-Agent'] = MOBILE_USERAGENT;
175
+
176
+ const response = await axios(url, opts);
177
+ const json = response.data;
178
+
179
+ if (response.status > 300 || json.reason) {
180
+ throw json;
181
+ }
182
+ if (json.status && !['ok', 'sent'].includes(json.status)) {
183
+ throw json;
184
+ }
185
+
186
+ return json;
187
+ }
188
+
189
+ export {
190
+ makeRegistrationSocket,
191
+ registrationParams,
192
+ mobileRegisterCode,
193
+ mobileRegisterExists,
194
+ mobileRegister,
195
+ mobileRegisterEncrypt,
196
+ mobileRegisterFetch,
197
+ };
@@ -308,15 +308,13 @@ export const makeSocket = (config) => {
308
308
  const hasSessions = !!sessionData?.['_index']
309
309
 
310
310
  const currentPreKeyId = creds.nextPreKeyId - 1
311
- const preKeyData = currentPreKeyId > 0
312
- ? await keys.get("pre-key", [currentPreKeyId.toString()])
313
- : {}
314
- const hasCurrentPreKey = !!preKeyData[currentPreKeyId?.toString()]
315
-
311
+ const expectedPreKeys = Math.max(0, currentPreKeyId)
312
+ const preKeyThreshold = 20
313
+
316
314
  logger.info({
317
315
  hasDeviceList,
318
316
  hasSessions,
319
- hasCurrentPreKey,
317
+ expectedPreKeys,
320
318
  currentPreKeyId
321
319
  }, "Auth state integrity check complete")
322
320
 
@@ -326,9 +324,6 @@ export const makeSocket = (config) => {
326
324
  if (!hasSessions) {
327
325
  logger.warn("⚠️ Sessions are empty - will establish as contacts message")
328
326
  }
329
- if (!hasCurrentPreKey && currentPreKeyId > 0) {
330
- logger.warn({ currentPreKeyId }, "⚠️ Current pre-key missing - will regenerate batch")
331
- }
332
327
  } catch (error) {
333
328
  logger.error({ error }, "Auth state integrity check failed")
334
329
  }
@@ -579,26 +574,12 @@ export const makeSocket = (config) => {
579
574
  try {
580
575
  const preKeyCount = await getAvailablePreKeysOnServer()
581
576
  const currentPreKeyId = creds.nextPreKeyId - 1
582
- const preKeys = currentPreKeyId > 0
583
- ? await keys.get("pre-key", [currentPreKeyId.toString()])
584
- : {}
585
- const currentPreKeyExists = !!preKeys[currentPreKeyId.toString()]
586
577
 
587
578
  logger.info(
588
- `${preKeyCount} pre-keys on server, current ID: ${currentPreKeyId}, exists: ${currentPreKeyExists}`
579
+ `Server: ${preKeyCount} pre-keys, Current ID: ${currentPreKeyId}`
589
580
  )
590
581
 
591
- // Critical: Missing current pre-key
592
- if (!currentPreKeyExists && currentPreKeyId > 0) {
593
- logger.warn(
594
- { currentPreKeyId },
595
- "🚨 Current pre-key file missing - regenerating full batch"
596
- )
597
- await uploadPreKeys(INITIAL_PREKEY_COUNT)
598
- return
599
- }
600
-
601
- // Low server count
582
+ // Only regenerate if server is low
602
583
  if (preKeyCount < MIN_PREKEY_COUNT) {
603
584
  const uploadCount = INITIAL_PREKEY_COUNT - preKeyCount
604
585
  logger.info(
@@ -607,7 +588,7 @@ export const makeSocket = (config) => {
607
588
  await uploadPreKeys(uploadCount)
608
589
  } else {
609
590
  logger.info(
610
- `✅ PreKey validation passed - Server: ${preKeyCount} pre-keys, Current ID: ${currentPreKeyId} exists`
591
+ `✅ PreKey validation passed - Server: ${preKeyCount} pre-keys`
611
592
  )
612
593
  }
613
594
  } catch (error) {
@@ -1214,7 +1195,7 @@ export const makeSocket = (config) => {
1214
1195
  onWhatsApp,
1215
1196
  listener: (eventName) => {
1216
1197
  if (typeof ev.listenerCount === "function") return ev.listenerCount(eventName)
1217
- if (typeof ev.listeners === "function") return ev.listeners(eventName)?.length || 0
1198
+ if (typeof ev.listener === "function") return ev.listener(eventName)?.length || 0
1218
1199
  return 0
1219
1200
  }
1220
1201
  }
@@ -209,6 +209,11 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
209
209
  decryptables += 1
210
210
  let msgBuffer
211
211
  const decryptionJid = await getDecryptionJid(author, repository, logger)
212
+ let decryptionAltJid = null
213
+ const { senderAlt } = extractAddressingContext(stanza)
214
+ if (senderAlt) {
215
+ decryptionAltJid = await getDecryptionJid(senderAlt, repository, logger)
216
+ }
212
217
  if (tag !== "plaintext") {
213
218
  // TODO: Handle hosted devices
214
219
  await storeMappingFromEnvelope(stanza, author, repository, decryptionJid, logger)
@@ -228,6 +233,7 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
228
233
  try {
229
234
  msgBuffer = await repository.decryptMessage({
230
235
  jid: decryptionJid,
236
+ alternateJid: decryptionAltJid,
231
237
  type: e2eType,
232
238
  ciphertext: content,
233
239
  })
@@ -1,7 +1,7 @@
1
1
  import { Boom } from '@hapi/boom'
2
2
  import { createHash, randomBytes } from 'crypto'
3
3
  import { proto } from '../../WAProto/index.js'
4
- const baileysVersion = [2, 3000, 1033105955]
4
+ const baileysVersion = [2, 3000, 1027934701]
5
5
  import { DisconnectReason } from '../Types/index.js'
6
6
  import { getAllBinaryNodeChildren, jidDecode } from '../WABinary/index.js'
7
7
  import { sha256 } from './crypto.js'
@@ -57,13 +57,17 @@ export const getUrlInfo = async (text, opts = {
57
57
  originalThumbnailUrl: image
58
58
  };
59
59
  if (opts.uploadImage) {
60
- const { imageMessage } = await prepareWAMessageMedia({ image: { url: image } }, {
61
- upload: opts.uploadImage,
62
- mediaTypeOverride: 'thumbnail-link',
63
- options: opts.fetchOpts
64
- });
65
- urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : undefined;
66
- urlInfo.highQualityThumbnail = imageMessage || undefined;
60
+ try {
61
+ const { imageMessage } = await prepareWAMessageMedia({ image: { url: image } }, {
62
+ upload: opts.uploadImage,
63
+ mediaTypeOverride: 'thumbnail-link',
64
+ options: opts.fetchOpts
65
+ });
66
+ urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : undefined;
67
+ urlInfo.highQualityThumbnail = imageMessage || undefined;
68
+ } catch (error) {
69
+ opts.logger?.warn({ err: error.message, url: image }, 'failed to upload link preview thumbnail, continuing without it');
70
+ }
67
71
  }
68
72
  else {
69
73
  try {