@twsxtd/baileys 7.0.0-rc.9.commit.71ed75d4aae0 → 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 +11 -3
  9. package/lib/Signal/libsignal.js.map +1 -1
  10. package/lib/Signal/lid-mapping.d.ts +3 -1
  11. package/lib/Signal/lid-mapping.d.ts.map +1 -1
  12. package/lib/Signal/lid-mapping.js +34 -14
  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 -5
  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 +252 -35
  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 +289 -61
  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 +106 -12
  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 -8
  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 -1
  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 +45 -0
  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 -2
  83. package/lib/Utils/event-buffer.d.ts.map +1 -1
  84. package/lib/Utils/event-buffer.js +3 -16
  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 +1 -0
  96. package/lib/Utils/identity-change-handler.js.map +1 -1
  97. package/lib/Utils/index.d.ts +1 -0
  98. package/lib/Utils/index.d.ts.map +1 -1
  99. package/lib/Utils/index.js +1 -0
  100. package/lib/Utils/index.js.map +1 -1
  101. package/lib/Utils/message-retry-manager.d.ts +1 -0
  102. package/lib/Utils/message-retry-manager.d.ts.map +1 -1
  103. package/lib/Utils/message-retry-manager.js +10 -0
  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 +70 -1
  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 +3 -3
@@ -5,36 +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
27
  useClones: false
25
28
  });
26
- const callOfferCache = config.callOfferCache ||
27
- new NodeCache({
29
+ const msgRetryCache = config.msgRetryCounterCache || internalMsgRetryCache;
30
+ const internalCallOfferCache = config.callOfferCache
31
+ ? undefined
32
+ : new NodeCache({
28
33
  stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
29
34
  useClones: false
30
35
  });
31
- const placeholderResendCache = config.placeholderResendCache ||
32
- new NodeCache({
36
+ const callOfferCache = config.callOfferCache || internalCallOfferCache;
37
+ const internalPlaceholderResendCache = config.placeholderResendCache
38
+ ? undefined
39
+ : new NodeCache({
33
40
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
34
41
  useClones: false
35
42
  });
43
+ const placeholderResendCache = config.placeholderResendCache || internalPlaceholderResendCache;
36
44
  // Debounce identity-change session refreshes per JID to avoid bursts
37
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);
38
65
  let sendActiveReceipts = false;
39
66
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
40
67
  if (!authState.creds.me?.id) {
@@ -89,20 +116,34 @@ export const makeMessagesRecvSocket = (config) => {
89
116
  // Handles mex newsletter notifications
90
117
  const handleMexNewsletterNotification = async (node) => {
91
118
  const mexNode = getBinaryNodeChild(node, 'mex');
92
- 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) {
93
122
  logger.warn({ node }, 'Invalid mex newsletter notification');
94
123
  return;
95
124
  }
96
125
  let data;
97
126
  try {
98
- 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());
99
134
  }
100
135
  catch (error) {
101
136
  logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
102
137
  return;
103
138
  }
104
- const operation = data?.operation;
105
- 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
+ }
106
147
  if (!updates || !operation) {
107
148
  logger.warn({ data }, 'Invalid mex newsletter notification content');
108
149
  return;
@@ -132,6 +173,22 @@ export const makeMessagesRecvSocket = (config) => {
132
173
  }
133
174
  }
134
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;
135
192
  default:
136
193
  logger.info({ operation, data }, 'Unhandled mex newsletter notification');
137
194
  break;
@@ -376,6 +433,35 @@ export const makeMessagesRecvSocket = (config) => {
376
433
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
377
434
  }, authState?.creds?.me?.id || 'sendRetryRequest');
378
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
+ };
379
465
  const handleEncryptNotification = async (node) => {
380
466
  const from = node.attrs.from;
381
467
  if (from === S_WHATSAPP_NET) {
@@ -394,7 +480,8 @@ export const makeMessagesRecvSocket = (config) => {
394
480
  validateSession: signalRepository.validateSession,
395
481
  assertSessions,
396
482
  debounceCache: identityAssertDebounce,
397
- logger
483
+ logger,
484
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
398
485
  });
399
486
  if (result.action === 'no_identity_node') {
400
487
  logger.info({ node }, 'unknown encrypt notification');
@@ -405,6 +492,7 @@ export const makeMessagesRecvSocket = (config) => {
405
492
  // TODO: Support PN/LID (Here is only LID now)
406
493
  const actingParticipantLid = fullNode.attrs.participant;
407
494
  const actingParticipantPn = fullNode.attrs.participant_pn;
495
+ const actingParticipantUsername = fullNode.attrs.participant_username;
408
496
  const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
409
497
  const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
410
498
  switch (child?.tag) {
@@ -424,7 +512,8 @@ export const makeMessagesRecvSocket = (config) => {
424
512
  {
425
513
  ...metadata,
426
514
  author: actingParticipantLid,
427
- authorPn: actingParticipantPn
515
+ authorPn: actingParticipantPn,
516
+ authorUsername: actingParticipantUsername
428
517
  }
429
518
  ]);
430
519
  break;
@@ -455,6 +544,7 @@ export const makeMessagesRecvSocket = (config) => {
455
544
  id: attrs.jid,
456
545
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
457
546
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
547
+ username: attrs.participant_username || attrs.username || undefined,
458
548
  admin: (attrs.type || null)
459
549
  };
460
550
  });
@@ -680,27 +770,70 @@ export const makeMessagesRecvSocket = (config) => {
680
770
  return result;
681
771
  }
682
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
+ }
683
818
  const handlePrivacyTokenNotification = async (node) => {
684
819
  const tokensNode = getBinaryNodeChild(node, 'tokens');
685
- const from = jidNormalizedUser(node.attrs.from);
686
820
  if (!tokensNode)
687
821
  return;
688
- const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
689
- for (const tokenNode of tokenNodes) {
690
- const { attrs, content } = tokenNode;
691
- const type = attrs.type;
692
- const timestamp = attrs.t;
693
- if (type === 'trusted_contact' && content instanceof Buffer) {
694
- logger.debug({
695
- from,
696
- timestamp,
697
- tcToken: content
698
- }, 'received trusted contact token');
699
- await authState.keys.set({
700
- tctoken: { [from]: { token: content, timestamp } }
701
- });
702
- }
703
- }
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
+ });
704
837
  };
705
838
  async function decipherLinkPublicKey(data) {
706
839
  const buffer = toRequiredBuffer(data);
@@ -819,11 +952,6 @@ export const makeMessagesRecvSocket = (config) => {
819
952
  fromMe,
820
953
  participant: attrs.participant
821
954
  };
822
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
823
- logger.debug({ remoteJid }, 'ignoring receipt from jid');
824
- await sendMessageAck(node);
825
- return;
826
- }
827
955
  const ids = [attrs.id];
828
956
  if (Array.isArray(content)) {
829
957
  const items = getBinaryNodeChildren(content[0], 'item');
@@ -888,11 +1016,6 @@ export const makeMessagesRecvSocket = (config) => {
888
1016
  };
889
1017
  const handleNotification = async (node) => {
890
1018
  const remoteJid = node.attrs.from;
891
- if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
892
- logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
893
- await sendMessageAck(node);
894
- return;
895
- }
896
1019
  try {
897
1020
  await Promise.all([
898
1021
  notificationMutex.mutex(async () => {
@@ -905,6 +1028,7 @@ export const makeMessagesRecvSocket = (config) => {
905
1028
  fromMe,
906
1029
  participant: node.attrs.participant,
907
1030
  participantAlt,
1031
+ participantUsername: node.attrs.participant_username,
908
1032
  addressingMode,
909
1033
  id: node.attrs.id,
910
1034
  ...(msg.key || {})
@@ -922,11 +1046,6 @@ export const makeMessagesRecvSocket = (config) => {
922
1046
  }
923
1047
  };
924
1048
  const handleMessage = async (node) => {
925
- if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
926
- logger.debug({ key: node.attrs.key }, 'ignored message');
927
- await sendMessageAck(node, NACK_REASONS.UnhandledError);
928
- return;
929
- }
930
1049
  const encNode = getBinaryNodeChild(node, 'enc');
931
1050
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
932
1051
  if (encNode?.attrs.type === 'msmsg') {
@@ -1144,6 +1263,13 @@ export const makeMessagesRecvSocket = (config) => {
1144
1263
  offline: !!attrs.offline,
1145
1264
  status
1146
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
+ }
1147
1273
  if (status === 'offer') {
1148
1274
  call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1149
1275
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
@@ -1188,7 +1314,19 @@ export const makeMessagesRecvSocket = (config) => {
1188
1314
  // error in acknowledgement,
1189
1315
  // device could not display the message
1190
1316
  if (attrs.error) {
1191
- 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
+ }
1192
1330
  ev.emit('messages.update', [
1193
1331
  {
1194
1332
  key,
@@ -1198,19 +1336,6 @@ export const makeMessagesRecvSocket = (config) => {
1198
1336
  }
1199
1337
  }
1200
1338
  ]);
1201
- // resend the message with device_fanout=false, use at your own risk
1202
- // if (attrs.error === '475') {
1203
- // const msg = await getMessage(key)
1204
- // if (msg) {
1205
- // await relayMessage(key.remoteJid!, msg, {
1206
- // messageId: key.id!,
1207
- // useUserDevicesCache: false,
1208
- // additionalAttributes: {
1209
- // device_fanout: 'false'
1210
- // }
1211
- // })
1212
- // }
1213
- // }
1214
1339
  }
1215
1340
  };
1216
1341
  /// processes a node with the given function
@@ -1234,6 +1359,19 @@ export const makeMessagesRecvSocket = (config) => {
1234
1359
  yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1235
1360
  });
1236
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
+ }
1237
1375
  const isOffline = !!node.attrs.offline;
1238
1376
  if (isOffline) {
1239
1377
  offlineNodeProcessor.enqueue(type, node);
@@ -1289,14 +1427,104 @@ export const makeMessagesRecvSocket = (config) => {
1289
1427
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1290
1428
  }
1291
1429
  });
1292
- ev.on('connection.update', ({ isOnline }) => {
1430
+ /** timestamp of last tctoken prune run — throttles to once per 24h */
1431
+ let lastTcTokenPruneTs = 0;
1432
+ ev.on('connection.update', ({ isOnline, connection }) => {
1293
1433
  if (typeof isOnline !== 'undefined') {
1294
1434
  sendActiveReceipts = isOnline;
1295
1435
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1296
1436
  }
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
+ }
1459
+ }
1297
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
+ }
1298
1522
  return {
1299
1523
  ...sock,
1524
+ end: async (error) => {
1525
+ await cleanupInternalCaches();
1526
+ await sock.end(error);
1527
+ },
1300
1528
  sendMessageAck,
1301
1529
  sendRetryRequest,
1302
1530
  rejectCall,