@twsxtd/baileys 7.0.0-rc.9.commit.7a91cac1d4ec → 7.0.0-rc.9.commit.8d1e795328c7

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.
Files changed (139) hide show
  1. package/WAProto/fix-imports.js +22 -18
  2. package/WAProto/index.js +22 -18
  3. package/lib/Defaults/index.d.ts +2 -0
  4. package/lib/Defaults/index.d.ts.map +1 -1
  5. package/lib/Defaults/index.js +3 -1
  6. package/lib/Defaults/index.js.map +1 -1
  7. package/lib/Signal/libsignal.d.ts.map +1 -1
  8. package/lib/Signal/libsignal.js +15 -12
  9. package/lib/Signal/libsignal.js.map +1 -1
  10. package/lib/Signal/lid-mapping.d.ts +3 -5
  11. package/lib/Signal/lid-mapping.d.ts.map +1 -1
  12. package/lib/Signal/lid-mapping.js +37 -22
  13. package/lib/Signal/lid-mapping.js.map +1 -1
  14. package/lib/Socket/Client/websocket.d.ts.map +1 -1
  15. package/lib/Socket/Client/websocket.js +3 -6
  16. package/lib/Socket/Client/websocket.js.map +1 -1
  17. package/lib/Socket/business.d.ts +7 -4
  18. package/lib/Socket/business.d.ts.map +1 -1
  19. package/lib/Socket/chats.d.ts +9 -3
  20. package/lib/Socket/chats.d.ts.map +1 -1
  21. package/lib/Socket/chats.js +253 -47
  22. package/lib/Socket/chats.js.map +1 -1
  23. package/lib/Socket/communities.d.ts +7 -4
  24. package/lib/Socket/communities.d.ts.map +1 -1
  25. package/lib/Socket/groups.d.ts +8 -4
  26. package/lib/Socket/groups.d.ts.map +1 -1
  27. package/lib/Socket/groups.js +20 -0
  28. package/lib/Socket/groups.js.map +1 -1
  29. package/lib/Socket/index.d.ts +7 -4
  30. package/lib/Socket/index.d.ts.map +1 -1
  31. package/lib/Socket/messages-recv.d.ts +7 -4
  32. package/lib/Socket/messages-recv.d.ts.map +1 -1
  33. package/lib/Socket/messages-recv.js +296 -103
  34. package/lib/Socket/messages-recv.js.map +1 -1
  35. package/lib/Socket/messages-send.d.ts +7 -4
  36. package/lib/Socket/messages-send.d.ts.map +1 -1
  37. package/lib/Socket/messages-send.js +113 -50
  38. package/lib/Socket/messages-send.js.map +1 -1
  39. package/lib/Socket/newsletter.d.ts +6 -3
  40. package/lib/Socket/newsletter.d.ts.map +1 -1
  41. package/lib/Socket/newsletter.js +2 -2
  42. package/lib/Socket/newsletter.js.map +1 -1
  43. package/lib/Socket/socket.d.ts +0 -2
  44. package/lib/Socket/socket.d.ts.map +1 -1
  45. package/lib/Socket/socket.js +15 -9
  46. package/lib/Socket/socket.js.map +1 -1
  47. package/lib/Types/Auth.d.ts +2 -0
  48. package/lib/Types/Auth.d.ts.map +1 -1
  49. package/lib/Types/Call.d.ts +1 -1
  50. package/lib/Types/Call.d.ts.map +1 -1
  51. package/lib/Types/Contact.d.ts +2 -0
  52. package/lib/Types/Contact.d.ts.map +1 -1
  53. package/lib/Types/Events.d.ts +16 -0
  54. package/lib/Types/Events.d.ts.map +1 -1
  55. package/lib/Types/GroupMetadata.d.ts +4 -0
  56. package/lib/Types/GroupMetadata.d.ts.map +1 -1
  57. package/lib/Types/Message.d.ts +15 -2
  58. package/lib/Types/Message.d.ts.map +1 -1
  59. package/lib/Types/Message.js.map +1 -1
  60. package/lib/Types/Newsletter.d.ts +4 -2
  61. package/lib/Types/Newsletter.d.ts.map +1 -1
  62. package/lib/Types/Newsletter.js +4 -2
  63. package/lib/Types/Newsletter.js.map +1 -1
  64. package/lib/Types/Signal.d.ts +1 -2
  65. package/lib/Types/Signal.d.ts.map +1 -1
  66. package/lib/Utils/auth-utils.d.ts +5 -0
  67. package/lib/Utils/auth-utils.d.ts.map +1 -1
  68. package/lib/Utils/auth-utils.js +49 -14
  69. package/lib/Utils/auth-utils.js.map +1 -1
  70. package/lib/Utils/chat-utils.d.ts +30 -0
  71. package/lib/Utils/chat-utils.d.ts.map +1 -1
  72. package/lib/Utils/chat-utils.js +34 -8
  73. package/lib/Utils/chat-utils.js.map +1 -1
  74. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  75. package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
  76. package/lib/Utils/companion-reg-client-utils.js +41 -0
  77. package/lib/Utils/companion-reg-client-utils.js.map +1 -0
  78. package/lib/Utils/decode-wa-message.d.ts +12 -0
  79. package/lib/Utils/decode-wa-message.d.ts.map +1 -1
  80. package/lib/Utils/decode-wa-message.js +22 -0
  81. package/lib/Utils/decode-wa-message.js.map +1 -1
  82. package/lib/Utils/event-buffer.d.ts +0 -7
  83. package/lib/Utils/event-buffer.d.ts.map +1 -1
  84. package/lib/Utils/event-buffer.js +9 -29
  85. package/lib/Utils/event-buffer.js.map +1 -1
  86. package/lib/Utils/generics.d.ts +1 -0
  87. package/lib/Utils/generics.d.ts.map +1 -1
  88. package/lib/Utils/generics.js +22 -1
  89. package/lib/Utils/generics.js.map +1 -1
  90. package/lib/Utils/history.d.ts.map +1 -1
  91. package/lib/Utils/history.js +11 -9
  92. package/lib/Utils/history.js.map +1 -1
  93. package/lib/Utils/identity-change-handler.d.ts +7 -0
  94. package/lib/Utils/identity-change-handler.d.ts.map +1 -1
  95. package/lib/Utils/identity-change-handler.js +2 -6
  96. package/lib/Utils/identity-change-handler.js.map +1 -1
  97. package/lib/Utils/index.d.ts +15 -15
  98. package/lib/Utils/index.d.ts.map +1 -1
  99. package/lib/Utils/index.js +15 -15
  100. package/lib/Utils/index.js.map +1 -1
  101. package/lib/Utils/message-retry-manager.d.ts +1 -5
  102. package/lib/Utils/message-retry-manager.d.ts.map +1 -1
  103. package/lib/Utils/message-retry-manager.js +7 -14
  104. package/lib/Utils/message-retry-manager.js.map +1 -1
  105. package/lib/Utils/messages-media.js +1 -1
  106. package/lib/Utils/messages-media.js.map +1 -1
  107. package/lib/Utils/messages.d.ts.map +1 -1
  108. package/lib/Utils/messages.js +22 -1
  109. package/lib/Utils/messages.js.map +1 -1
  110. package/lib/Utils/process-message.d.ts.map +1 -1
  111. package/lib/Utils/process-message.js +74 -7
  112. package/lib/Utils/process-message.js.map +1 -1
  113. package/lib/Utils/sync-action-utils.d.ts.map +1 -1
  114. package/lib/Utils/sync-action-utils.js +1 -0
  115. package/lib/Utils/sync-action-utils.js.map +1 -1
  116. package/lib/Utils/tc-token-utils.d.ts +26 -1
  117. package/lib/Utils/tc-token-utils.d.ts.map +1 -1
  118. package/lib/Utils/tc-token-utils.js +149 -4
  119. package/lib/Utils/tc-token-utils.js.map +1 -1
  120. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -1
  121. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  122. package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -1
  123. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  124. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
  125. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  126. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
  127. package/lib/WAUSync/Protocols/index.d.ts +1 -0
  128. package/lib/WAUSync/Protocols/index.d.ts.map +1 -1
  129. package/lib/WAUSync/Protocols/index.js +1 -0
  130. package/lib/WAUSync/Protocols/index.js.map +1 -1
  131. package/lib/WAUSync/USyncQuery.d.ts +1 -0
  132. package/lib/WAUSync/USyncQuery.d.ts.map +1 -1
  133. package/lib/WAUSync/USyncQuery.js +5 -1
  134. package/lib/WAUSync/USyncQuery.js.map +1 -1
  135. package/lib/WAUSync/USyncUser.d.ts +4 -0
  136. package/lib/WAUSync/USyncUser.d.ts.map +1 -1
  137. package/lib/WAUSync/USyncUser.js +8 -0
  138. package/lib/WAUSync/USyncUser.js.map +1 -1
  139. package/package.json +5 -3
@@ -5,39 +5,63 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, bindCleanupOnConnectionClose, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
11
  import { buildAckStanza } from '../Utils/stanza-ack.js';
12
+ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
12
13
  import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
13
14
  import { extractGroupMetadata } from './groups.js';
14
15
  import { makeMessagesSocket } from './messages-send.js';
15
16
  export const makeMessagesRecvSocket = (config) => {
16
17
  const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
17
18
  const sock = makeMessagesSocket(config);
18
- const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
19
+ const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, issuePrivacyTokens } = sock;
20
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
19
21
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
20
22
  const retryMutex = makeMutex();
21
- const msgRetryCache = config.msgRetryCounterCache ||
22
- new NodeCache({
23
+ const internalMsgRetryCache = config.msgRetryCounterCache
24
+ ? undefined
25
+ : new NodeCache({
23
26
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
24
- useClones: false,
25
- maxKeys: 5000
27
+ useClones: false
26
28
  });
27
- const callOfferCache = config.callOfferCache ||
28
- new NodeCache({
29
+ const msgRetryCache = config.msgRetryCounterCache || internalMsgRetryCache;
30
+ const internalCallOfferCache = config.callOfferCache
31
+ ? undefined
32
+ : new NodeCache({
29
33
  stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
30
- useClones: false,
31
- maxKeys: 1000
34
+ useClones: false
32
35
  });
33
- const placeholderResendCache = config.placeholderResendCache ||
34
- new NodeCache({
36
+ const callOfferCache = config.callOfferCache || internalCallOfferCache;
37
+ const internalPlaceholderResendCache = config.placeholderResendCache
38
+ ? undefined
39
+ : new NodeCache({
35
40
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
36
- useClones: false,
37
- maxKeys: 5000
41
+ useClones: false
38
42
  });
43
+ const placeholderResendCache = config.placeholderResendCache || internalPlaceholderResendCache;
39
44
  // Debounce identity-change session refreshes per JID to avoid bursts
40
- const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false, maxKeys: 500 });
45
+ const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
46
+ let cleanedUp = false;
47
+ const cleanupInternalCaches = async () => {
48
+ if (cleanedUp) {
49
+ return;
50
+ }
51
+ cleanedUp = true;
52
+ const closers = [Promise.resolve(identityAssertDebounce.close())];
53
+ if (internalMsgRetryCache) {
54
+ closers.push(Promise.resolve(internalMsgRetryCache.close()));
55
+ }
56
+ if (internalCallOfferCache) {
57
+ closers.push(Promise.resolve(internalCallOfferCache.close()));
58
+ }
59
+ if (internalPlaceholderResendCache) {
60
+ closers.push(Promise.resolve(internalPlaceholderResendCache.close()));
61
+ }
62
+ await Promise.allSettled(closers);
63
+ };
64
+ bindCleanupOnConnectionClose(ev, cleanupInternalCaches);
41
65
  let sendActiveReceipts = false;
42
66
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
43
67
  if (!authState.creds.me?.id) {
@@ -66,12 +90,7 @@ export const makeMessagesRecvSocket = (config) => {
66
90
  else {
67
91
  // Store original message data so PDO response handler can preserve
68
92
  // metadata (LID details, timestamps, etc.) that the phone may omit
69
- try {
70
- await placeholderResendCache.set(messageKey?.id, msgData || true);
71
- }
72
- catch {
73
- /* maxKeys exceeded */
74
- }
93
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
75
94
  }
76
95
  await delay(2000);
77
96
  if (!(await placeholderResendCache.get(messageKey?.id))) {
@@ -97,20 +116,34 @@ export const makeMessagesRecvSocket = (config) => {
97
116
  // Handles mex newsletter notifications
98
117
  const handleMexNewsletterNotification = async (node) => {
99
118
  const mexNode = getBinaryNodeChild(node, 'mex');
100
- if (!mexNode?.content) {
119
+ const updateNode = mexNode?.content ? null : getBinaryNodeChild(node, 'update') || getAllBinaryNodeChildren(node)[0];
120
+ const payloadNode = mexNode?.content ? mexNode : updateNode;
121
+ if (!payloadNode?.content) {
101
122
  logger.warn({ node }, 'Invalid mex newsletter notification');
102
123
  return;
103
124
  }
104
125
  let data;
105
126
  try {
106
- data = JSON.parse(mexNode.content.toString());
127
+ const payloadContent = payloadNode.content;
128
+ if (Array.isArray(payloadContent)) {
129
+ logger.warn({ payloadNode }, 'Invalid mex newsletter notification payload format');
130
+ return;
131
+ }
132
+ const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
133
+ data = JSON.parse(contentBuf.toString());
107
134
  }
108
135
  catch (error) {
109
136
  logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
110
137
  return;
111
138
  }
112
- const operation = data?.operation;
113
- const updates = data?.updates;
139
+ const operation = data?.operation ?? payloadNode?.attrs?.op_name;
140
+ let updates = data?.updates;
141
+ if (!updates) {
142
+ const linkedProfiles = data?.data?.xwa2_notify_linked_profiles;
143
+ if (linkedProfiles) {
144
+ updates = [linkedProfiles];
145
+ }
146
+ }
114
147
  if (!updates || !operation) {
115
148
  logger.warn({ data }, 'Invalid mex newsletter notification content');
116
149
  return;
@@ -140,6 +173,22 @@ export const makeMessagesRecvSocket = (config) => {
140
173
  }
141
174
  }
142
175
  break;
176
+ case 'NotificationLinkedProfilesUpdates':
177
+ for (const update of updates) {
178
+ const lid = update?.jid;
179
+ const addedProfiles = Array.isArray(update?.added_profiles) ? update.added_profiles : [];
180
+ const mappings = [];
181
+ for (const profile of addedProfiles) {
182
+ const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
183
+ if (lid && pn) {
184
+ const mapping = { lid, pn };
185
+ ev.emit('lid-mapping.update', mapping);
186
+ mappings.push(mapping);
187
+ }
188
+ }
189
+ await signalRepository.lidMapping.storeLIDPNMappings(mappings);
190
+ }
191
+ break;
143
192
  default:
144
193
  logger.info({ operation, data }, 'Unhandled mex newsletter notification');
145
194
  break;
@@ -268,12 +317,7 @@ export const makeMessagesRecvSocket = (config) => {
268
317
  const retryCount = messageRetryManager.incrementRetryCount(msgId);
269
318
  // Use the new retry count for the rest of the logic
270
319
  const key = `${msgId}:${msgKey?.participant}`;
271
- try {
272
- await msgRetryCache.set(key, retryCount);
273
- }
274
- catch {
275
- /* maxKeys exceeded */
276
- }
320
+ await msgRetryCache.set(key, retryCount);
277
321
  }
278
322
  else {
279
323
  // Fallback to old system
@@ -285,12 +329,7 @@ export const makeMessagesRecvSocket = (config) => {
285
329
  return;
286
330
  }
287
331
  retryCount += 1;
288
- try {
289
- await msgRetryCache.set(key, retryCount);
290
- }
291
- catch {
292
- /* maxKeys exceeded */
293
- }
332
+ await msgRetryCache.set(key, retryCount);
294
333
  }
295
334
  const key = `${msgId}:${msgKey?.participant}`;
296
335
  const retryCount = (await msgRetryCache.get(key)) || 1;
@@ -394,6 +433,35 @@ export const makeMessagesRecvSocket = (config) => {
394
433
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
395
434
  }, authState?.creds?.me?.id || 'sendRetryRequest');
396
435
  };
436
+ /**
437
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
438
+ * Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
439
+ * the session refresh (not after it).
440
+ */
441
+ const reissueTcTokenAfterIdentityChange = (from) => {
442
+ void (async () => {
443
+ const normalizedJid = jidNormalizedUser(from);
444
+ const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
445
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
446
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
447
+ if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
448
+ return;
449
+ }
450
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
451
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
452
+ const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
453
+ const result = await issuePrivacyTokens([issueJid], senderTs);
454
+ await storeTcTokensFromIqResult({
455
+ result,
456
+ fallbackJid: tcJid,
457
+ keys: authState.keys,
458
+ getLIDForPN,
459
+ onNewJidStored: trackTcTokenJid
460
+ });
461
+ })().catch(err => {
462
+ logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
463
+ });
464
+ };
397
465
  const handleEncryptNotification = async (node) => {
398
466
  const from = node.attrs.from;
399
467
  if (from === S_WHATSAPP_NET) {
@@ -412,7 +480,8 @@ export const makeMessagesRecvSocket = (config) => {
412
480
  validateSession: signalRepository.validateSession,
413
481
  assertSessions,
414
482
  debounceCache: identityAssertDebounce,
415
- logger
483
+ logger,
484
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
416
485
  });
417
486
  if (result.action === 'no_identity_node') {
418
487
  logger.info({ node }, 'unknown encrypt notification');
@@ -423,6 +492,7 @@ export const makeMessagesRecvSocket = (config) => {
423
492
  // TODO: Support PN/LID (Here is only LID now)
424
493
  const actingParticipantLid = fullNode.attrs.participant;
425
494
  const actingParticipantPn = fullNode.attrs.participant_pn;
495
+ const actingParticipantUsername = fullNode.attrs.participant_username;
426
496
  const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
427
497
  const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
428
498
  switch (child?.tag) {
@@ -442,7 +512,8 @@ export const makeMessagesRecvSocket = (config) => {
442
512
  {
443
513
  ...metadata,
444
514
  author: actingParticipantLid,
445
- authorPn: actingParticipantPn
515
+ authorPn: actingParticipantPn,
516
+ authorUsername: actingParticipantUsername
446
517
  }
447
518
  ]);
448
519
  break;
@@ -473,6 +544,7 @@ export const makeMessagesRecvSocket = (config) => {
473
544
  id: attrs.jid,
474
545
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
475
546
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
547
+ username: attrs.participant_username || attrs.username || undefined,
476
548
  admin: (attrs.type || null)
477
549
  };
478
550
  });
@@ -698,27 +770,70 @@ export const makeMessagesRecvSocket = (config) => {
698
770
  return result;
699
771
  }
700
772
  };
773
+ /**
774
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
775
+ * Used to coalesce writes during a session; pruning always re-reads the persisted index
776
+ * to cover writes made by other layers (e.g. history sync).
777
+ */
778
+ const tcTokenKnownJids = new Set();
779
+ const tcTokenIndexLoaded = (async () => {
780
+ try {
781
+ const jids = await readTcTokenIndex(authState.keys);
782
+ for (const jid of jids)
783
+ tcTokenKnownJids.add(jid);
784
+ logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
785
+ }
786
+ catch (err) {
787
+ logger.warn({ err: err?.message }, 'failed to load tctoken index');
788
+ }
789
+ })();
790
+ let tcTokenIndexTimer;
791
+ async function flushTcTokenIndex() {
792
+ if (tcTokenIndexTimer) {
793
+ clearTimeout(tcTokenIndexTimer);
794
+ tcTokenIndexTimer = undefined;
795
+ }
796
+ // Merge with whatever is already persisted so we don't clobber writes from other
797
+ // paths (history sync, concurrent sessions on the same store).
798
+ const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
799
+ return authState.keys.set({ tctoken: write });
800
+ }
801
+ function scheduleTcTokenIndexSave() {
802
+ if (tcTokenIndexTimer) {
803
+ clearTimeout(tcTokenIndexTimer);
804
+ }
805
+ tcTokenIndexTimer = setTimeout(() => {
806
+ tcTokenIndexTimer = undefined;
807
+ flushTcTokenIndex().catch(err => {
808
+ logger.warn({ err: err?.message }, 'failed to save tctoken index');
809
+ });
810
+ }, 5000);
811
+ }
812
+ function trackTcTokenJid(jid) {
813
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
814
+ tcTokenKnownJids.add(jid);
815
+ scheduleTcTokenIndexSave();
816
+ }
817
+ }
701
818
  const handlePrivacyTokenNotification = async (node) => {
702
819
  const tokensNode = getBinaryNodeChild(node, 'tokens');
703
- const from = jidNormalizedUser(node.attrs.from);
704
820
  if (!tokensNode)
705
821
  return;
706
- const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
707
- for (const tokenNode of tokenNodes) {
708
- const { attrs, content } = tokenNode;
709
- const type = attrs.type;
710
- const timestamp = attrs.t;
711
- if (type === 'trusted_contact' && content instanceof Buffer) {
712
- logger.debug({
713
- from,
714
- timestamp,
715
- tcToken: content
716
- }, 'received trusted contact token');
717
- await authState.keys.set({
718
- tctoken: { [from]: { token: content, timestamp } }
719
- });
720
- }
721
- }
822
+ const from = jidNormalizedUser(node.attrs.from);
823
+ // WA Web uses: senderLid ?? toLid(from) for the storage key
824
+ // The sender_lid attribute provides the LID directly when available
825
+ const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
826
+ ? jidNormalizedUser(node.attrs.sender_lid)
827
+ : undefined;
828
+ const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
829
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
830
+ await storeTcTokensFromIqResult({
831
+ result: node,
832
+ fallbackJid,
833
+ keys: authState.keys,
834
+ getLIDForPN,
835
+ onNewJidStored: trackTcTokenJid
836
+ });
722
837
  };
723
838
  async function decipherLinkPublicKey(data) {
724
839
  const buffer = toRequiredBuffer(data);
@@ -742,12 +857,7 @@ export const makeMessagesRecvSocket = (config) => {
742
857
  const updateSendMessageAgainCount = async (id, participant) => {
743
858
  const key = `${id}:${participant}`;
744
859
  const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
745
- try {
746
- await msgRetryCache.set(key, newValue);
747
- }
748
- catch {
749
- /* maxKeys exceeded */
750
- }
860
+ await msgRetryCache.set(key, newValue);
751
861
  };
752
862
  const sendMessagesAgain = async (key, ids, retryNode) => {
753
863
  const remoteJid = key.remoteJid;
@@ -842,11 +952,6 @@ export const makeMessagesRecvSocket = (config) => {
842
952
  fromMe,
843
953
  participant: attrs.participant
844
954
  };
845
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
846
- logger.debug({ remoteJid }, 'ignoring receipt from jid');
847
- await sendMessageAck(node);
848
- return;
849
- }
850
955
  const ids = [attrs.id];
851
956
  if (Array.isArray(content)) {
852
957
  const items = getBinaryNodeChildren(content[0], 'item');
@@ -911,11 +1016,6 @@ export const makeMessagesRecvSocket = (config) => {
911
1016
  };
912
1017
  const handleNotification = async (node) => {
913
1018
  const remoteJid = node.attrs.from;
914
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
915
- logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
916
- await sendMessageAck(node);
917
- return;
918
- }
919
1019
  try {
920
1020
  await Promise.all([
921
1021
  notificationMutex.mutex(async () => {
@@ -928,6 +1028,7 @@ export const makeMessagesRecvSocket = (config) => {
928
1028
  fromMe,
929
1029
  participant: node.attrs.participant,
930
1030
  participantAlt,
1031
+ participantUsername: node.attrs.participant_username,
931
1032
  addressingMode,
932
1033
  id: node.attrs.id,
933
1034
  ...(msg.key || {})
@@ -945,11 +1046,6 @@ export const makeMessagesRecvSocket = (config) => {
945
1046
  }
946
1047
  };
947
1048
  const handleMessage = async (node) => {
948
- if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
949
- logger.debug({ key: node.attrs.key }, 'ignored message');
950
- await sendMessageAck(node, NACK_REASONS.UnhandledError);
951
- return;
952
- }
953
1049
  const encNode = getBinaryNodeChild(node, 'enc');
954
1050
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
955
1051
  if (encNode?.attrs.type === 'msmsg') {
@@ -1167,16 +1263,18 @@ export const makeMessagesRecvSocket = (config) => {
1167
1263
  offline: !!attrs.offline,
1168
1264
  status
1169
1265
  };
1266
+ if (status === 'relaylatency') {
1267
+ const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms'];
1268
+ const latencyMs = latencyValue ? Number(latencyValue) : undefined;
1269
+ if (Number.isFinite(latencyMs)) {
1270
+ call.latencyMs = latencyMs;
1271
+ }
1272
+ }
1170
1273
  if (status === 'offer') {
1171
1274
  call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1172
1275
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
1173
1276
  call.groupJid = infoChild.attrs['group-jid'];
1174
- try {
1175
- await callOfferCache.set(call.id, call);
1176
- }
1177
- catch {
1178
- /* maxKeys exceeded */
1179
- }
1277
+ await callOfferCache.set(call.id, call);
1180
1278
  }
1181
1279
  const existingCall = await callOfferCache.get(call.id);
1182
1280
  // use existing call info to populate this event
@@ -1216,7 +1314,19 @@ export const makeMessagesRecvSocket = (config) => {
1216
1314
  // error in acknowledgement,
1217
1315
  // device could not display the message
1218
1316
  if (attrs.error) {
1219
- logger.warn({ attrs }, 'received error in ack');
1317
+ if (attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
1318
+ // 463 = account restricted + no tctoken for this contact.
1319
+ // WA Web prevents this client-side (disables compose bar).
1320
+ // No retry — retrying worsens the restriction by counting
1321
+ // as another "reach out" to an unknown contact.
1322
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
1323
+ }
1324
+ else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
1325
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
1326
+ }
1327
+ else {
1328
+ logger.warn({ attrs }, 'received error in ack');
1329
+ }
1220
1330
  ev.emit('messages.update', [
1221
1331
  {
1222
1332
  key,
@@ -1226,19 +1336,6 @@ export const makeMessagesRecvSocket = (config) => {
1226
1336
  }
1227
1337
  }
1228
1338
  ]);
1229
- // resend the message with device_fanout=false, use at your own risk
1230
- // if (attrs.error === '475') {
1231
- // const msg = await getMessage(key)
1232
- // if (msg) {
1233
- // await relayMessage(key.remoteJid!, msg, {
1234
- // messageId: key.id!,
1235
- // useUserDevicesCache: false,
1236
- // additionalAttributes: {
1237
- // device_fanout: 'false'
1238
- // }
1239
- // })
1240
- // }
1241
- // }
1242
1339
  }
1243
1340
  };
1244
1341
  /// processes a node with the given function
@@ -1262,6 +1359,19 @@ export const makeMessagesRecvSocket = (config) => {
1262
1359
  yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1263
1360
  });
1264
1361
  const processNode = async (type, node, identifier, exec) => {
1362
+ // Fast path: ack and drop ignored JIDs before entering the buffer/queue
1363
+ const from = node.attrs.from;
1364
+ let ignoreJid = from;
1365
+ if (type === 'receipt' && from) {
1366
+ const attrs = node.attrs;
1367
+ const isLid = attrs.from.includes('lid');
1368
+ const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
1369
+ ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
1370
+ }
1371
+ if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
1372
+ await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined);
1373
+ return;
1374
+ }
1265
1375
  const isOffline = !!node.attrs.offline;
1266
1376
  if (isOffline) {
1267
1377
  offlineNodeProcessor.enqueue(type, node);
@@ -1317,21 +1427,104 @@ export const makeMessagesRecvSocket = (config) => {
1317
1427
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1318
1428
  }
1319
1429
  });
1430
+ /** timestamp of last tctoken prune run — throttles to once per 24h */
1431
+ let lastTcTokenPruneTs = 0;
1320
1432
  ev.on('connection.update', ({ isOnline, connection }) => {
1321
1433
  if (typeof isOnline !== 'undefined') {
1322
1434
  sendActiveReceipts = isOnline;
1323
1435
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1324
1436
  }
1325
- // Clean up local caches when connection closes
1326
- if (connection === 'close') {
1327
- msgRetryCache.flushAll();
1328
- callOfferCache.flushAll();
1329
- placeholderResendCache.flushAll();
1330
- identityAssertDebounce.flushAll();
1437
+ // Flush pending tctoken index save on disconnect to avoid writing after close
1438
+ if (connection === 'close' && tcTokenIndexTimer) {
1439
+ clearTimeout(tcTokenIndexTimer);
1440
+ tcTokenIndexTimer = undefined;
1441
+ // Best-effort flush — may fail if store is already closed
1442
+ try {
1443
+ void Promise.resolve(flushTcTokenIndex()).catch(() => { });
1444
+ }
1445
+ catch {
1446
+ /* ignore sync errors */
1447
+ }
1448
+ }
1449
+ // Prune expired tctokens when coming online, at most once per 24 hours
1450
+ // Matches WA Web's CLEAN_TC_TOKENS task
1451
+ // Note: don't gate on tcTokenKnownJids.size — the index may still be loading
1452
+ if (isOnline) {
1453
+ const now = Date.now();
1454
+ const DAY_MS = 24 * 60 * 60 * 1000;
1455
+ if (now - lastTcTokenPruneTs >= DAY_MS) {
1456
+ lastTcTokenPruneTs = now;
1457
+ void pruneExpiredTcTokens();
1458
+ }
1331
1459
  }
1332
1460
  });
1461
+ async function pruneExpiredTcTokens() {
1462
+ try {
1463
+ await tcTokenIndexLoaded;
1464
+ // Union with the persisted index picks up JIDs added by other layers
1465
+ // (history sync) without needing inter-module wiring.
1466
+ const persisted = await readTcTokenIndex(authState.keys);
1467
+ const allJids = new Set(tcTokenKnownJids);
1468
+ for (const jid of persisted)
1469
+ allJids.add(jid);
1470
+ if (!allJids.size)
1471
+ return;
1472
+ const jids = [...allJids];
1473
+ const allTokens = await authState.keys.get('tctoken', jids);
1474
+ const writes = {};
1475
+ const survivors = new Set();
1476
+ let mutated = 0;
1477
+ for (const jid of jids) {
1478
+ const entry = allTokens[jid];
1479
+ if (!entry) {
1480
+ // Tracked but nothing in store — drop from index.
1481
+ mutated++;
1482
+ continue;
1483
+ }
1484
+ const hasPeerToken = !!entry.token?.length;
1485
+ const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
1486
+ const hasSenderTs = entry.senderTimestamp !== undefined;
1487
+ const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
1488
+ const keepPeerToken = hasPeerToken && !peerTokenExpired;
1489
+ const keepSenderTs = hasSenderTs && !senderTsExpired;
1490
+ if (!keepPeerToken && !keepSenderTs) {
1491
+ writes[jid] = null;
1492
+ mutated++;
1493
+ }
1494
+ else if (peerTokenExpired && keepSenderTs) {
1495
+ writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
1496
+ survivors.add(jid);
1497
+ mutated++;
1498
+ }
1499
+ else {
1500
+ survivors.add(jid);
1501
+ }
1502
+ }
1503
+ if (mutated === 0)
1504
+ return;
1505
+ await authState.keys.set({
1506
+ tctoken: {
1507
+ ...writes,
1508
+ [TC_TOKEN_INDEX_KEY]: {
1509
+ token: Buffer.from(JSON.stringify([...survivors]))
1510
+ }
1511
+ }
1512
+ });
1513
+ tcTokenKnownJids.clear();
1514
+ for (const jid of survivors)
1515
+ tcTokenKnownJids.add(jid);
1516
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
1517
+ }
1518
+ catch (err) {
1519
+ logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
1520
+ }
1521
+ }
1333
1522
  return {
1334
1523
  ...sock,
1524
+ end: async (error) => {
1525
+ await cleanupInternalCaches();
1526
+ await sock.end(error);
1527
+ },
1335
1528
  sendMessageAck,
1336
1529
  sendRetryRequest,
1337
1530
  rejectCall,