@queenanya/baileys 9.2.4 → 9.4.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.
Files changed (299) hide show
  1. package/README.md +329 -1237
  2. package/WAProto/fix-imports.js +22 -18
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/index.d.ts +17 -0
  5. package/lib/Defaults/index.d.ts.map +1 -1
  6. package/lib/Defaults/index.js +29 -3
  7. package/lib/Defaults/index.js.map +1 -1
  8. package/lib/Signal/libsignal.d.ts.map +1 -1
  9. package/lib/Signal/libsignal.js +61 -2
  10. package/lib/Signal/libsignal.js.map +1 -1
  11. package/lib/Signal/lid-mapping.d.ts +5 -9
  12. package/lib/Signal/lid-mapping.d.ts.map +1 -1
  13. package/lib/Signal/lid-mapping.js +170 -70
  14. package/lib/Signal/lid-mapping.js.map +1 -1
  15. package/lib/Socket/business.d.ts +112 -2
  16. package/lib/Socket/business.d.ts.map +1 -1
  17. package/lib/Socket/chats.d.ts +10 -0
  18. package/lib/Socket/chats.d.ts.map +1 -1
  19. package/lib/Socket/chats.js +243 -38
  20. package/lib/Socket/chats.js.map +1 -1
  21. package/lib/Socket/communities.d.ts +112 -2
  22. package/lib/Socket/communities.d.ts.map +1 -1
  23. package/lib/Socket/groups.d.ts +7 -0
  24. package/lib/Socket/groups.d.ts.map +1 -1
  25. package/lib/Socket/groups.js +6 -0
  26. package/lib/Socket/groups.js.map +1 -1
  27. package/lib/Socket/index.d.ts +112 -2
  28. package/lib/Socket/index.d.ts.map +1 -1
  29. package/lib/Socket/index.js +0 -6
  30. package/lib/Socket/index.js.map +1 -1
  31. package/lib/Socket/messages-recv.d.ts +113 -3
  32. package/lib/Socket/messages-recv.d.ts.map +1 -1
  33. package/lib/Socket/messages-recv.js +739 -150
  34. package/lib/Socket/messages-recv.js.map +1 -1
  35. package/lib/Socket/messages-send.d.ts +116 -4
  36. package/lib/Socket/messages-send.d.ts.map +1 -1
  37. package/lib/Socket/messages-send.js +328 -86
  38. package/lib/Socket/messages-send.js.map +1 -1
  39. package/lib/Socket/newsletter.d.ts +7 -0
  40. package/lib/Socket/newsletter.d.ts.map +1 -1
  41. package/lib/Socket/socket.d.ts +2 -0
  42. package/lib/Socket/socket.d.ts.map +1 -1
  43. package/lib/Socket/socket.js +142 -16
  44. package/lib/Socket/socket.js.map +1 -1
  45. package/lib/Types/Auth.d.ts +2 -0
  46. package/lib/Types/Auth.d.ts.map +1 -1
  47. package/lib/Types/Call.d.ts +10 -1
  48. package/lib/Types/Call.d.ts.map +1 -1
  49. package/lib/Types/Contact.d.ts +2 -0
  50. package/lib/Types/Contact.d.ts.map +1 -1
  51. package/lib/Types/Events.d.ts +21 -1
  52. package/lib/Types/Events.d.ts.map +1 -1
  53. package/lib/Types/GroupMetadata.d.ts +4 -0
  54. package/lib/Types/GroupMetadata.d.ts.map +1 -1
  55. package/lib/Types/Message.d.ts +530 -16
  56. package/lib/Types/Message.d.ts.map +1 -1
  57. package/lib/Types/Message.js.map +1 -1
  58. package/lib/Types/Newsletter.d.ts +33 -29
  59. package/lib/Types/Newsletter.d.ts.map +1 -1
  60. package/lib/Types/Newsletter.js +25 -23
  61. package/lib/Types/Newsletter.js.map +1 -1
  62. package/lib/Types/State.d.ts +54 -0
  63. package/lib/Types/State.d.ts.map +1 -1
  64. package/lib/Types/State.js +42 -0
  65. package/lib/Types/State.js.map +1 -1
  66. package/lib/Types/index.d.ts +9 -0
  67. package/lib/Types/index.d.ts.map +1 -1
  68. package/lib/Types/index.js.map +1 -1
  69. package/lib/Utils/browser-utils.d.ts +13 -0
  70. package/lib/Utils/browser-utils.d.ts.map +1 -1
  71. package/lib/Utils/browser-utils.js +90 -10
  72. package/lib/Utils/browser-utils.js.map +1 -1
  73. package/lib/Utils/chat-utils.d.ts +30 -0
  74. package/lib/Utils/chat-utils.d.ts.map +1 -1
  75. package/lib/Utils/chat-utils.js +81 -52
  76. package/lib/Utils/chat-utils.js.map +1 -1
  77. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  78. package/lib/Utils/companion-reg-client-utils.d.ts.map +1 -0
  79. package/lib/Utils/companion-reg-client-utils.js +34 -0
  80. package/lib/Utils/companion-reg-client-utils.js.map +1 -0
  81. package/lib/Utils/crypto.d.ts +4 -8
  82. package/lib/Utils/crypto.d.ts.map +1 -1
  83. package/lib/Utils/crypto.js +2 -26
  84. package/lib/Utils/crypto.js.map +1 -1
  85. package/lib/Utils/decode-wa-message.d.ts +12 -0
  86. package/lib/Utils/decode-wa-message.d.ts.map +1 -1
  87. package/lib/Utils/decode-wa-message.js +16 -0
  88. package/lib/Utils/decode-wa-message.js.map +1 -1
  89. package/lib/Utils/event-buffer.js +10 -1
  90. package/lib/Utils/event-buffer.js.map +1 -1
  91. package/lib/Utils/generics.d.ts +3 -1
  92. package/lib/Utils/generics.d.ts.map +1 -1
  93. package/lib/Utils/generics.js +16 -3
  94. package/lib/Utils/generics.js.map +1 -1
  95. package/lib/Utils/history.d.ts +5 -2
  96. package/lib/Utils/history.d.ts.map +1 -1
  97. package/lib/Utils/history.js +53 -17
  98. package/lib/Utils/history.js.map +1 -1
  99. package/lib/Utils/identity-change-handler.d.ts +44 -0
  100. package/lib/Utils/identity-change-handler.d.ts.map +1 -0
  101. package/lib/Utils/identity-change-handler.js +50 -0
  102. package/lib/Utils/identity-change-handler.js.map +1 -0
  103. package/lib/Utils/index.d.ts +6 -0
  104. package/lib/Utils/index.d.ts.map +1 -1
  105. package/lib/Utils/index.js +6 -0
  106. package/lib/Utils/index.js.map +1 -1
  107. package/lib/Utils/interactive-message.d.ts +201 -0
  108. package/lib/Utils/interactive-message.d.ts.map +1 -0
  109. package/lib/Utils/interactive-message.js +256 -0
  110. package/lib/Utils/interactive-message.js.map +1 -0
  111. package/lib/Utils/lt-hash.d.ts +7 -12
  112. package/lib/Utils/lt-hash.d.ts.map +1 -1
  113. package/lib/Utils/lt-hash.js +2 -42
  114. package/lib/Utils/lt-hash.js.map +1 -1
  115. package/lib/Utils/message-composer.d.ts +5 -0
  116. package/lib/Utils/message-composer.d.ts.map +1 -0
  117. package/lib/Utils/message-composer.js +5 -0
  118. package/lib/Utils/message-composer.js.map +1 -0
  119. package/lib/Utils/message-retry-manager.d.ts +30 -2
  120. package/lib/Utils/message-retry-manager.d.ts.map +1 -1
  121. package/lib/Utils/message-retry-manager.js +59 -2
  122. package/lib/Utils/message-retry-manager.js.map +1 -1
  123. package/lib/Utils/messages-media.d.ts +19 -5
  124. package/lib/Utils/messages-media.d.ts.map +1 -1
  125. package/lib/Utils/messages-media.js +26 -17
  126. package/lib/Utils/messages-media.js.map +1 -1
  127. package/lib/Utils/messages.d.ts.map +1 -1
  128. package/lib/Utils/messages.js +433 -13
  129. package/lib/Utils/messages.js.map +1 -1
  130. package/lib/Utils/noise-handler.d.ts +2 -2
  131. package/lib/Utils/noise-handler.d.ts.map +1 -1
  132. package/lib/Utils/noise-handler.js +10 -10
  133. package/lib/Utils/noise-handler.js.map +1 -1
  134. package/lib/Utils/offline-node-processor.d.ts +17 -0
  135. package/lib/Utils/offline-node-processor.d.ts.map +1 -0
  136. package/lib/Utils/offline-node-processor.js +40 -0
  137. package/lib/Utils/offline-node-processor.js.map +1 -0
  138. package/lib/Utils/process-message.d.ts.map +1 -1
  139. package/lib/Utils/process-message.js +96 -16
  140. package/lib/Utils/process-message.js.map +1 -1
  141. package/lib/Utils/reporting-utils.js +2 -2
  142. package/lib/Utils/reporting-utils.js.map +1 -1
  143. package/lib/Utils/stanza-ack.d.ts +11 -0
  144. package/lib/Utils/stanza-ack.d.ts.map +1 -0
  145. package/lib/Utils/stanza-ack.js +38 -0
  146. package/lib/Utils/stanza-ack.js.map +1 -0
  147. package/lib/Utils/sync-action-utils.d.ts.map +1 -1
  148. package/lib/Utils/sync-action-utils.js +2 -1
  149. package/lib/Utils/sync-action-utils.js.map +1 -1
  150. package/lib/Utils/tc-token-utils.d.ts +26 -1
  151. package/lib/Utils/tc-token-utils.d.ts.map +1 -1
  152. package/lib/Utils/tc-token-utils.js +149 -4
  153. package/lib/Utils/tc-token-utils.js.map +1 -1
  154. package/lib/Utils/use-mongo-file-auth-state.d.ts +16 -0
  155. package/lib/Utils/use-mongo-file-auth-state.d.ts.map +1 -0
  156. package/lib/Utils/use-mongo-file-auth-state.js +60 -0
  157. package/lib/Utils/use-mongo-file-auth-state.js.map +1 -0
  158. package/lib/Utils/use-multi-file-auth-state.js +1 -1
  159. package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
  160. package/lib/Utils/validate-connection.d.ts.map +1 -1
  161. package/lib/Utils/validate-connection.js +11 -1
  162. package/lib/Utils/validate-connection.js.map +1 -1
  163. package/lib/WABinary/generic-utils.d.ts +9 -0
  164. package/lib/WABinary/generic-utils.d.ts.map +1 -1
  165. package/lib/WABinary/generic-utils.js +23 -0
  166. package/lib/WABinary/generic-utils.js.map +1 -1
  167. package/lib/WABinary/jid-utils.js.map +1 -1
  168. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts.map +1 -1
  169. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  170. package/lib/WAUSync/Protocols/USyncContactProtocol.js.map +1 -1
  171. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  172. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts.map +1 -0
  173. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  174. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js.map +1 -0
  175. package/lib/WAUSync/Protocols/index.d.ts +1 -0
  176. package/lib/WAUSync/Protocols/index.d.ts.map +1 -1
  177. package/lib/WAUSync/Protocols/index.js +1 -0
  178. package/lib/WAUSync/Protocols/index.js.map +1 -1
  179. package/lib/WAUSync/USyncQuery.d.ts +1 -0
  180. package/lib/WAUSync/USyncQuery.d.ts.map +1 -1
  181. package/lib/WAUSync/USyncQuery.js +5 -1
  182. package/lib/WAUSync/USyncQuery.js.map +1 -1
  183. package/lib/WAUSync/USyncUser.d.ts +4 -0
  184. package/lib/WAUSync/USyncUser.d.ts.map +1 -1
  185. package/lib/WAUSync/USyncUser.js +8 -0
  186. package/lib/WAUSync/USyncUser.js.map +1 -1
  187. package/lib/addons/anti-delete.d.ts +72 -0
  188. package/lib/addons/anti-delete.d.ts.map +1 -0
  189. package/lib/addons/anti-delete.js +165 -0
  190. package/lib/addons/anti-delete.js.map +1 -0
  191. package/lib/addons/auto-reply.d.ts +67 -0
  192. package/lib/addons/auto-reply.d.ts.map +1 -0
  193. package/lib/addons/auto-reply.js +145 -0
  194. package/lib/addons/auto-reply.js.map +1 -0
  195. package/lib/addons/browser-presets.d.ts +16 -0
  196. package/lib/addons/browser-presets.d.ts.map +1 -0
  197. package/lib/addons/browser-presets.js +24 -0
  198. package/lib/addons/browser-presets.js.map +1 -0
  199. package/lib/addons/button-sender.d.ts +260 -0
  200. package/lib/addons/button-sender.d.ts.map +1 -0
  201. package/lib/addons/button-sender.js +771 -0
  202. package/lib/addons/button-sender.js.map +1 -0
  203. package/lib/addons/call-handler.d.ts +79 -0
  204. package/lib/addons/call-handler.d.ts.map +1 -0
  205. package/lib/addons/call-handler.js +342 -0
  206. package/lib/addons/call-handler.js.map +1 -0
  207. package/lib/addons/from-chats.d.ts +30 -0
  208. package/lib/addons/from-chats.d.ts.map +1 -0
  209. package/lib/addons/from-chats.js +38 -0
  210. package/lib/addons/from-chats.js.map +1 -0
  211. package/lib/addons/from-messages-recv.d.ts +59 -0
  212. package/lib/addons/from-messages-recv.d.ts.map +1 -0
  213. package/lib/addons/from-messages-recv.js +326 -0
  214. package/lib/addons/from-messages-recv.js.map +1 -0
  215. package/lib/addons/from-messages-send.d.ts +50 -0
  216. package/lib/addons/from-messages-send.d.ts.map +1 -0
  217. package/lib/addons/from-messages-send.js +148 -0
  218. package/lib/addons/from-messages-send.js.map +1 -0
  219. package/lib/addons/from-messages.d.ts +52 -0
  220. package/lib/addons/from-messages.d.ts.map +1 -0
  221. package/lib/addons/from-messages.js +304 -0
  222. package/lib/addons/from-messages.js.map +1 -0
  223. package/lib/addons/index.d.ts +67 -0
  224. package/lib/addons/index.d.ts.map +1 -0
  225. package/lib/addons/index.js +86 -0
  226. package/lib/addons/index.js.map +1 -0
  227. package/lib/addons/interactive-message.d.ts +201 -0
  228. package/lib/addons/interactive-message.d.ts.map +1 -0
  229. package/lib/addons/interactive-message.js +256 -0
  230. package/lib/addons/interactive-message.js.map +1 -0
  231. package/lib/addons/jid-plot.d.ts +49 -0
  232. package/lib/addons/jid-plot.d.ts.map +1 -0
  233. package/lib/addons/jid-plot.js +84 -0
  234. package/lib/addons/jid-plot.js.map +1 -0
  235. package/lib/addons/jid-plotting.d.ts +54 -0
  236. package/lib/addons/jid-plotting.d.ts.map +1 -0
  237. package/lib/addons/jid-plotting.js +150 -0
  238. package/lib/addons/jid-plotting.js.map +1 -0
  239. package/lib/addons/lid-support.d.ts +41 -0
  240. package/lib/addons/lid-support.d.ts.map +1 -0
  241. package/lib/addons/lid-support.js +42 -0
  242. package/lib/addons/lid-support.js.map +1 -0
  243. package/lib/addons/message-composer.d.ts +142 -0
  244. package/lib/addons/message-composer.d.ts.map +1 -0
  245. package/lib/addons/message-composer.js +377 -0
  246. package/lib/addons/message-composer.js.map +1 -0
  247. package/lib/addons/message-scheduler.d.ts +77 -0
  248. package/lib/addons/message-scheduler.d.ts.map +1 -0
  249. package/lib/addons/message-scheduler.js +108 -0
  250. package/lib/addons/message-scheduler.js.map +1 -0
  251. package/lib/addons/message-search.d.ts +51 -0
  252. package/lib/addons/message-search.d.ts.map +1 -0
  253. package/lib/addons/message-search.js +171 -0
  254. package/lib/addons/message-search.js.map +1 -0
  255. package/lib/addons/message-utils.d.ts +88 -0
  256. package/lib/addons/message-utils.d.ts.map +1 -0
  257. package/lib/addons/message-utils.js +292 -0
  258. package/lib/addons/message-utils.js.map +1 -0
  259. package/lib/addons/outgoing-calls.d.ts +64 -0
  260. package/lib/addons/outgoing-calls.d.ts.map +1 -0
  261. package/lib/addons/outgoing-calls.js +139 -0
  262. package/lib/addons/outgoing-calls.js.map +1 -0
  263. package/lib/addons/pairing-fix.d.ts +31 -0
  264. package/lib/addons/pairing-fix.d.ts.map +1 -0
  265. package/lib/addons/pairing-fix.js +74 -0
  266. package/lib/addons/pairing-fix.js.map +1 -0
  267. package/lib/addons/past-participants.d.ts +42 -0
  268. package/lib/addons/past-participants.d.ts.map +1 -0
  269. package/lib/addons/past-participants.js +41 -0
  270. package/lib/addons/past-participants.js.map +1 -0
  271. package/lib/addons/rich-response.d.ts +111 -0
  272. package/lib/addons/rich-response.d.ts.map +1 -0
  273. package/lib/addons/rich-response.js +152 -0
  274. package/lib/addons/rich-response.js.map +1 -0
  275. package/lib/addons/scheduling.d.ts +41 -0
  276. package/lib/addons/scheduling.d.ts.map +1 -0
  277. package/lib/addons/scheduling.js +110 -0
  278. package/lib/addons/scheduling.js.map +1 -0
  279. package/lib/addons/status-posting.d.ts +177 -0
  280. package/lib/addons/status-posting.d.ts.map +1 -0
  281. package/lib/addons/status-posting.js +240 -0
  282. package/lib/addons/status-posting.js.map +1 -0
  283. package/lib/addons/stickerpack.d.ts +37 -0
  284. package/lib/addons/stickerpack.d.ts.map +1 -0
  285. package/lib/addons/stickerpack.js +39 -0
  286. package/lib/addons/stickerpack.js.map +1 -0
  287. package/lib/addons/templates.d.ts +72 -0
  288. package/lib/addons/templates.d.ts.map +1 -0
  289. package/lib/addons/templates.js +145 -0
  290. package/lib/addons/templates.js.map +1 -0
  291. package/lib/addons/vcard.d.ts +59 -0
  292. package/lib/addons/vcard.d.ts.map +1 -0
  293. package/lib/addons/vcard.js +88 -0
  294. package/lib/addons/vcard.js.map +1 -0
  295. package/lib/index.d.ts +1 -0
  296. package/lib/index.d.ts.map +1 -1
  297. package/lib/index.js +1 -0
  298. package/lib/index.js.map +1 -1
  299. package/package.json +4 -2
@@ -3,17 +3,19 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
- import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
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, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
6
+ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
+ import { ReachoutTimelockEnforcementType, 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, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
- import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
10
+ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
11
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
12
  import { extractGroupMetadata } from './groups.js';
12
13
  import { makeMessagesSocket } from './messages-send.js';
13
14
  export const makeMessagesRecvSocket = (config) => {
14
15
  const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
15
16
  const sock = makeMessagesSocket(config);
16
- const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
17
+ const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, getUSyncDevices, createParticipantNodes, messageRetryManager, issuePrivacyTokens } = sock;
18
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
17
19
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
18
20
  const retryMutex = makeMutex();
19
21
  const msgRetryCache = config.msgRetryCounterCache ||
@@ -50,7 +52,7 @@ export const makeMessagesRecvSocket = (config) => {
50
52
  };
51
53
  return sendPeerDataOperationMessage(pdoMessage);
52
54
  };
53
- const requestPlaceholderResend = async (messageKey) => {
55
+ const requestPlaceholderResend = async (messageKey, msgData) => {
54
56
  if (!authState.creds.me?.id) {
55
57
  throw new Boom('Not authenticated');
56
58
  }
@@ -59,7 +61,9 @@ export const makeMessagesRecvSocket = (config) => {
59
61
  return;
60
62
  }
61
63
  else {
62
- await placeholderResendCache.set(messageKey?.id, true);
64
+ // Store original message data so PDO response handler can preserve
65
+ // metadata (LID details, timestamps, etc.) that the phone may omit
66
+ await placeholderResendCache.set(messageKey?.id, msgData || true);
63
67
  }
64
68
  await delay(2000);
65
69
  if (!(await placeholderResendCache.get(messageKey?.id))) {
@@ -82,25 +86,145 @@ export const makeMessagesRecvSocket = (config) => {
82
86
  }, 8000);
83
87
  return sendPeerDataOperationMessage(pdoMessage);
84
88
  };
85
- // Handles mex newsletter notifications
86
- const handleMexNewsletterNotification = async (node) => {
89
+ const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
90
+ function isValidEnforcementType(value) {
91
+ return typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value);
92
+ }
93
+ // ── Top-level mex dispatcher (PR: feat-mex-notification-dispatch) ─────────
94
+ const handleMexNotification = async (node) => {
95
+ const updateNode = getBinaryNodeChild(node, 'update');
96
+ if (updateNode) {
97
+ const opName = updateNode.attrs?.op_name;
98
+ if (!opName) {
99
+ logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name');
100
+ return;
101
+ }
102
+ let mexResponse;
103
+ try {
104
+ mexResponse = JSON.parse(updateNode.content.toString());
105
+ }
106
+ catch (error) {
107
+ logger.error({ err: error, opName }, 'failed to parse mex notification JSON');
108
+ return;
109
+ }
110
+ if (mexResponse.errors?.length) {
111
+ logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors');
112
+ return;
113
+ }
114
+ const data = mexResponse.data;
115
+ if (!data) {
116
+ logger.warn({ opName }, 'mex notification has null data');
117
+ return;
118
+ }
119
+ logger.debug({ opName }, 'processing mex notification');
120
+ switch (opName) {
121
+ case 'NotificationUserReachoutTimelockUpdate':
122
+ handleReachoutTimelockNotification(data);
123
+ break;
124
+ case 'MessageCappingInfoNotification':
125
+ handleMessageCappingNotification(data);
126
+ break;
127
+ case 'NotificationLinkedProfilesUpdates': {
128
+ // PR: fix-mex-linked-profiles
129
+ const linkedProfiles = data.xwa2_notify_linked_profiles;
130
+ if (!linkedProfiles)
131
+ break;
132
+ const lid = linkedProfiles.jid;
133
+ for (const profile of linkedProfiles.added_profiles ?? []) {
134
+ const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null);
135
+ if (lid && pn)
136
+ ev.emit('lid-mapping.update', { lid, pn });
137
+ }
138
+ break;
139
+ }
140
+ // newsletter ops still use the legacy <mex> child structure
141
+ case 'NotificationNewsletterUpdate':
142
+ case 'NotificationNewsletterAdminPromote':
143
+ case 'NotificationNewsletterAdminDemote':
144
+ case 'NotificationNewsletterUserSettingChange':
145
+ case 'NotificationNewsletterJoin':
146
+ case 'NotificationNewsletterLeave':
147
+ case 'NotificationNewsletterStateChange':
148
+ case 'NotificationNewsletterAdminMetadataUpdate':
149
+ case 'NotificationNewsletterOwnerUpdate':
150
+ case 'NotificationNewsletterAdminInviteRevoke':
151
+ case 'NotificationNewsletterWamoSubStatusChange':
152
+ case 'NotificationNewsletterBlockUser':
153
+ case 'NotificationNewsletterPaidPartnership':
154
+ case 'NotificationNewsletterMilestone':
155
+ case 'NewsletterResponseStateUpdate':
156
+ await handleLegacyMexNewsletterNotification(node);
157
+ break;
158
+ default:
159
+ logger.debug({ opName }, 'unhandled mex notification');
160
+ break;
161
+ }
162
+ return;
163
+ }
164
+ await handleLegacyMexNewsletterNotification(node);
165
+ };
166
+ const handleReachoutTimelockNotification = (data) => {
167
+ const payload = data.xwa2_notify_account_reachout_timelock;
168
+ if (!payload) {
169
+ logger.warn('reachout timelock notification missing payload');
170
+ return;
171
+ }
172
+ if (!payload.is_active) {
173
+ logger.info('reachout timelock restriction lifted');
174
+ ev.emit('connection.update', {
175
+ reachoutTimeLock: {
176
+ isActive: false,
177
+ enforcementType: ReachoutTimelockEnforcementType.DEFAULT
178
+ }
179
+ });
180
+ return;
181
+ }
182
+ // WA Web defaults to now+60s when the server omits the expiry
183
+ const timeEnforcementEnds = payload.time_enforcement_ends
184
+ ? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000)
185
+ : new Date(Date.now() + 60000);
186
+ const enforcementType = isValidEnforcementType(payload.enforcement_type)
187
+ ? payload.enforcement_type
188
+ : ReachoutTimelockEnforcementType.DEFAULT;
189
+ logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set');
190
+ ev.emit('connection.update', {
191
+ reachoutTimeLock: {
192
+ isActive: true,
193
+ timeEnforcementEnds,
194
+ enforcementType
195
+ }
196
+ });
197
+ };
198
+ const handleMessageCappingNotification = (data) => {
199
+ const payload = data.xwa2_notify_new_chat_messages_capping_info_update;
200
+ if (!payload) {
201
+ logger.warn('message capping notification missing payload');
202
+ return;
203
+ }
204
+ logger.info({ payload }, 'received message capping update');
205
+ ev.emit('message-capping.update', payload);
206
+ };
207
+ // ── Legacy mex newsletter notification handler ────────────────────────────
208
+ const handleLegacyMexNewsletterNotification = async (node) => {
87
209
  const mexNode = getBinaryNodeChild(node, 'mex');
88
210
  if (!mexNode?.content) {
89
- logger.warn({ node }, 'Invalid mex newsletter notification');
211
+ logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification');
90
212
  return;
91
213
  }
92
- let data;
214
+ let parsed;
93
215
  try {
94
- data = JSON.parse(mexNode.content.toString());
216
+ // PR fix-mex-linked-profiles: handle binary-encoded content correctly
217
+ const payloadContent = mexNode.content;
218
+ const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent);
219
+ parsed = JSON.parse(contentBuf.toString());
95
220
  }
96
221
  catch (error) {
97
- logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
222
+ logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification');
98
223
  return;
99
224
  }
100
- const operation = data?.operation;
101
- const updates = data?.updates;
225
+ const { operation, updates } = parsed;
102
226
  if (!updates || !operation) {
103
- logger.warn({ data }, 'Invalid mex newsletter notification content');
227
+ logger.warn({ parsed }, 'invalid mex newsletter notification content');
104
228
  return;
105
229
  }
106
230
  logger.info({ operation, updates }, 'got mex newsletter notification');
@@ -129,7 +253,7 @@ export const makeMessagesRecvSocket = (config) => {
129
253
  }
130
254
  break;
131
255
  default:
132
- logger.info({ operation, data }, 'Unhandled mex newsletter notification');
256
+ logger.info({ operation, parsed }, 'unhandled mex newsletter notification');
133
257
  break;
134
258
  }
135
259
  };
@@ -243,26 +367,275 @@ export const makeMessagesRecvSocket = (config) => {
243
367
  logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
244
368
  await sendNode(stanza);
245
369
  };
370
+ // ── Call handlers ─────────────────────────────────────────────────
246
371
  const rejectCall = async (callId, callFrom) => {
247
- const stanza = {
372
+ await query({
248
373
  tag: 'call',
249
- attrs: {
250
- from: authState.creds.me.id,
251
- to: callFrom
252
- },
374
+ attrs: { from: authState.creds.me.id, to: callFrom },
375
+ content: [
376
+ { tag: 'reject', attrs: { 'call-id': callId, 'call-creator': callFrom, count: '0' }, content: undefined }
377
+ ]
378
+ });
379
+ };
380
+ const offerCall = async (toJid, isVideo = false) => {
381
+ const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64);
382
+ const offerContent = [];
383
+ if (isVideo) {
384
+ offerContent.push({
385
+ tag: 'video',
386
+ attrs: {
387
+ enc: 'vp8',
388
+ dec: 'vp8',
389
+ orientation: '0',
390
+ screen_width: '1920',
391
+ screen_height: '1080',
392
+ device_orientation: '0'
393
+ },
394
+ content: undefined
395
+ });
396
+ }
397
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined });
398
+ offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined });
399
+ offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined });
400
+ offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) });
401
+ offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
402
+ const encKey = randomBytes(32);
403
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
404
+ await assertSessions(devices, true);
405
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, { call: { callKey: new Uint8Array(encKey) } }, { count: '0' });
406
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
407
+ if (shouldIncludeDeviceIdentity) {
408
+ offerContent.push({
409
+ tag: 'device-identity',
410
+ attrs: {},
411
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
412
+ });
413
+ }
414
+ await query({
415
+ tag: 'call',
416
+ attrs: { id: generateMessageTag(), to: toJid },
417
+ content: [
418
+ { tag: 'offer', attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id }, content: offerContent }
419
+ ]
420
+ });
421
+ return { id: callId, to: toJid };
422
+ };
423
+ const initiateCall = async (jid, options = {}) => {
424
+ const meId = authState.creds.me?.id;
425
+ if (!meId)
426
+ throw new Boom('Not authenticated');
427
+ const isVideo = !!options.isVideo;
428
+ const isGroup = isJidGroup(jid);
429
+ const result = await offerCall(jid, isVideo);
430
+ const callId = result.id;
431
+ await callOfferCache.set(callId, {
432
+ chatId: jid,
433
+ from: meId,
434
+ id: callId,
435
+ date: new Date(),
436
+ offline: false,
437
+ status: 'offer',
438
+ isVideo,
439
+ isGroup,
440
+ groupJid: isGroup ? jid : undefined
441
+ });
442
+ return { callId, to: jid, isVideo };
443
+ };
444
+ const terminateCall = async (callId, callTo, callCreator, reason, duration) => {
445
+ const meId = authState.creds.me?.id;
446
+ if (!meId)
447
+ throw new Boom('Not authenticated', { statusCode: 401 });
448
+ const attrs = { 'call-id': callId, 'call-creator': callCreator || meId };
449
+ if (reason)
450
+ attrs.reason = reason;
451
+ if (typeof duration === 'number') {
452
+ attrs.duration = String(duration);
453
+ attrs.audio_duration = String(duration);
454
+ }
455
+ await query({
456
+ tag: 'call',
457
+ attrs: { to: callTo, id: randomBytes(16).toString('hex').toUpperCase() },
458
+ content: [{ tag: 'terminate', attrs, content: undefined }]
459
+ });
460
+ await callOfferCache.del(callId);
461
+ };
462
+ const cancelCall = async (callId, callTo) => terminateCall(callId, callTo);
463
+ const acceptCall = async (callId, callFrom, isVideo) => {
464
+ const meId = authState.creds.me?.id;
465
+ if (!meId)
466
+ throw new Boom('Not authenticated', { statusCode: 401 });
467
+ const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
468
+ if (isVideo)
469
+ content.push({ tag: 'video', attrs: { dec: 'H264,AV1', device_orientation: '1' }, content: undefined });
470
+ content.push({ tag: 'net', attrs: { medium: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
471
+ await query({
472
+ tag: 'call',
473
+ attrs: { from: meId, to: callFrom, id: randomBytes(16).toString('hex').toUpperCase() },
474
+ content: [{ tag: 'accept', attrs: { 'call-id': callId, 'call-creator': callFrom }, content }]
475
+ });
476
+ };
477
+ const preacceptCall = async (callId, callCreator, isVideo) => {
478
+ const content = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }];
479
+ if (isVideo) {
480
+ content.push({
481
+ tag: 'video',
482
+ attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
483
+ content: undefined
484
+ });
485
+ }
486
+ content.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined });
487
+ await query({
488
+ tag: 'call',
489
+ attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
490
+ content: [{ tag: 'preaccept', attrs: { 'call-id': callId, 'call-creator': callCreator }, content }]
491
+ });
492
+ };
493
+ const sendRelayLatency = async (callId, callCreator, relays, transactionId) => {
494
+ const attrs = { 'call-id': callId, 'call-creator': callCreator };
495
+ if (transactionId)
496
+ attrs['transaction-id'] = transactionId;
497
+ await sendNode({
498
+ tag: 'call',
499
+ attrs: { to: callCreator, id: randomBytes(16).toString('hex').toUpperCase() },
253
500
  content: [
254
501
  {
255
- tag: 'reject',
502
+ tag: 'relaylatency',
503
+ attrs,
504
+ content: relays.map(r => {
505
+ const a = {};
506
+ if (r.relayName)
507
+ a.relay_name = r.relayName;
508
+ a.latency = String(r.latency);
509
+ if (r.relayId)
510
+ a.relay_id = r.relayId;
511
+ if (r.dlBw !== undefined)
512
+ a.dl_bw = String(r.dlBw);
513
+ if (r.ulBw !== undefined)
514
+ a.ul_bw = String(r.ulBw);
515
+ return { tag: 'te', attrs: a, content: undefined };
516
+ })
517
+ }
518
+ ]
519
+ });
520
+ };
521
+ const sendTransport = async (callId, callCreator, to, candidates, round) => {
522
+ const attrs = {
523
+ 'call-id': callId,
524
+ 'call-creator': callCreator,
525
+ 'transport-message-type': '1'
526
+ };
527
+ if (round !== undefined)
528
+ attrs['p2p-cand-round'] = String(round);
529
+ await sendNode({
530
+ tag: 'call',
531
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
532
+ content: [
533
+ {
534
+ tag: 'transport',
535
+ attrs,
536
+ content: candidates.map(c => ({ tag: 'te', attrs: { priority: c.priority }, content: c.data }))
537
+ }
538
+ ]
539
+ });
540
+ };
541
+ const sendCallDuration = async (callId, callCreator, peer, audioDuration, callType = '1x1') => {
542
+ await sendNode({
543
+ tag: 'call',
544
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
545
+ content: [
546
+ {
547
+ tag: 'duration',
256
548
  attrs: {
257
549
  'call-id': callId,
258
- 'call-creator': callFrom,
259
- count: '0'
550
+ 'call-creator': callCreator,
551
+ peer,
552
+ audio_duration: String(audioDuration),
553
+ type: callType
260
554
  },
261
555
  content: undefined
262
556
  }
263
557
  ]
264
- };
265
- await query(stanza);
558
+ });
559
+ };
560
+ const muteCall = async (callId, callCreator, to, muted) => {
561
+ await sendNode({
562
+ tag: 'call',
563
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
564
+ content: [
565
+ {
566
+ tag: 'mute_v2',
567
+ attrs: { 'mute-state': muted ? '1' : '0', 'call-id': callId, 'call-creator': callCreator },
568
+ content: undefined
569
+ }
570
+ ]
571
+ });
572
+ };
573
+ const sendHeartbeat = async (callId, callCreator) => {
574
+ await sendNode({
575
+ tag: 'call',
576
+ attrs: { to: `${callId}@call`, id: randomBytes(16).toString('hex').toUpperCase() },
577
+ content: [{ tag: 'heartbeat', attrs: { 'call-id': callId, 'call-creator': callCreator }, content: undefined }]
578
+ });
579
+ };
580
+ const sendEncRekey = async (callId, callCreator, to, transactionId) => {
581
+ await sendNode({
582
+ tag: 'call',
583
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
584
+ content: [
585
+ {
586
+ tag: 'enc_rekey',
587
+ attrs: { 'transaction-id': transactionId, 'call-id': callId, 'call-creator': callCreator },
588
+ content: [
589
+ { tag: 'encopt', attrs: { keygen: '2' }, content: undefined },
590
+ { tag: 'enc', attrs: { v: '2', type: 'msg' }, content: undefined }
591
+ ]
592
+ }
593
+ ]
594
+ });
595
+ };
596
+ const sendVideoState = async (callId, callCreator, to, enabled, orientation = '1') => {
597
+ await sendNode({
598
+ tag: 'call',
599
+ attrs: { to, id: randomBytes(16).toString('hex').toUpperCase() },
600
+ content: [
601
+ {
602
+ tag: 'video',
603
+ attrs: {
604
+ 'call-id': callId,
605
+ 'call-creator': callCreator,
606
+ state: enabled ? '1' : '0',
607
+ device_orientation: orientation
608
+ },
609
+ content: undefined
610
+ }
611
+ ]
612
+ });
613
+ };
614
+ const queryCallLink = async (token, media = 'video') => {
615
+ return await query({
616
+ tag: 'call',
617
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
618
+ content: [{ tag: 'link_query', attrs: { media, token }, content: undefined }]
619
+ });
620
+ };
621
+ const joinCallLink = async (token, media = 'video') => {
622
+ const content = [
623
+ { tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
624
+ { tag: 'net', attrs: { medium: '2' }, content: undefined },
625
+ { tag: 'capability', attrs: { ver: '1' }, content: undefined }
626
+ ];
627
+ if (media === 'video') {
628
+ content.splice(1, 0, {
629
+ tag: 'video',
630
+ attrs: { screen_width: '1080', screen_height: '2400', dec: 'H264,H265,AV1', device_orientation: '0' },
631
+ content: undefined
632
+ });
633
+ }
634
+ return await query({
635
+ tag: 'call',
636
+ attrs: { to: 'call', id: randomBytes(16).toString('hex').toUpperCase() },
637
+ content: [{ tag: 'link_join', attrs: { media, token }, content }]
638
+ });
266
639
  };
267
640
  const sendRetryRequest = async (node, forceIncludeKeys = false) => {
268
641
  const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
@@ -407,22 +780,16 @@ export const makeMessagesRecvSocket = (config) => {
407
780
  }
408
781
  }
409
782
  else {
410
- const identityNode = getBinaryNodeChild(node, 'identity');
411
- if (identityNode) {
412
- logger.info({ jid: from }, 'identity changed');
413
- if (identityAssertDebounce.get(from)) {
414
- logger.debug({ jid: from }, 'skipping identity assert (debounced)');
415
- return;
416
- }
417
- identityAssertDebounce.set(from, true);
418
- try {
419
- await assertSessions([from], true);
420
- }
421
- catch (error) {
422
- logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
423
- }
424
- }
425
- else {
783
+ const result = await handleIdentityChange(node, {
784
+ meId: authState.creds.me?.id,
785
+ meLid: authState.creds.me?.lid,
786
+ validateSession: signalRepository.validateSession,
787
+ assertSessions,
788
+ debounceCache: identityAssertDebounce,
789
+ logger,
790
+ onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
791
+ });
792
+ if (result.action === 'no_identity_node') {
426
793
  logger.info({ node }, 'unknown encrypt notification');
427
794
  }
428
795
  }
@@ -431,6 +798,7 @@ export const makeMessagesRecvSocket = (config) => {
431
798
  // TODO: Support PN/LID (Here is only LID now)
432
799
  const actingParticipantLid = fullNode.attrs.participant;
433
800
  const actingParticipantPn = fullNode.attrs.participant_pn;
801
+ const actingParticipantUsername = fullNode.attrs.participant_username;
434
802
  const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
435
803
  const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
436
804
  switch (child?.tag) {
@@ -450,7 +818,8 @@ export const makeMessagesRecvSocket = (config) => {
450
818
  {
451
819
  ...metadata,
452
820
  author: actingParticipantLid,
453
- authorPn: actingParticipantPn
821
+ authorPn: actingParticipantPn,
822
+ authorUsername: actingParticipantUsername
454
823
  }
455
824
  ]);
456
825
  break;
@@ -560,7 +929,7 @@ export const makeMessagesRecvSocket = (config) => {
560
929
  await handleNewsletterNotification(node);
561
930
  break;
562
931
  case 'mex':
563
- await handleMexNewsletterNotification(node);
932
+ await handleMexNotification(node);
564
933
  break;
565
934
  case 'w:gp2':
566
935
  // TODO: HANDLE PARTICIPANT_PN
@@ -592,6 +961,7 @@ export const makeMessagesRecvSocket = (config) => {
592
961
  case 'picture':
593
962
  const setPicture = getBinaryNodeChild(node, 'set');
594
963
  const delPicture = getBinaryNodeChild(node, 'delete');
964
+ // TODO: WAJIDHASH stuff proper support inhouse
595
965
  ev.emit('contacts.update', [
596
966
  {
597
967
  id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
@@ -644,7 +1014,7 @@ export const makeMessagesRecvSocket = (config) => {
644
1014
  const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
645
1015
  const random = randomBytes(32);
646
1016
  const linkCodeSalt = randomBytes(32);
647
- const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
1017
+ const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
648
1018
  salt: linkCodeSalt,
649
1019
  info: 'link_code_pairing_key_bundle_encryption_key'
650
1020
  });
@@ -658,7 +1028,7 @@ export const makeMessagesRecvSocket = (config) => {
658
1028
  const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
659
1029
  const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
660
1030
  const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
661
- authState.creds.advSecretKey = (await hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
1031
+ authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
662
1032
  await query({
663
1033
  tag: 'iq',
664
1034
  attrs: {
@@ -705,27 +1075,91 @@ export const makeMessagesRecvSocket = (config) => {
705
1075
  return result;
706
1076
  }
707
1077
  };
1078
+ /**
1079
+ * In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
1080
+ */
1081
+ const tcTokenKnownJids = new Set();
1082
+ const tcTokenIndexLoaded = (async () => {
1083
+ try {
1084
+ const jids = await readTcTokenIndex(authState.keys);
1085
+ for (const jid of jids)
1086
+ tcTokenKnownJids.add(jid);
1087
+ logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
1088
+ }
1089
+ catch (err) {
1090
+ logger.warn({ err: err?.message }, 'failed to load tctoken index');
1091
+ }
1092
+ })();
1093
+ let tcTokenIndexTimer;
1094
+ async function flushTcTokenIndex() {
1095
+ if (tcTokenIndexTimer) {
1096
+ clearTimeout(tcTokenIndexTimer);
1097
+ tcTokenIndexTimer = undefined;
1098
+ }
1099
+ const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
1100
+ return authState.keys.set({ tctoken: write });
1101
+ }
1102
+ function scheduleTcTokenIndexSave() {
1103
+ if (tcTokenIndexTimer) {
1104
+ clearTimeout(tcTokenIndexTimer);
1105
+ }
1106
+ tcTokenIndexTimer = setTimeout(() => {
1107
+ tcTokenIndexTimer = undefined;
1108
+ flushTcTokenIndex().catch(err => {
1109
+ logger.warn({ err: err?.message }, 'failed to save tctoken index');
1110
+ });
1111
+ }, 5000);
1112
+ }
1113
+ function trackTcTokenJid(jid) {
1114
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
1115
+ tcTokenKnownJids.add(jid);
1116
+ scheduleTcTokenIndexSave();
1117
+ }
1118
+ }
708
1119
  const handlePrivacyTokenNotification = async (node) => {
709
1120
  const tokensNode = getBinaryNodeChild(node, 'tokens');
710
- const from = jidNormalizedUser(node.attrs.from);
711
1121
  if (!tokensNode)
712
1122
  return;
713
- const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
714
- for (const tokenNode of tokenNodes) {
715
- const { attrs, content } = tokenNode;
716
- const type = attrs.type;
717
- const timestamp = attrs.t;
718
- if (type === 'trusted_contact' && content instanceof Buffer) {
719
- logger.debug({
720
- from,
721
- timestamp,
722
- tcToken: content
723
- }, 'received trusted contact token');
724
- await authState.keys.set({
725
- tctoken: { [from]: { token: content, timestamp } }
726
- });
1123
+ const from = jidNormalizedUser(node.attrs.from);
1124
+ const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
1125
+ ? jidNormalizedUser(node.attrs.sender_lid)
1126
+ : undefined;
1127
+ const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
1128
+ logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
1129
+ await storeTcTokensFromIqResult({
1130
+ result: node,
1131
+ fallbackJid,
1132
+ keys: authState.keys,
1133
+ getLIDForPN,
1134
+ onNewJidStored: trackTcTokenJid
1135
+ });
1136
+ };
1137
+ /**
1138
+ * Fire-and-forget tctoken re-issuance after a peer's device identity changed.
1139
+ */
1140
+ const reissueTcTokenAfterIdentityChange = (from) => {
1141
+ void (async () => {
1142
+ const normalizedJid = jidNormalizedUser(from);
1143
+ const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
1144
+ const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
1145
+ const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
1146
+ if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
1147
+ return;
727
1148
  }
728
- }
1149
+ logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
1150
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
1151
+ const issueJid = await resolveIssuanceJid(normalizedJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
1152
+ const result = await issuePrivacyTokens([issueJid], senderTs);
1153
+ await storeTcTokensFromIqResult({
1154
+ result,
1155
+ fallbackJid: tcJid,
1156
+ keys: authState.keys,
1157
+ getLIDForPN,
1158
+ onNewJidStored: trackTcTokenJid
1159
+ });
1160
+ })().catch(err => {
1161
+ logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
1162
+ });
729
1163
  };
730
1164
  async function decipherLinkPublicKey(data) {
731
1165
  const buffer = toRequiredBuffer(data);
@@ -959,81 +1393,148 @@ export const makeMessagesRecvSocket = (config) => {
959
1393
  await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
960
1394
  return;
961
1395
  }
962
- const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
963
- const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
964
- // store new mappings we didn't have before
965
- if (!!alt) {
966
- const altServer = jidDecode(alt)?.server;
967
- const primaryJid = msg.key.participant || msg.key.remoteJid;
968
- if (altServer === 'lid') {
969
- if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
970
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
971
- await signalRepository.migrateSession(primaryJid, alt);
1396
+ let acked = false;
1397
+ try {
1398
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1399
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1400
+ // store new mappings we didn't have before
1401
+ if (!!alt) {
1402
+ const altServer = jidDecode(alt)?.server;
1403
+ const primaryJid = msg.key.participant || msg.key.remoteJid;
1404
+ if (altServer === 'lid') {
1405
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1406
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1407
+ await signalRepository.migrateSession(primaryJid, alt);
1408
+ }
1409
+ }
1410
+ else {
1411
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1412
+ await signalRepository.migrateSession(alt, primaryJid);
972
1413
  }
973
1414
  }
974
- else {
975
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
976
- await signalRepository.migrateSession(alt, primaryJid);
1415
+ // Cache for retry receipts BEFORE decrypt — so retry logic works even if decryption throws
1416
+ if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
1417
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1418
+ logger.debug({ jid: msg.key.remoteJid, id: msg.key.id }, 'Added message to recent cache for retry receipts');
977
1419
  }
978
- }
979
- if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
980
- messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
981
- logger.debug({
982
- jid: msg.key.remoteJid,
983
- id: msg.key.id
984
- }, 'Added message to recent cache for retry receipts');
985
- }
986
- try {
987
1420
  await messageMutex.mutex(async () => {
988
1421
  await decrypt();
989
1422
  // message failed to decrypt
990
1423
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
991
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT ||
992
- msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
993
- return sendMessageAck(node);
1424
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
1425
+ acked = true;
1426
+ return sendMessageAck(node, NACK_REASONS.ParsingError);
994
1427
  }
995
- const errorMessage = msg?.messageStubParameters?.[0] || '';
996
- const isPreKeyError = errorMessage.includes('PreKey');
997
- logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
998
- // Handle both pre-key and normal retries in single mutex
999
- await retryMutex.mutex(async () => {
1000
- try {
1001
- if (!ws.isOpen) {
1002
- logger.debug({ node }, 'Connection closed, skipping retry');
1003
- return;
1004
- }
1005
- // Handle pre-key errors with upload and delay
1006
- if (isPreKeyError) {
1007
- logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1008
- try {
1009
- logger.debug('Uploading pre-keys for error recovery');
1010
- await uploadPreKeys(5);
1011
- logger.debug('Waiting for server to process new pre-keys');
1012
- await delay(1000);
1013
- }
1014
- catch (uploadErr) {
1015
- logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1016
- }
1428
+ if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
1429
+ // Message arrived without encryption (e.g. CTWA ads messages).
1430
+ // Check if this is eligible for placeholder resend (matching WA Web filters).
1431
+ const unavailableNode = getBinaryNodeChild(node, 'unavailable');
1432
+ const unavailableType = unavailableNode?.attrs?.type;
1433
+ if (unavailableType === 'bot_unavailable_fanout' ||
1434
+ unavailableType === 'hosted_unavailable_fanout' ||
1435
+ unavailableType === 'view_once_unavailable_fanout') {
1436
+ logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
1437
+ acked = true;
1438
+ return sendMessageAck(node);
1439
+ }
1440
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1441
+ if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
1442
+ logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
1443
+ acked = true;
1444
+ return sendMessageAck(node);
1445
+ }
1446
+ // Request the real content from the phone via placeholder resend PDO.
1447
+ // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
1448
+ // and store the requestId in stubParameters[1] so users can correlate
1449
+ // with the incoming PDO response event.
1450
+ const cleanKey = {
1451
+ remoteJid: msg.key.remoteJid,
1452
+ fromMe: msg.key.fromMe,
1453
+ id: msg.key.id,
1454
+ participant: msg.key.participant
1455
+ };
1456
+ // Cache the original message metadata so the PDO response handler
1457
+ // can preserve key fields (LID details etc.) that the phone may omit
1458
+ const msgData = {
1459
+ key: msg.key,
1460
+ messageTimestamp: msg.messageTimestamp,
1461
+ pushName: msg.pushName,
1462
+ participant: msg.participant,
1463
+ verifiedBizName: msg.verifiedBizName
1464
+ };
1465
+ requestPlaceholderResend(cleanKey, msgData)
1466
+ .then(requestId => {
1467
+ if (requestId && requestId !== 'RESOLVED') {
1468
+ logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
1469
+ ev.emit('messages.update', [
1470
+ {
1471
+ key: msg.key,
1472
+ update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
1473
+ }
1474
+ ]);
1017
1475
  }
1018
- const encNode = getBinaryNodeChild(node, 'enc');
1019
- await sendRetryRequest(node, !encNode);
1020
- if (retryRequestDelayMs) {
1021
- await delay(retryRequestDelayMs);
1476
+ })
1477
+ .catch(err => {
1478
+ logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
1479
+ });
1480
+ acked = true;
1481
+ await sendMessageAck(node);
1482
+ // Don't return — fall through to upsertMessage so the stub is emitted
1483
+ }
1484
+ else {
1485
+ // Skip retry for expired status messages (>24h old)
1486
+ if (isJidStatusBroadcast(msg.key.remoteJid)) {
1487
+ const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1488
+ if (messageAge > STATUS_EXPIRY_SECONDS) {
1489
+ logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
1490
+ acked = true;
1491
+ return sendMessageAck(node);
1022
1492
  }
1023
1493
  }
1024
- catch (err) {
1025
- logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1026
- // Still attempt retry even if pre-key upload failed
1494
+ const errorMessage = msg?.messageStubParameters?.[0] || '';
1495
+ const isPreKeyError = errorMessage.includes('PreKey');
1496
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1497
+ // Handle both pre-key and normal retries in single mutex
1498
+ await retryMutex.mutex(async () => {
1027
1499
  try {
1500
+ if (!ws.isOpen) {
1501
+ logger.debug({ node }, 'Connection closed, skipping retry');
1502
+ return;
1503
+ }
1504
+ // Handle pre-key errors with upload and delay
1505
+ if (isPreKeyError) {
1506
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1507
+ try {
1508
+ logger.debug('Uploading pre-keys for error recovery');
1509
+ await uploadPreKeys(5);
1510
+ logger.debug('Waiting for server to process new pre-keys');
1511
+ await delay(1000);
1512
+ }
1513
+ catch (uploadErr) {
1514
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1515
+ }
1516
+ }
1028
1517
  const encNode = getBinaryNodeChild(node, 'enc');
1029
1518
  await sendRetryRequest(node, !encNode);
1519
+ if (retryRequestDelayMs) {
1520
+ await delay(retryRequestDelayMs);
1521
+ }
1030
1522
  }
1031
- catch (retryErr) {
1032
- logger.error({ retryErr }, 'Failed to send retry after error handling');
1523
+ catch (err) {
1524
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1525
+ // Still attempt retry even if pre-key upload failed
1526
+ try {
1527
+ const encNode = getBinaryNodeChild(node, 'enc');
1528
+ await sendRetryRequest(node, !encNode);
1529
+ }
1530
+ catch (retryErr) {
1531
+ logger.error({ retryErr }, 'Failed to send retry after error handling');
1532
+ }
1033
1533
  }
1034
- }
1035
- await sendMessageAck(node, NACK_REASONS.UnhandledError);
1036
- });
1534
+ acked = true;
1535
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1536
+ });
1537
+ }
1037
1538
  }
1038
1539
  else {
1039
1540
  if (messageRetryManager && msg.key.id) {
@@ -1059,6 +1560,7 @@ export const makeMessagesRecvSocket = (config) => {
1059
1560
  else if (!sendActiveReceipts) {
1060
1561
  type = 'inactive';
1061
1562
  }
1563
+ acked = true;
1062
1564
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
1063
1565
  // send ack for history message
1064
1566
  const isAnyHistoryMsg = getHistoryMsg(msg.message);
@@ -1068,6 +1570,7 @@ export const makeMessagesRecvSocket = (config) => {
1068
1570
  }
1069
1571
  }
1070
1572
  else {
1573
+ acked = true;
1071
1574
  await sendMessageAck(node);
1072
1575
  logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
1073
1576
  }
@@ -1078,6 +1581,9 @@ export const makeMessagesRecvSocket = (config) => {
1078
1581
  }
1079
1582
  catch (error) {
1080
1583
  logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
1584
+ if (!acked) {
1585
+ await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
1586
+ }
1081
1587
  }
1082
1588
  };
1083
1589
  const handleCall = async (node) => {
@@ -1092,6 +1598,7 @@ export const makeMessagesRecvSocket = (config) => {
1092
1598
  const call = {
1093
1599
  chatId: attrs.from,
1094
1600
  from,
1601
+ callerPn: infoChild.attrs['caller_pn'],
1095
1602
  id: callId,
1096
1603
  date: new Date(+attrs.t * 1000),
1097
1604
  offline: !!attrs.offline,
@@ -1108,6 +1615,7 @@ export const makeMessagesRecvSocket = (config) => {
1108
1615
  if (existingCall) {
1109
1616
  call.isVideo = existingCall.isVideo;
1110
1617
  call.isGroup = existingCall.isGroup;
1618
+ call.callerPn = call.callerPn || existingCall.callerPn;
1111
1619
  }
1112
1620
  // delete data once call has ended
1113
1621
  if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
@@ -1118,23 +1626,22 @@ export const makeMessagesRecvSocket = (config) => {
1118
1626
  };
1119
1627
  const handleBadAck = async ({ attrs }) => {
1120
1628
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
1121
- // WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
1122
- // // current hypothesis is that if pash is sent in the ack
1123
- // // it means -- the message hasn't reached all devices yet
1124
- // // we'll retry sending the message here
1125
- // if(attrs.phash) {
1126
- // logger.info({ attrs }, 'received phash in ack, resending message...')
1127
- // const msg = await getMessage(key)
1128
- // if(msg) {
1129
- // await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
1130
- // } else {
1131
- // logger.warn({ attrs }, 'could not send message again, as it was not found')
1132
- // }
1133
- // }
1134
1629
  // error in acknowledgement,
1135
1630
  // device could not display the message
1136
1631
  if (attrs.error) {
1137
- logger.warn({ attrs }, 'received error in ack');
1632
+ if (attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
1633
+ // 463 = account restricted + no tctoken for this contact.
1634
+ // WA Web prevents this client-side (disables compose bar).
1635
+ // No retry — retrying worsens the restriction by counting
1636
+ // as another "reach out" to an unknown contact.
1637
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
1638
+ }
1639
+ else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
1640
+ logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
1641
+ }
1642
+ else {
1643
+ logger.warn({ attrs }, 'received error in ack');
1644
+ }
1138
1645
  ev.emit('messages.update', [
1139
1646
  {
1140
1647
  key,
@@ -1144,19 +1651,6 @@ export const makeMessagesRecvSocket = (config) => {
1144
1651
  }
1145
1652
  }
1146
1653
  ]);
1147
- // resend the message with device_fanout=false, use at your own risk
1148
- // if (attrs.error === '475') {
1149
- // const msg = await getMessage(key)
1150
- // if (msg) {
1151
- // await relayMessage(key.remoteJid!, msg, {
1152
- // messageId: key.id!,
1153
- // useUserDevicesCache: false,
1154
- // additionalAttributes: {
1155
- // device_fanout: 'false'
1156
- // }
1157
- // })
1158
- // }
1159
- // }
1160
1654
  }
1161
1655
  };
1162
1656
  /// processes a node with the given function
@@ -1271,17 +1765,112 @@ export const makeMessagesRecvSocket = (config) => {
1271
1765
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1272
1766
  }
1273
1767
  });
1274
- ev.on('connection.update', ({ isOnline }) => {
1768
+ /** timestamp of last tctoken prune run — throttles to once per 24h */
1769
+ let lastTcTokenPruneTs = 0;
1770
+ ev.on('connection.update', ({ isOnline, connection }) => {
1275
1771
  if (typeof isOnline !== 'undefined') {
1276
1772
  sendActiveReceipts = isOnline;
1277
1773
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1278
1774
  }
1775
+ // Flush pending tctoken index save on disconnect to avoid writing after close
1776
+ if (connection === 'close' && tcTokenIndexTimer) {
1777
+ clearTimeout(tcTokenIndexTimer);
1778
+ tcTokenIndexTimer = undefined;
1779
+ try {
1780
+ void Promise.resolve(flushTcTokenIndex()).catch(() => { });
1781
+ }
1782
+ catch {
1783
+ /* ignore sync errors */
1784
+ }
1785
+ }
1786
+ // Prune expired tctokens when coming online, at most once per 24 hours
1787
+ if (isOnline) {
1788
+ const now = Date.now();
1789
+ const DAY_MS = 24 * 60 * 60 * 1000;
1790
+ if (now - lastTcTokenPruneTs >= DAY_MS) {
1791
+ lastTcTokenPruneTs = now;
1792
+ void pruneExpiredTcTokens();
1793
+ }
1794
+ }
1279
1795
  });
1796
+ async function pruneExpiredTcTokens() {
1797
+ try {
1798
+ await tcTokenIndexLoaded;
1799
+ const persisted = await readTcTokenIndex(authState.keys);
1800
+ const allJids = new Set(tcTokenKnownJids);
1801
+ for (const jid of persisted)
1802
+ allJids.add(jid);
1803
+ if (!allJids.size)
1804
+ return;
1805
+ const jids = [...allJids];
1806
+ const allTokens = await authState.keys.get('tctoken', jids);
1807
+ const writes = {};
1808
+ const survivors = new Set();
1809
+ let mutated = 0;
1810
+ for (const jid of jids) {
1811
+ const entry = allTokens[jid];
1812
+ if (!entry) {
1813
+ mutated++;
1814
+ continue;
1815
+ }
1816
+ const hasPeerToken = !!entry.token?.length;
1817
+ const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
1818
+ const hasSenderTs = entry.senderTimestamp !== undefined;
1819
+ const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
1820
+ const keepPeerToken = hasPeerToken && !peerTokenExpired;
1821
+ const keepSenderTs = hasSenderTs && !senderTsExpired;
1822
+ if (!keepPeerToken && !keepSenderTs) {
1823
+ writes[jid] = null;
1824
+ mutated++;
1825
+ }
1826
+ else if (peerTokenExpired && keepSenderTs) {
1827
+ writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
1828
+ survivors.add(jid);
1829
+ mutated++;
1830
+ }
1831
+ else {
1832
+ survivors.add(jid);
1833
+ }
1834
+ }
1835
+ if (mutated === 0)
1836
+ return;
1837
+ await authState.keys.set({
1838
+ tctoken: {
1839
+ ...writes,
1840
+ [TC_TOKEN_INDEX_KEY]: {
1841
+ token: Buffer.from(JSON.stringify([...survivors]))
1842
+ }
1843
+ }
1844
+ });
1845
+ tcTokenKnownJids.clear();
1846
+ for (const jid of survivors)
1847
+ tcTokenKnownJids.add(jid);
1848
+ logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
1849
+ }
1850
+ catch (err) {
1851
+ logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
1852
+ }
1853
+ }
1280
1854
  return {
1281
1855
  ...sock,
1282
1856
  sendMessageAck,
1283
1857
  sendRetryRequest,
1858
+ offerCall,
1859
+ initiateCall,
1860
+ cancelCall,
1284
1861
  rejectCall,
1862
+ acceptCall,
1863
+ preacceptCall,
1864
+ terminateCall,
1865
+ sendRelayLatency,
1866
+ sendTransport,
1867
+ sendCallDuration,
1868
+ muteCall,
1869
+ sendHeartbeat,
1870
+ sendEncRekey,
1871
+ sendVideoState,
1872
+ queryCallLink,
1873
+ joinCallLink,
1285
1874
  fetchMessageHistory,
1286
1875
  requestPlaceholderResend,
1287
1876
  messageRetryManager