@nexustechpro/baileys 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -527,7 +527,7 @@ export const makeMessagesRecvSocket = (config) => {
527
527
 
528
528
  const tcTokenKnownJids = new Set()
529
529
  const tcTokenIndexLoaded = (async () => {
530
- try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
530
+ try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); if (tcTokenKnownJids.size > 0) logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
531
531
  catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
532
532
  })()
533
533
 
@@ -942,7 +942,8 @@ export const makeMessagesRecvSocket = (config) => {
942
942
  else survivors.add(jid)
943
943
  }
944
944
  if (mutated === 0) return
945
- await authState.keys.set({ tctoken: { ...writes, [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) } } })
945
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [...survivors])
946
+ await authState.keys.set({ tctoken: { ...writes, ...indexWrite } })
946
947
  tcTokenKnownJids.clear()
947
948
  for (const jid of survivors) tcTokenKnownJids.add(jid)
948
949
  logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
@@ -962,5 +963,5 @@ export const makeMessagesRecvSocket = (config) => {
962
963
  sendActiveReceipts = false
963
964
  })
964
965
 
965
- return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
966
+ return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager, tcTokenIndexLoaded }
966
967
  }
@@ -367,13 +367,6 @@ export const makeMessagesSocket = (config) => {
367
367
  const isStatus = jid === 'status@broadcast'
368
368
  let isLid = server === 'lid'
369
369
  const isNewsletter = server === 'newsletter'
370
- if (isLid && !isGroup && !isStatus) {
371
- const pn = await signalRepository.lidMapping.getPNForLID(jid)
372
- if (pn) {
373
- const resolvedJid = pn.replace(/:\d+(?=@)/, ''); logger.debug({ from: jid, to: resolvedJid }, 'relayMessage: resolved 1:1 LID to PN for wire protocol')
374
- jid = resolvedJid; const decoded = jidDecode(jid); user = decoded.user; server = decoded.server; isLid = false
375
- }
376
- }
377
370
  let activeSender = meId
378
371
  let groupAddressingMode = 'pn'
379
372
  if (isGroup && !isStatus) {
@@ -542,6 +535,14 @@ export const makeMessagesSocket = (config) => {
542
535
  if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
543
536
 
544
537
  const { user: ownUser } = jidDecode(ownId)
538
+ const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
539
+ devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
540
+
541
+ if (user !== ownUser) {
542
+ const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
543
+ const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
544
+ devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
545
+ }
545
546
 
546
547
  if (!participant) {
547
548
  const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
@@ -570,22 +571,16 @@ export const makeMessagesSocket = (config) => {
570
571
  }
571
572
  }
572
573
  }
573
-
574
574
  const allRecipients = [], meRecipients = [], otherRecipients = []
575
575
  const { user: mePnUser } = jidDecode(meId)
576
576
  const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
577
-
578
- for (const { user, jid } of devices) {
579
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
580
- if (isExactSenderDevice) continue
581
- const isMe = user === mePnUser || user === meLidUser
582
- if (isMe) meRecipients.push(jid)
583
- else otherRecipients.push(jid)
584
- allRecipients.push(jid)
577
+ for (const { user: devUser, jid: devJid } of devices) {
578
+ const isOwnUser = devUser === mePnUser || devUser === meLidUser
579
+ if (isOwnUser) meRecipients.push(devJid)
580
+ else otherRecipients.push(devJid)
581
+ allRecipients.push(devJid)
585
582
  }
586
-
587
583
  await assertSessions(allRecipients)
588
-
589
584
  const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
590
585
  createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
591
586
  createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
@@ -613,7 +608,7 @@ export const makeMessagesSocket = (config) => {
613
608
  id: finalMsgId,
614
609
  to: destinationJid,
615
610
  type: getMessageType(message),
616
- ...(isLid || (isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
611
+ ...((isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
617
612
  ...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
618
613
  ...(additionalAttributes || {})
619
614
  },
@@ -53,7 +53,7 @@ export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
53
53
  }
54
54
  if (prevOp) subBuffs.push(prevOp.valueMac)
55
55
  },
56
- finish: () => {
56
+ finish: async () => {
57
57
  const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)
58
58
  return { hash: Buffer.from(result), indexValueMap }
59
59
  }
@@ -99,7 +99,7 @@ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, op
99
99
  const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
100
100
  const generator = makeLtHashGenerator(state)
101
101
  generator.mix({ indexMac, valueMac, operation })
102
- Object.assign(state, generator.finish())
102
+ Object.assign(state, await generator.finish())
103
103
  state.version += 1
104
104
  const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
105
105
  const patch = {
@@ -2,16 +2,21 @@
2
2
  // Single source of truth for blob key names — change here, changes everywhere
3
3
  export const SESSION_INDEX_KEY = 'index'
4
4
  export const DEVICE_LIST_INDEX_KEY = 'index'
5
+ export const TC_TOKEN_INDEX_KEY = 'index'
5
6
 
6
7
  // Migrates old _index blob to new index key in-place, returns the batch data
7
8
  export const migrateIndexKey = async (keys, type) => {
8
- const oldKey = '_index'
9
+ const oldKeys = ['_index', '__index']
9
10
  const newKey = 'index'
10
- const oldData = await keys.get(type, [oldKey])
11
- if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') { // only migrate if old blob actually exists and has data
12
- await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
13
- return oldData[oldKey]
11
+
12
+ for (const oldKey of oldKeys) {
13
+ const oldData = await keys.get(type, [oldKey])
14
+ if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') {
15
+ await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
16
+ return oldData[oldKey]
17
+ }
14
18
  }
19
+
15
20
  const newData = await keys.get(type, [newKey])
16
21
  return newData?.[newKey] || {}
17
22
  }
@@ -1,48 +1,7 @@
1
- import { hkdf } from './crypto.js';
1
+ import { LTHashAntiTampering } from 'whatsapp-rust-bridge';
2
2
  /**
3
3
  * LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data
4
4
  * over a series of mutations. You can add/remove mutations and it'll return a hash equal to
5
5
  * if the same series of mutations was made sequentially.
6
6
  */
7
- const o = 128;
8
- class LTHash {
9
- constructor(e) {
10
- this.salt = e;
11
- }
12
- async add(e, t) {
13
- for (const item of t) {
14
- e = await this._addSingle(e, item);
15
- }
16
- return e;
17
- }
18
- async subtract(e, t) {
19
- for (const item of t) {
20
- e = await this._subtractSingle(e, item);
21
- }
22
- return e;
23
- }
24
- async subtractThenAdd(e, addList, subtractList) {
25
- const subtracted = await this.subtract(e, subtractList);
26
- return this.add(subtracted, addList);
27
- }
28
- async _addSingle(e, t) {
29
- const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
30
- return this.performPointwiseWithOverflow(e, derived, (a, b) => a + b);
31
- }
32
- async _subtractSingle(e, t) {
33
- const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
34
- return this.performPointwiseWithOverflow(e, derived, (a, b) => a - b);
35
- }
36
- performPointwiseWithOverflow(e, t, op) {
37
- const n = new DataView(e);
38
- const i = new DataView(t);
39
- const out = new ArrayBuffer(n.byteLength);
40
- const s = new DataView(out);
41
- for (let offset = 0; offset < n.byteLength; offset += 2) {
42
- s.setUint16(offset, op(n.getUint16(offset, true), i.getUint16(offset, true)), true);
43
- }
44
- return out;
45
- }
46
- }
47
- export const LT_HASH_ANTI_TAMPERING = new LTHash('WhatsApp Patch Integrity');
48
- //# sourceMappingURL=lt-hash.js.map
7
+ export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering();
@@ -1,179 +1,200 @@
1
1
  import { proto } from '../../WAProto/index.js';
2
- import { Boom } from '@hapi/boom'
2
+ import { Boom } from '@hapi/boom';
3
3
  import { WAMessageStubType } from '../Types/index.js';
4
4
  import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
5
- import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
5
+ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
6
6
  import { aesDecryptGCM, hmacSign } from './crypto.js';
7
- import { resolveTcTokenJid, buildMergedTcTokenIndexWrite } from './tc-token-utils.js'
8
- import { toNumber } from './generics.js'
7
+ import { getKeyAuthor, toNumber } from './generics.js';
9
8
  import { downloadAndProcessHistorySyncNotification } from './history.js';
9
+ import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
10
+
10
11
  const REAL_MSG_STUB_TYPES = new Set([
11
12
  WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
12
13
  WAMessageStubType.CALL_MISSED_GROUP_VOICE,
13
14
  WAMessageStubType.CALL_MISSED_VIDEO,
14
- WAMessageStubType.CALL_MISSED_VOICE
15
+ WAMessageStubType.CALL_MISSED_VOICE,
15
16
  ]);
17
+
16
18
  const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
17
- /** Cleans a received message to further processing */
19
+
20
+ // Protocol message types that can only legitimately arrive from our own device.
21
+ // If they arrive from any other sender we drop them to prevent local-state spoofing.
22
+ // Cross-user types (REVOKE, MESSAGE_EDIT, EPHEMERAL_SETTING, GROUP_MEMBER_LABEL_CHANGE)
23
+ // must NOT be listed here — they legitimately arrive from other participants.
24
+ // Reference: https://github.com/tulir/whatsmeow/blob/8d3700152a/message.go#L842-L845
25
+ const SELF_ONLY_PROTOCOL_TYPES = new Set([
26
+ proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
27
+ proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
28
+ proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC,
29
+ proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE,
30
+ ]);
31
+
32
+ /** Persist tc-tokens discovered inside a history-sync payload, skipping stale entries. */
33
+ async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
34
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
35
+ const candidates = [];
36
+ for (const chat of chats) {
37
+ const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
38
+ if (!chat.tcToken?.length || ts <= 0) continue;
39
+ const jid = jidNormalizedUser(chat.id);
40
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
41
+ candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined });
42
+ }
43
+ if (!candidates.length) return;
44
+
45
+ const existing = await keyStore.get('tctoken', candidates.map(c => c.storageJid));
46
+ const entries = {};
47
+ for (const c of candidates) {
48
+ const existingTs = existing[c.storageJid]?.timestamp ? Number(existing[c.storageJid].timestamp) : 0;
49
+ if (existingTs > 0 && existingTs >= c.ts) continue;
50
+ entries[c.storageJid] = { ...existing[c.storageJid], token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) };
51
+ }
52
+ if (!Object.keys(entries).length) return;
53
+
54
+ logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
55
+ try {
56
+ // Include updated __index so cross-session pruning picks these JIDs up.
57
+ const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
58
+ await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
59
+ } catch (err) {
60
+ logger?.warn({ err }, 'failed to store tctokens from history sync');
61
+ }
62
+ }
63
+
64
+ /** Cleans a received message for further processing — strips device/agent from JIDs and corrects key perspective for reactions/polls. */
18
65
  export const cleanMessage = (message, meId, meLid) => {
19
- // ensure remoteJid and participant doesn't have device or agent in it
20
66
  if (isHostedPnUser(message.key.remoteJid) || isHostedLidUser(message.key.remoteJid)) {
21
67
  message.key.remoteJid = jidEncode(jidDecode(message.key?.remoteJid)?.user, isHostedPnUser(message.key.remoteJid) ? 's.whatsapp.net' : 'lid');
22
- }
23
- else {
68
+ } else {
24
69
  message.key.remoteJid = jidNormalizedUser(message.key.remoteJid);
25
70
  }
26
71
  if (isHostedPnUser(message.key.participant) || isHostedLidUser(message.key.participant)) {
27
72
  message.key.participant = jidEncode(jidDecode(message.key.participant)?.user, isHostedPnUser(message.key.participant) ? 's.whatsapp.net' : 'lid');
28
- }
29
- else {
73
+ } else {
30
74
  message.key.participant = jidNormalizedUser(message.key.participant);
31
75
  }
76
+
32
77
  const content = normalizeMessageContent(message.message);
33
- // if the message has a reaction, ensure fromMe & remoteJid are from our perspective
34
- if (content?.reactionMessage) {
35
- normaliseKey(content.reactionMessage.key);
36
- }
37
- if (content?.pollUpdateMessage) {
38
- normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
39
- }
78
+ if (content?.reactionMessage) normaliseKey(content.reactionMessage.key);
79
+ if (content?.pollUpdateMessage) normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
80
+
40
81
  function normaliseKey(msgKey) {
41
- // if the reaction is from another user
42
- // we've to correctly map the key to this user's perspective
43
- if (!message.key.fromMe) {
44
- // if the sender believed the message being reacted to is not from them
45
- // we've to correct the key to be from them, or some other participant
46
- msgKey.fromMe = !msgKey.fromMe
47
- ? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) ||
48
- areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
49
- : // if the message being reacted to, was from them
50
- // fromMe automatically becomes false
51
- false;
52
- // set the remoteJid to being the same as the chat the message came from
53
- // TODO: investigate inconsistencies
54
- msgKey.remoteJid = message.key.remoteJid;
55
- // set participant of the message
56
- msgKey.participant = msgKey.participant || message.key.participant;
57
- }
82
+ if (message.key.fromMe) return;
83
+ msgKey.fromMe = !msgKey.fromMe
84
+ ? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) || areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
85
+ : false; // message was from them fromMe is definitively false from our perspective
86
+ msgKey.remoteJid = message.key.remoteJid; // TODO: investigate inconsistencies
87
+ msgKey.participant = msgKey.participant || message.key.participant;
58
88
  }
59
89
  };
90
+
60
91
  // TODO: target:audit AUDIT THIS FUNCTION AGAIN
61
92
  export const isRealMessage = (message) => {
62
93
  const normalizedContent = normalizeMessageContent(message.message);
63
94
  const hasSomeContent = !!getContentType(normalizedContent);
64
- return ((!!normalizedContent ||
65
- REAL_MSG_STUB_TYPES.has(message.messageStubType) ||
66
- REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
95
+ return (
96
+ (!!normalizedContent || REAL_MSG_STUB_TYPES.has(message.messageStubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
67
97
  hasSomeContent &&
68
98
  !normalizedContent?.protocolMessage &&
69
99
  !normalizedContent?.reactionMessage &&
70
- !normalizedContent?.pollUpdateMessage);
100
+ !normalizedContent?.pollUpdateMessage
101
+ );
71
102
  };
103
+
72
104
  export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
105
+
73
106
  /**
74
- * Get the ID of the chat from the given key.
75
- * Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
107
+ * Derive the chat ID from a message key.
108
+ * For non-status broadcasts the chat is the participant (DM from that sender),
109
+ * for everything else it's the remoteJid.
76
110
  */
77
111
  export const getChatId = ({ remoteJid, participant, fromMe }) => {
78
- if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } })
79
- if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) return participant
80
- return remoteJid
112
+ if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } });
113
+ if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
114
+ if (!participant) throw new Boom('Cannot derive chat id: broadcast message key is missing participant', { data: { remoteJid, fromMe } });
115
+ return participant;
116
+ }
117
+ return remoteJid;
81
118
  };
119
+
82
120
  /**
83
- * Decrypt a poll vote
84
- * @param vote encrypted vote
85
- * @param ctx additional info about the poll required for decryption
86
- * @returns list of SHA256 options
121
+ * Decrypt a poll vote.
122
+ * @returns Decoded PollVoteMessage containing the SHA256 option hashes the voter selected.
87
123
  */
88
124
  export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsgId, pollEncKey, voterJid }) {
89
- const sign = Buffer.concat([
90
- toBinary(pollMsgId),
91
- toBinary(pollCreatorJid),
92
- toBinary(voterJid),
93
- toBinary('Poll Vote'),
94
- new Uint8Array([1])
95
- ]);
125
+ const toBinary = (txt) => Buffer.from(txt);
126
+ const sign = Buffer.concat([toBinary(pollMsgId), toBinary(pollCreatorJid), toBinary(voterJid), toBinary('Poll Vote'), new Uint8Array([1])]);
96
127
  const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256');
97
128
  const decKey = hmacSign(sign, key0, 'sha256');
98
129
  const aad = toBinary(`${pollMsgId}\u0000${voterJid}`);
99
- const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad);
100
- return proto.Message.PollVoteMessage.decode(decrypted);
101
- function toBinary(txt) {
102
- return Buffer.from(txt);
103
- }
130
+ return proto.Message.PollVoteMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
104
131
  }
105
132
 
106
- async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
107
- const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
108
- const candidates = []
109
- for (const chat of chats) {
110
- const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0
111
- if (chat.tcToken?.length && ts > 0) { const jid = jidNormalizedUser(chat.id); const storageJid = await resolveTcTokenJid(jid, getLIDForPN); candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined }) }
112
- }
113
- if (!candidates.length) return
114
- const jids = candidates.map(c => c.storageJid)
115
- const existing = await keyStore.get('tctoken', jids)
116
- const entries = {}
117
- for (const c of candidates) {
118
- const existingEntry = existing[c.storageJid]
119
- const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
120
- if (existingTs > 0 && existingTs >= c.ts) continue
121
- entries[c.storageJid] = { ...existingEntry, token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) }
122
- }
123
- if (Object.keys(entries).length) {
124
- logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync')
125
- try { const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries)); await keyStore.set({ tctoken: { ...entries, ...indexWrite } }) }
126
- catch (err) { logger?.warn({ err }, 'failed to store tctokens from history sync') }
127
- }
133
+ /**
134
+ * Decrypt an event RSVP response.
135
+ * Mirrors decryptPollVote but for EventResponseMessage — uses 'Event Response' as the domain separator.
136
+ * @returns Decoded EventResponseMessage with the responder's RSVP choice.
137
+ */
138
+ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, eventMsgId, eventEncKey, responderJid }) {
139
+ const toBinary = (txt) => Buffer.from(txt);
140
+ const sign = Buffer.concat([toBinary(eventMsgId), toBinary(eventCreatorJid), toBinary(responderJid), toBinary('Event Response'), new Uint8Array([1])]);
141
+ const key0 = hmacSign(eventEncKey, new Uint8Array(32), 'sha256');
142
+ const decKey = hmacSign(sign, key0, 'sha256');
143
+ const aad = toBinary(`${eventMsgId}\u0000${responderJid}`);
144
+ return proto.Message.EventResponseMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
128
145
  }
129
146
 
130
- const SELF_ONLY_TYPES = new Set([proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION, proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE, proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC, proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE])
131
147
 
132
148
  const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => {
133
149
  const meId = creds.me.id;
134
150
  const { accountSettings } = creds;
135
151
  const chat = { id: jidNormalizedUser(getChatId(message.key)) };
136
152
  const isRealMsg = isRealMessage(message);
153
+
137
154
  if (isRealMsg) {
138
155
  chat.messages = [{ message }];
139
156
  chat.conversationTimestamp = toNumber(message.messageTimestamp);
140
- // only increment unread count if not CIPHERTEXT and from another person
141
- if (shouldIncrementChatUnread(message)) {
142
- chat.unreadCount = (chat.unreadCount || 0) + 1;
143
- }
157
+ if (shouldIncrementChatUnread(message)) chat.unreadCount = (chat.unreadCount || 0) + 1;
144
158
  }
159
+
145
160
  const content = normalizeMessageContent(message.message);
146
- // unarchive chat if it's a real message, or someone reacted to our message
147
- // and we've the unarchive chats setting on
161
+
162
+ // Unarchive if real message or someone reacted to our message and the setting is on
148
163
  if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
149
164
  chat.archived = false;
150
165
  chat.readOnly = false;
151
166
  }
167
+
152
168
  const protocolMsg = content?.protocolMessage;
153
169
  if (protocolMsg) {
154
- if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_TYPES.has(protocolMsg.type) && !message.key.fromMe) { logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin'); return }
170
+ // Drop self-only protocol messages that didn't come from our own device
171
+ // an attacker could otherwise spoof history syncs, key shares, etc. to manipulate local state.
172
+ if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_PROTOCOL_TYPES.has(protocolMsg.type) && !message.key.fromMe) {
173
+ logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin');
174
+ return;
175
+ }
176
+
155
177
  switch (protocolMsg.type) {
156
- case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
178
+ case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION: {
157
179
  const histNotification = protocolMsg.historySyncNotification;
158
- const process = shouldProcessHistoryMsg;
159
180
  const isLatest = !creds.processedHistoryMessages?.length;
160
- logger?.info({
161
- histNotification,
162
- process,
163
- id: message.key.id,
164
- isLatest
165
- }, 'got history notification');
166
- if (process) {
181
+ logger?.info({ histNotification, process: shouldProcessHistoryMsg, id: message.key.id, isLatest }, 'got history notification');
182
+ if (shouldProcessHistoryMsg) {
167
183
  if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
168
184
  ev.emit('creds.update', { processedHistoryMessages: [...(creds.processedHistoryMessages || []), { key: message.key, messageTimestamp: message.messageTimestamp }] });
169
185
  }
170
- const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
171
- if (data.lidPnMappings?.length) { logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync'); await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync')) }
172
- await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
186
+ const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger);
187
+ if (data.lidPnMappings?.length) {
188
+ logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync');
189
+ await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
190
+ }
191
+ await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
173
192
  ev.emit('messaging-history.set', { ...data, isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined, chunkOrder: histNotification.chunkOrder, peerDataRequestSessionId: histNotification.peerDataRequestSessionId });
174
193
  }
175
194
  break;
176
- case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
195
+ }
196
+
197
+ case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE: {
177
198
  const keys = protocolMsg.appStateSyncKeyShare.keys;
178
199
  if (keys?.length) {
179
200
  let newAppStateSyncKeyId = '';
@@ -188,68 +209,75 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
188
209
  logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys');
189
210
  }, meId);
190
211
  ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
191
- }
192
- else {
212
+ } else {
193
213
  logger?.info({ protocolMsg }, 'recv app state sync with 0 keys');
194
214
  }
195
215
  break;
196
- case proto.Message.ProtocolMessage.Type.REVOKE:
197
- ev.emit('messages.update', [
198
- {
199
- key: {
200
- ...message.key,
201
- id: protocolMsg.key.id
202
- },
203
- update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
204
- }
205
- ]);
216
+ }
217
+
218
+ case proto.Message.ProtocolMessage.Type.REVOKE: {
219
+ ev.emit('messages.update', [{ key: { ...message.key, id: protocolMsg.key.id }, update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } }]);
206
220
  break;
207
- case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
208
- Object.assign(chat, {
209
- ephemeralSettingTimestamp: toNumber(message.messageTimestamp),
210
- ephemeralExpiration: protocolMsg.ephemeralExpiration || null
211
- });
221
+ }
222
+
223
+ case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING: {
224
+ Object.assign(chat, { ephemeralSettingTimestamp: toNumber(message.messageTimestamp), ephemeralExpiration: protocolMsg.ephemeralExpiration || null });
212
225
  break;
213
- case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
214
- const response = protocolMsg.peerDataOperationRequestResponseMessage;
215
- if (response) {
216
- const peerDataOperationResult = response.peerDataOperationResult || []
217
- for (const result of peerDataOperationResult) {
218
- const retryResponse = result?.placeholderMessageResendResponse
219
- if (!retryResponse?.webMessageInfoBytes) continue
220
- try {
221
- const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
222
- const msgId = webMessageInfo.key?.id
223
- const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined
224
- if (msgId) await placeholderResendCache?.del(msgId)
225
- let finalMsg
226
- if (cachedData && typeof cachedData === 'object') { cachedData.message = webMessageInfo.message; if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp; finalMsg = cachedData }
227
- else finalMsg = webMessageInfo
228
- logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend')
229
- ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId })
230
- } catch (err) { logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response') }
231
- }
226
+ }
227
+
228
+ case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT: {
229
+ ev.emit('messages.update', [{
230
+ // Key is in sender's perspective — flip fromMe so it's in ours
231
+ key: { ...message.key, id: protocolMsg.key?.id },
232
+ update: {
233
+ message: { editedMessage: { message: protocolMsg.editedMessage } },
234
+ messageTimestamp: protocolMsg.timestampMs ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000) : message.messageTimestamp,
235
+ },
236
+ }]);
237
+ break;
238
+ }
239
+
240
+ case proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE: {
241
+ const labelAssociationMsg = protocolMsg.memberLabel;
242
+ if (labelAssociationMsg?.label) {
243
+ ev.emit('group.member-tag.update', { groupId: chat.id, label: labelAssociationMsg.label, participant: message.key.participant, participantAlt: message.key.participantAlt, messageTimestamp: Number(message.messageTimestamp) });
232
244
  }
233
245
  break;
234
- case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
235
- ev.emit('messages.update', [
236
- {
237
- // flip the sender / fromMe properties because they're in the perspective of the sender
238
- key: { ...message.key, id: protocolMsg.key?.id },
239
- update: {
240
- message: {
241
- editedMessage: {
242
- message: protocolMsg.editedMessage
243
- }
244
- },
245
- messageTimestamp: protocolMsg.timestampMs
246
- ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000)
247
- : message.messageTimestamp
246
+ }
247
+
248
+ case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: {
249
+ const response = protocolMsg.peerDataOperationRequestResponseMessage;
250
+ if (!response) break;
251
+ // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.)
252
+ for (const result of (response.peerDataOperationResult || [])) {
253
+ const retryResponse = result?.placeholderMessageResendResponse;
254
+ if (!retryResponse?.webMessageInfoBytes) continue;
255
+ try {
256
+ const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
257
+ const msgId = webMessageInfo.key?.id;
258
+ // Retrieve cached original message data — preserves LID details,
259
+ // timestamps, etc. that the phone may omit in its PDO response
260
+ const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined;
261
+ if (msgId) await placeholderResendCache?.del(msgId);
262
+ let finalMsg;
263
+ if (cachedData && typeof cachedData === 'object') {
264
+ // Apply decoded message content onto cached metadata (preserves LID etc.)
265
+ cachedData.message = webMessageInfo.message;
266
+ if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
267
+ finalMsg = cachedData;
268
+ } else {
269
+ finalMsg = webMessageInfo;
248
270
  }
271
+ logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend');
272
+ ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId });
273
+ } catch (err) {
274
+ logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response');
249
275
  }
250
- ]);
276
+ }
251
277
  break;
252
- case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
278
+ }
279
+
280
+ case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC: {
253
281
  const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload;
254
282
  const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload);
255
283
  logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp');
@@ -259,82 +287,73 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
259
287
  pairs.push({ lid: `${lid}@lid`, pn: `${pn}@s.whatsapp.net` });
260
288
  }
261
289
  await signalRepository.lidMapping.storeLIDPNMappings(pairs);
262
- if (pairs.length) {
263
- for (const { pn, lid } of pairs) {
264
- await signalRepository.migrateSession(pn, lid);
265
- }
266
- }
290
+ for (const { pn, lid } of pairs) await signalRepository.migrateSession(pn, lid);
291
+ break;
292
+ }
267
293
  }
268
- }
269
- else if (content?.reactionMessage) {
270
- const reaction = {
271
- ...content.reactionMessage,
272
- key: message.key
273
- };
274
- ev.emit('messages.reaction', [
275
- {
276
- reaction,
277
- key: content.reactionMessage?.key
294
+ } else if (content?.reactionMessage) {
295
+ ev.emit('messages.reaction', [{ reaction: { ...content.reactionMessage, key: message.key }, key: content.reactionMessage?.key }]);
296
+ } else if (content?.encEventResponseMessage) {
297
+ // Decrypt and re-emit event RSVPs — mirrors the poll vote flow but for calendar events
298
+ const encEventResponse = content.encEventResponseMessage;
299
+ const creationMsgKey = encEventResponse.eventCreationMessageKey;
300
+ const eventMsg = await getMessage(creationMsgKey);
301
+ if (eventMsg) {
302
+ try {
303
+ const meIdNormalised = jidNormalizedUser(meId);
304
+ // Event creator JID must be a PN — resolve from LID if necessary
305
+ const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid;
306
+ const eventCreatorPn = isLidUser(eventCreatorKey) ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) : eventCreatorKey;
307
+ const eventCreatorJid = getKeyAuthor({ remoteJid: jidNormalizedUser(eventCreatorPn), fromMe: meIdNormalised === eventCreatorPn }, meIdNormalised);
308
+ const responderJid = getKeyAuthor(message.key, meIdNormalised);
309
+ const eventEncKey = eventMsg?.messageContextInfo?.messageSecret;
310
+ if (!eventEncKey) {
311
+ logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption');
312
+ } else {
313
+ const responseMsg = decryptEventResponse(encEventResponse, { eventEncKey, eventCreatorJid, eventMsgId: creationMsgKey.id, responderJid });
314
+ ev.emit('messages.update', [{ key: creationMsgKey, update: { eventResponses: [{ eventResponseMessageKey: message.key, senderTimestampMs: responseMsg.timestampMs, response: responseMsg }] } }]);
315
+ }
316
+ } catch (err) {
317
+ logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response');
278
318
  }
279
- ]);
280
- }
281
- else if (message.messageStubType) {
319
+ } else {
320
+ logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response');
321
+ }
322
+ } else if (message.messageStubType) {
282
323
  const jid = message.key?.remoteJid;
283
- //let actor = whatsappID (message.participant)
284
324
  let participants;
285
- const emitParticipantsUpdate = (action) => ev.emit('group-participants.update', {
286
- id: jid,
287
- author: message.key.participant,
288
- authorPn: message.key.participantAlt,
289
- participants,
290
- action
291
- });
292
- const emitGroupUpdate = (update) => {
293
- ev.emit('groups.update', [
294
- { id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
295
- ]);
296
- };
297
- const emitGroupRequestJoin = (participant, action, method) => {
298
- ev.emit('group.join-request', {
299
- id: jid,
300
- author: message.key.participant,
301
- authorPn: message.key.participantAlt,
302
- participant: participant.lid,
303
- participantPn: participant.pn,
304
- action,
305
- method: method
306
- });
307
- };
308
- const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)); // ADD SUPPORT FOR LID
325
+
326
+ const emitParticipantsUpdate = (action) => ev.emit('group-participants.update', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participants, action });
327
+ const emitGroupUpdate = (update) => ev.emit('groups.update', [{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }]);
328
+ const emitGroupRequestJoin = (participant, action, method) => ev.emit('group.join-request', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participant: participant.lid, participantPn: participant.pn, action, method });
329
+
330
+ // TODO: ADD SUPPORT FOR LID in participantsIncludesMe
331
+ const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.phoneNumber));
332
+
309
333
  switch (message.messageStubType) {
310
334
  case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
311
- participants = message.messageStubParameters.map(a => { try { return JSON.parse(a) } catch { return { id: a } } }) || []
312
- emitParticipantsUpdate('modify')
313
- break
335
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
336
+ emitParticipantsUpdate('modify');
337
+ break;
314
338
  case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
315
339
  case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
316
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
340
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
317
341
  emitParticipantsUpdate('remove');
318
- // mark the chat read only if you left the group
319
- if (participantsIncludesMe()) {
320
- chat.readOnly = true;
321
- }
342
+ if (participantsIncludesMe()) chat.readOnly = true;
322
343
  break;
323
344
  case WAMessageStubType.GROUP_PARTICIPANT_ADD:
324
345
  case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
325
346
  case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
326
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
327
- if (participantsIncludesMe()) {
328
- chat.readOnly = false;
329
- }
347
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
348
+ if (participantsIncludesMe()) chat.readOnly = false;
330
349
  emitParticipantsUpdate('add');
331
350
  break;
332
351
  case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
333
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
352
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
334
353
  emitParticipantsUpdate('demote');
335
354
  break;
336
355
  case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
337
- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
356
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
338
357
  emitParticipantsUpdate('promote');
339
358
  break;
340
359
  case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
@@ -423,9 +442,9 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
423
442
  )
424
443
  }
425
444
  } */
426
- if (Object.keys(chat).length > 1) {
427
- ev.emit('chats.update', [chat]);
428
- }
445
+
446
+ if (Object.keys(chat).length > 1) ev.emit('chats.update', [chat]);
429
447
  };
448
+
430
449
  export default processMessage;
431
450
  //# sourceMappingURL=process-message.js.map
@@ -1,7 +1,10 @@
1
1
  import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
2
+ import { migrateIndexKey, TC_TOKEN_INDEX_KEY } from '../Utils/index.js';
3
+
2
4
  // Same phone-number pattern as WABinary's isJidBot, applied against the user
3
5
  // part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
4
6
  const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
7
+
5
8
  /**
6
9
  * Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
7
10
  * storage against malformed notifications — WA Web filters server-side but we
@@ -9,93 +12,73 @@ const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
9
12
  * Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
10
13
  */
11
14
  function isRegularUser(jid) {
12
- if (!jid)
13
- return false;
15
+ if (!jid) return false;
14
16
  const user = jid.split('@')[0] ?? '';
15
- if (user === '0')
16
- return false; // PSA
17
- if (BOT_PHONE_REGEX.test(user))
18
- return false; // Bot by phone pattern
19
- if (isJidMetaAI(jid))
20
- return false; // MetaAI (@bot server)
17
+ if (user === '0') return false; // PSA
18
+ if (BOT_PHONE_REGEX.test(user)) return false; // Bot by phone pattern
19
+ if (isJidMetaAI(jid)) return false; // MetaAI (@bot server)
21
20
  return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
22
21
  }
22
+
23
23
  const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
24
24
  const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
25
- /** Sentinel key under `tctoken` store holding a JSON array of tracked storage JIDs for cross-session pruning. */
26
- export const TC_TOKEN_INDEX_KEY = '__index';
27
- /** Read the persisted tctoken JID index and return its entries (never contains the sentinel key itself). */
25
+
26
+ /** Read the persisted tctoken JID index and return its entries. */
28
27
  export async function readTcTokenIndex(keys) {
29
- const data = await keys.get('tctoken', [TC_TOKEN_INDEX_KEY]);
30
- const entry = data[TC_TOKEN_INDEX_KEY];
31
- if (!entry?.token?.length)
32
- return [];
33
- try {
34
- const parsed = JSON.parse(Buffer.from(entry.token).toString());
35
- if (!Array.isArray(parsed))
36
- return [];
37
- return parsed.filter((j) => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY);
38
- }
39
- catch {
40
- return [];
41
- }
28
+ const batch = await migrateIndexKey(keys, 'tctoken');
29
+ return Object.keys(batch).filter(j => j && j !== TC_TOKEN_INDEX_KEY);
42
30
  }
43
- /** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the sentinel key. */
31
+
32
+ /** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the index key. */
44
33
  export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
45
- const persisted = await readTcTokenIndex(keys);
46
- const merged = new Set(persisted);
47
- for (const jid of addedJids) {
48
- if (jid && jid !== TC_TOKEN_INDEX_KEY)
49
- merged.add(jid);
50
- }
51
- return {
52
- [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
53
- };
34
+ const batch = await migrateIndexKey(keys, 'tctoken');
35
+ const merged = { ...batch };
36
+ for (const jid of addedJids) { if (jid && jid !== TC_TOKEN_INDEX_KEY) merged[jid] = merged[jid] || true; }
37
+ return { [TC_TOKEN_INDEX_KEY]: merged };
54
38
  }
39
+
55
40
  // WA Web has separate sender/receiver AB props for these but they're identical today
56
41
  export function isTcTokenExpired(timestamp) {
57
- if (timestamp === null || timestamp === undefined)
58
- return true;
42
+ if (timestamp === null || timestamp === undefined) return true;
59
43
  const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
60
- if (isNaN(ts))
61
- return true;
44
+ if (isNaN(ts)) return true;
62
45
  const now = Math.floor(Date.now() / 1000);
63
46
  const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
64
47
  const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
65
48
  const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
66
49
  return ts < cutoffTimestamp;
67
50
  }
51
+
68
52
  export function shouldSendNewTcToken(senderTimestamp) {
69
- if (senderTimestamp === undefined)
70
- return true;
53
+ if (senderTimestamp === undefined) return true;
71
54
  const now = Math.floor(Date.now() / 1000);
72
55
  const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
73
56
  const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
74
57
  return currentBucket > senderBucket;
75
58
  }
59
+
76
60
  /** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
77
61
  export async function resolveTcTokenJid(jid, getLIDForPN) {
78
- if (isLidUser(jid))
79
- return jid;
62
+ if (isLidUser(jid)) return jid;
80
63
  const lid = await getLIDForPN(jid);
81
64
  return lid ?? jid;
82
65
  }
66
+
83
67
  /** Resolve target JID for issuing privacy token based on AB prop 14303 */
84
68
  export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
85
69
  if (issueToLid) {
86
- if (isLidUser(jid))
87
- return jid;
70
+ if (isLidUser(jid)) return jid;
88
71
  const lid = await getLIDForPN(jid);
89
72
  return lid ?? jid;
90
73
  }
91
- if (!isLidUser(jid))
92
- return jid;
74
+ if (!isLidUser(jid)) return jid;
93
75
  if (getPNForLID) {
94
76
  const pn = await getPNForLID(jid);
95
77
  return pn ?? jid;
96
78
  }
97
79
  return jid;
98
80
  }
81
+
99
82
  export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
100
83
  try {
101
84
  const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
@@ -114,49 +97,31 @@ export async function buildTcTokenFromJid({ authState, jid, baseContent = [], ge
114
97
  }
115
98
  return baseContent.length > 0 ? baseContent : undefined;
116
99
  }
117
- baseContent.push({
118
- tag: 'tctoken',
119
- attrs: {},
120
- content: tcTokenBuffer
121
- });
100
+ baseContent.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer });
122
101
  return baseContent;
123
- }
124
- catch (error) {
102
+ } catch (error) {
125
103
  return baseContent.length > 0 ? baseContent : undefined;
126
104
  }
127
105
  }
106
+
128
107
  export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
129
108
  const tokensNode = getBinaryNodeChild(result, 'tokens');
130
- if (!tokensNode)
131
- return;
109
+ if (!tokensNode) return;
132
110
  const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
133
111
  for (const tokenNode of tokenNodes) {
134
- if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
135
- continue;
136
- }
112
+ if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) continue;
137
113
  // In notifications tokenNode.attrs.jid is your own device JID, not the sender's
138
114
  const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
139
- if (!isRegularUser(rawJid))
140
- continue;
115
+ if (!isRegularUser(rawJid)) continue;
141
116
  const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
142
117
  const existingTcData = await keys.get('tctoken', [storageJid]);
143
118
  const existingEntry = existingTcData[storageJid];
144
119
  const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
145
120
  const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
146
121
  // timestamp-less tokens would be immediately expired
147
- if (!incomingTs)
148
- continue;
149
- if (existingTs > 0 && existingTs > incomingTs)
150
- continue;
151
- await keys.set({
152
- tctoken: {
153
- [storageJid]: {
154
- ...existingEntry,
155
- token: Buffer.from(tokenNode.content),
156
- timestamp: tokenNode.attrs.t
157
- }
158
- }
159
- });
122
+ if (!incomingTs) continue;
123
+ if (existingTs > 0 && existingTs > incomingTs) continue;
124
+ await keys.set({ tctoken: { [storageJid]: { ...existingEntry, token: Buffer.from(tokenNode.content), timestamp: tokenNode.attrs.t } } });
160
125
  onNewJidStored?.(storageJid);
161
126
  }
162
127
  }
package/lib/index.js CHANGED
@@ -31,7 +31,7 @@ const banner = `
31
31
  const info = `
32
32
  ┌───────────────────────────────────────────────────────────────────────┐
33
33
  │ 📦 Package: @nexustechpro/baileys │
34
- │ 🔖 Version: 2.1.2
34
+ │ 🔖 Version: 2.1.3
35
35
  │ ⚡ Status: Production Ready │
36
36
  ├───────────────────────────────────────────────────────────────────────┤
37
37
  │ 🚀 Advanced WhatsApp Web API Client │
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@nexustechpro/baileys",
4
4
  "type": "module",
5
- "version": "2.1.2",
5
+ "version": "2.1.3",
6
6
  "description": "Advanced WhatsApp Web API built on Baileys — interactive messages, buttons, carousels, products, events, payments, polls, albums, sticker packs and more.",
7
7
  "keywords": [
8
8
  "whatsapp",