@itsliaaa/baileys 0.1.32 → 0.2.0

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.
@@ -33,6 +33,7 @@ export const DECRYPTION_RETRY_CONFIG = {
33
33
  baseDelayMs: 100,
34
34
  sessionRecordErrors: ['No session record', 'SessionError: No session record']
35
35
  };
36
+ /** NACK reason codes we send to the server (client → server) */
36
37
  export const NACK_REASONS = {
37
38
  ParsingError: 487,
38
39
  UnrecognizedStanza: 488,
@@ -48,6 +49,17 @@ export const NACK_REASONS = {
48
49
  UnsupportedLIDGroup: 551,
49
50
  DBOperationFailed: 552
50
51
  };
52
+ /**
53
+ * Server-side error codes returned in ack stanzas (server → client) that we
54
+ * currently have dedicated handlers for. Extend as more handlers are added.
55
+ * Distinct from the client-side NackReason enum (WAWebCreateNackFromStanza).
56
+ */
57
+ export const SERVER_ERROR_CODES = {
58
+ /** 1:1 message missing privacy token (tctoken) */
59
+ MissingTcToken: '463',
60
+ /** Stanza validation failure (SMAX_INVALID) — likely stale device session */
61
+ SmaxInvalid: '479'
62
+ };
51
63
  export const extractAddressingContext = (stanza) => {
52
64
  let senderAlt;
53
65
  let recipientAlt;
@@ -148,10 +160,12 @@ export function decodeMessageNode(stanza, meId, meLid) {
148
160
  const key = {
149
161
  remoteJid: chatId,
150
162
  remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
163
+ remoteJidUsername: !isJidGroup(chatId) ? stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username : undefined,
151
164
  fromMe,
152
165
  id: msgId,
153
166
  participant,
154
167
  participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
168
+ participantUsername: stanza.attrs.participant ? stanza.attrs.participant_username : undefined,
155
169
  addressingMode: addressingContext.addressingMode,
156
170
  ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
157
171
  };
@@ -252,6 +252,7 @@ eventData, logger) {
252
252
  data.historySets.empty = false;
253
253
  data.historySets.syncType = eventData.syncType;
254
254
  data.historySets.progress = eventData.progress;
255
+ data.historySets.chunkOrder = eventData.chunkOrder;
255
256
  data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId;
256
257
  data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest;
257
258
  break;
@@ -519,6 +520,7 @@ function consolidateEvents(data) {
519
520
  syncType: data.historySets.syncType,
520
521
  progress: data.historySets.progress,
521
522
  isLatest: data.historySets.isLatest,
523
+ chunkOrder: data.historySets.chunkOrder,
522
524
  peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
523
525
  };
524
526
  }
@@ -316,6 +316,15 @@ export const getCallStatusFromNode = ({ tag, attrs }) => {
316
316
  status = 'terminate';
317
317
  }
318
318
  break;
319
+ case 'preaccept':
320
+ status = 'preaccept';
321
+ break;
322
+ case 'transport':
323
+ status = 'transport';
324
+ break;
325
+ case 'relaylatency':
326
+ status = 'relaylatency';
327
+ break;
319
328
  case 'reject':
320
329
  status = 'reject';
321
330
  break;
@@ -1,5 +1,6 @@
1
+ import { pipeline } from 'stream/promises';
1
2
  import { promisify } from 'util';
2
- import { inflate } from 'zlib';
3
+ import { createInflate, inflate } from 'zlib';
3
4
  import { proto } from '../../WAProto/index.js';
4
5
  import { WAMessageStubType } from '../Types/index.js';
5
6
  import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/index.js';
@@ -24,13 +25,13 @@ const extractPnFromMessages = (messages) => {
24
25
  };
25
26
  export const downloadHistory = async (msg, options) => {
26
27
  const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options });
27
- const bufferArray = [];
28
- for await (const chunk of stream) {
29
- bufferArray.push(chunk);
30
- }
31
- let buffer = Buffer.concat(bufferArray);
32
- // decompress buffer
33
- buffer = await inflatePromise(buffer);
28
+ // Pipe decrypted stream directly through zlib inflate
29
+ // This avoids allocating an intermediate buffer for the compressed data
30
+ const inflater = createInflate();
31
+ const chunks = [];
32
+ inflater.on('data', (chunk) => chunks.push(chunk));
33
+ await pipeline(stream, inflater);
34
+ const buffer = Buffer.concat(chunks);
34
35
  const syncData = proto.HistorySync.decode(buffer);
35
36
  return syncData;
36
37
  };
@@ -55,6 +56,7 @@ export const processHistoryMessage = (item, logger) => {
55
56
  contacts.push({
56
57
  id: chat.id,
57
58
  name: chat.displayName || chat.name || chat.username || undefined,
59
+ username: chat.username || undefined,
58
60
  lid: chat.lidJid || chat.accountLid || undefined,
59
61
  phoneNumber: chat.pnJid || undefined
60
62
  });
@@ -95,7 +97,7 @@ export const processHistoryMessage = (item, logger) => {
95
97
  });
96
98
  }
97
99
  }
98
- chats.push({ ...chat });
100
+ chats.push(chat);
99
101
  }
100
102
  break;
101
103
  case proto.HistorySync.HistorySyncType.PUSH_NAME:
@@ -37,6 +37,7 @@ export async function handleIdentityChange(node, ctx) {
37
37
  ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing');
38
38
  return { action: 'skipped_offline' };
39
39
  }
40
+ ctx.onBeforeSessionRefresh?.(from);
40
41
  try {
41
42
  await ctx.assertSessions([from], true);
42
43
  return { action: 'session_refreshed' };
@@ -516,7 +516,7 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
516
516
  };
517
517
  const output = new Transform({
518
518
  transform(chunk, _, callback) {
519
- let data = Buffer.concat([remainingBytes, chunk]);
519
+ let data = remainingBytes.length ? Buffer.concat([remainingBytes, chunk]) : chunk;
520
520
  const decryptLength = toSmallestChunkSize(data.length);
521
521
  remainingBytes = data.slice(decryptLength);
522
522
  data = data.slice(0, decryptLength);
@@ -657,6 +657,7 @@ export const generateWAMessageContent = async (message, options) => {
657
657
  else if (hasNonNullishProperty(message, 'code') ||
658
658
  hasNonNullishProperty(message, 'expressions') ||
659
659
  hasNonNullishProperty(message, 'items') ||
660
+ hasNonNullishProperty(message, 'links') ||
660
661
  hasNonNullishProperty(message, 'table') ||
661
662
  hasNonNullishProperty(message, 'richResponse')) {
662
663
  m = prepareRichResponseMessage(message);
@@ -673,6 +674,7 @@ export const generateWAMessageContent = async (message, options) => {
673
674
  extContent.description = urlInfo.description;
674
675
  extContent.title = urlInfo.title;
675
676
  extContent.previewType = urlInfo.previewType ?? 0;
677
+ extContent.linkPreviewMetadata = urlInfo.linkPreviewMetadata;
676
678
  const img = urlInfo.highQualityThumbnail;
677
679
  if (img) {
678
680
  extContent.thumbnailDirectPath = img.directPath;
@@ -684,6 +686,19 @@ export const generateWAMessageContent = async (message, options) => {
684
686
  extContent.thumbnailEncSha256 = img.fileEncSha256;
685
687
  }
686
688
  }
689
+ let faviconInfo = message.favicon;
690
+ if (faviconInfo) {
691
+ faviconInfo = await generateWAMessageMedia(faviconInfo, options);
692
+ extContent.faviconMMSMetadata = {
693
+ thumbnailDirectPath: faviconInfo.directPath,
694
+ mediaKey: faviconInfo.mediaKey,
695
+ mediaKeyTimestamp: faviconInfo.mediaKeyTimestamp,
696
+ thumbnailWidth: 32,
697
+ thumbnailHeight: 32,
698
+ thumbnailSha256: faviconInfo.fileSha256,
699
+ thumbnailEncSha256: faviconInfo.fileEncSha256
700
+ };
701
+ }
687
702
  if (options.backgroundColor) {
688
703
  extContent.backgroundArgb = await assertColor(options.backgroundColor);
689
704
  }
@@ -1270,12 +1285,6 @@ export const generateWAMessageContent = async (message, options) => {
1270
1285
  }
1271
1286
  m = { invoiceMessage };
1272
1287
  }
1273
- if (shouldIncludeReportingToken(m)) {
1274
- m.messageContextInfo = m.messageContextInfo || {};
1275
- if (!m.messageContextInfo.messageSecret) {
1276
- m.messageContextInfo.messageSecret = randomBytes(32);
1277
- }
1278
- }
1279
1288
  // Lia@Changes 31-01-26 --- Add direct externalAdReply access (no need to create contextInfo first)
1280
1289
  if (hasOptionalProperty(message, 'externalAdReply') && !!message.externalAdReply) {
1281
1290
  const messageType = Object.keys(m)[0];
@@ -1391,6 +1400,12 @@ export const generateWAMessageContent = async (message, options) => {
1391
1400
  }
1392
1401
  }
1393
1402
  }
1403
+ if (shouldIncludeReportingToken(m)) {
1404
+ m.messageContextInfo = m.messageContextInfo || {};
1405
+ if (!m.messageContextInfo.messageSecret) {
1406
+ m.messageContextInfo.messageSecret = randomBytes(32);
1407
+ }
1408
+ }
1394
1409
  return proto.Message.create(m);
1395
1410
  };
1396
1411
  export const generateWAMessageFromContent = (jid, message, options) => {
@@ -5,6 +5,7 @@ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJid
5
5
  import { aesDecryptGCM, hmacSign } from './crypto.js';
6
6
  import { getKeyAuthor, toNumber } from './generics.js';
7
7
  import { downloadAndProcessHistorySyncNotification } from './history.js';
8
+ import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
8
9
  const REAL_MSG_STUB_TYPES = new Set([
9
10
  WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
10
11
  WAMessageStubType.CALL_MISSED_GROUP_VOICE,
@@ -12,6 +13,53 @@ const REAL_MSG_STUB_TYPES = new Set([
12
13
  WAMessageStubType.CALL_MISSED_VOICE
13
14
  ]);
14
15
  const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
16
+ async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
17
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
18
+ const candidates = [];
19
+ for (const chat of chats) {
20
+ const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
21
+ if (chat.tcToken?.length && ts > 0) {
22
+ const jid = jidNormalizedUser(chat.id);
23
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
24
+ candidates.push({
25
+ storageJid,
26
+ token: Buffer.from(chat.tcToken),
27
+ ts,
28
+ senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined
29
+ });
30
+ }
31
+ }
32
+ if (!candidates.length) {
33
+ return;
34
+ }
35
+ const jids = candidates.map(c => c.storageJid);
36
+ const existing = await keyStore.get('tctoken', jids);
37
+ const entries = {};
38
+ for (const c of candidates) {
39
+ const existingEntry = existing[c.storageJid];
40
+ const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
41
+ if (existingTs > 0 && existingTs >= c.ts) {
42
+ continue;
43
+ }
44
+ entries[c.storageJid] = {
45
+ ...existingEntry,
46
+ token: c.token,
47
+ timestamp: String(c.ts),
48
+ ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {})
49
+ };
50
+ }
51
+ if (Object.keys(entries).length) {
52
+ logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
53
+ try {
54
+ // Include updated __index so cross-session pruning picks these JIDs up.
55
+ const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
56
+ await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
57
+ }
58
+ catch (err) {
59
+ logger?.warn({ err }, 'failed to store tctokens from history sync');
60
+ }
61
+ }
62
+ };
15
63
  /** Cleans a received message to further processing */
16
64
  export const cleanMessage = (message, meId, meLid) => {
17
65
  // ensure remoteJid and participant doesn't have device or agent in it
@@ -174,9 +222,11 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
174
222
  .storeLIDPNMappings(data.lidPnMappings)
175
223
  .catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
176
224
  }
225
+ await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
177
226
  ev.emit('messaging-history.set', {
178
227
  ...data,
179
228
  isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
229
+ chunkOrder: histNotification.chunkOrder,
180
230
  peerDataRequestSessionId: histNotification.peerDataRequestSessionId
181
231
  });
182
232
  }
@@ -383,12 +433,13 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
383
433
  id: jid,
384
434
  author: message.key.participant,
385
435
  authorPn: message.key.participantAlt,
436
+ authorUsername: message.key.participantUsername,
386
437
  participants,
387
438
  action
388
439
  });
389
440
  const emitGroupUpdate = (update) => {
390
441
  ev.emit('groups.update', [
391
- { id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
442
+ { id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }
392
443
  ]);
393
444
  };
394
445
  const emitGroupRequestJoin = (participant, action, method) => {
@@ -396,6 +447,7 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
396
447
  id: jid,
397
448
  author: message.key.participant,
398
449
  authorPn: message.key.participantAlt,
450
+ authorUsername: message.key.participantUsername,
399
451
  participant: participant.lid,
400
452
  participantPn: participant.pn,
401
453
  action,
@@ -11,7 +11,6 @@ import { LANGUAGE_KEYWORDS } from '../WABinary/constants.js';
11
11
  import { CodeHighlightType, RichSubMessageType } from '../Types/RichType.js';
12
12
  import { proto } from '../../WAProto/index.js';
13
13
  import { unixTimestampSeconds } from './generics.js';
14
- const textEncoder = new TextEncoder();
15
14
  const NOOP = new Set([]);
16
15
  export const tokenizeCode = (code, language = 'javascript') => {
17
16
  const keywords = LANGUAGE_KEYWORDS[language] || NOOP;
@@ -121,33 +120,11 @@ export const toUnified = (submessages) =>
121
120
  }
122
121
  };
123
122
  case RichSubMessageType.TEXT:
124
- const shouldAddInlineEntity = index == 0;
125
- const inlineEntity = [{
126
- key: 'Starseed',
127
- metadata: {
128
- reference_id: 1,
129
- reference_url: DONATE_URL,
130
- reference_title: 'For Donation via Saweria',
131
- reference_display_name: 'Donate',
132
- sources: [{
133
- source_type: 'THIRD_PARTY',
134
- source_display_name: 'Donate',
135
- source_subtitle: '',
136
- source_url: DONATE_URL
137
- }],
138
- __typename: 'GenAISearchCitationItem'
139
- }
140
- }];
141
- const textEntity = shouldAddInlineEntity ?
142
- '{{Starseed}}¹{{/Starseed}}' :
143
- '';
144
123
  return {
145
124
  view_model: {
146
125
  primitive: {
147
- text: submessage.messageText + textEntity,
148
- inline_entities: shouldAddInlineEntity ?
149
- inlineEntity :
150
- [],
126
+ text: submessage.messageText,
127
+ inline_entities: submessage.inlineEntities || [],
151
128
  __typename: 'GenAIMarkdownTextUXPrimitive'
152
129
  },
153
130
  __typename: 'GenAISingleLayoutViewModel'
@@ -212,14 +189,15 @@ export const buildAdditionalBotMetadataContext = (submessages) => {
212
189
  return { sources, mediaDetailsMetadataList };
213
190
  }
214
191
  export const prepareRichResponseMessage = (content) => {
215
- const { code, contentText, expressions, footerText, headerText, items, language, richResponse, table, text, title } = content;
192
+ const { code, contentText, expressions, footerText, headerText, items, language, links, noHeading, richResponse, table, text, title } = content;
216
193
  let submessages = [];
217
194
  if (Array.isArray(richResponse)) {
218
195
  submessages = richResponse.map((submessage) => {
219
196
  if (submessage.text) {
220
197
  return {
221
198
  messageType: RichSubMessageType.TEXT,
222
- messageText: submessage.text
199
+ messageText: submessage.text,
200
+ inlineEntities: submessage.inlineEntities
223
201
  };
224
202
  }
225
203
  else if (submessage.code) {
@@ -301,16 +279,42 @@ export const prepareRichResponseMessage = (content) => {
301
279
  }
302
280
  });
303
281
  }
282
+ else if (links) {
283
+ links.forEach((linkField, index) => {
284
+ const prefix = 'SS_' + index;
285
+ const url = linkField.url || DONATE_URL;
286
+ const sources = linkField.sources?.map((sourceField) => ({
287
+ source_type: 'THIRD_PARTY',
288
+ source_display_name: sourceField.displayName || 'Donate',
289
+ source_subtitle: sourceField.subtitle || 'Saweria',
290
+ source_url: sourceField.url || url
291
+ }));
292
+ submessages.push({
293
+ messageType: RichSubMessageType.TEXT,
294
+ messageText: linkField.text + ` {{${prefix}}}¹{{/${prefix}}} `,
295
+ inlineEntities: [{
296
+ key: prefix,
297
+ metadata: {
298
+ reference_id: index + 1,
299
+ reference_url: url,
300
+ reference_title: linkField.title || 'For Donation via Saweria',
301
+ reference_display_name: linkField.displayName || 'Donation',
302
+ sources: sources || [],
303
+ __typename: 'GenAISearchCitationItem'
304
+ }
305
+ }]
306
+ });
307
+ });
308
+ }
304
309
  else if (table) {
305
- const tableRows = table.map((items, index) => ({
306
- isHeading: index == 0,
307
- items
308
- }));
309
310
  submessages.push({
310
311
  messageType: RichSubMessageType.TABLE,
311
312
  tableMetadata: {
312
313
  title,
313
- rows: tableRows
314
+ rows: table.map((items, index) => ({
315
+ isHeading: !noHeading && index == 0,
316
+ items
317
+ }))
314
318
  }
315
319
  });
316
320
  }
@@ -323,16 +327,17 @@ export const prepareRichResponseMessage = (content) => {
323
327
  }
324
328
  const unified = toUnified(submessages);
325
329
  const message = wrapToBotForwardedMessage({
326
- submessages,
330
+ submessages: [],
327
331
  messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
328
332
  unifiedResponse: {
329
- data: textEncoder.encode(JSON.stringify(unified))
333
+ data: Buffer.from(JSON.stringify(unified), 'utf-8') // Lia@Note 25-04-26 --- Expects "ArrayBufferLike"
330
334
  },
331
335
  contextInfo: {
332
336
  isForwarded: true,
333
337
  forwardingScore: 1,
334
338
  forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
335
- forwardOrigin: 4
339
+ forwardOrigin: 4,
340
+ botMessageSharingInfo: { forwardScore: 1 }
336
341
  }
337
342
  });
338
343
  // Lia@Note 17-04-26 --- TODO: Fill mediaDetailsMetadataList and sources field
@@ -21,6 +21,7 @@ export const processContactAction = (action, id, logger) => {
21
21
  {
22
22
  id,
23
23
  name: action.fullName || action.firstName || action.username || undefined,
24
+ username: action.username || undefined,
24
25
  lid: lidJid || undefined,
25
26
  phoneNumber
26
27
  }
@@ -1,9 +1,119 @@
1
- export async function buildTcTokenFromJid({ authState, jid, baseContent = [] }) {
1
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
2
+ // Same phone-number pattern as WABinary's isJidBot, applied against the user
3
+ // part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
4
+ const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
5
+ /**
6
+ * Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
7
+ * storage against malformed notifications — WA Web filters server-side but we
8
+ * defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
9
+ * Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
10
+ */
11
+ function isRegularUser(jid) {
12
+ if (!jid)
13
+ return false;
14
+ 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)
21
+ return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
22
+ }
23
+ const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
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). */
28
+ 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 [];
2
33
  try {
3
- const tcTokenData = await authState.keys.get('tctoken', [jid]);
4
- const tcTokenBuffer = tcTokenData?.[jid]?.token;
5
- if (!tcTokenBuffer)
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
+ }
42
+ }
43
+ /** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the sentinel key. */
44
+ 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
+ };
54
+ }
55
+ // WA Web has separate sender/receiver AB props for these but they're identical today
56
+ export function isTcTokenExpired(timestamp) {
57
+ if (timestamp === null || timestamp === undefined)
58
+ return true;
59
+ const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
60
+ if (isNaN(ts))
61
+ return true;
62
+ const now = Math.floor(Date.now() / 1000);
63
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
64
+ const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
65
+ const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
66
+ return ts < cutoffTimestamp;
67
+ }
68
+ export function shouldSendNewTcToken(senderTimestamp) {
69
+ if (senderTimestamp === undefined)
70
+ return true;
71
+ const now = Math.floor(Date.now() / 1000);
72
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
73
+ const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
74
+ return currentBucket > senderBucket;
75
+ }
76
+ /** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
77
+ export async function resolveTcTokenJid(jid, getLIDForPN) {
78
+ if (isLidUser(jid))
79
+ return jid;
80
+ const lid = await getLIDForPN(jid);
81
+ return lid ?? jid;
82
+ }
83
+ /** Resolve target JID for issuing privacy token based on AB prop 14303 */
84
+ export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
85
+ if (issueToLid) {
86
+ if (isLidUser(jid))
87
+ return jid;
88
+ const lid = await getLIDForPN(jid);
89
+ return lid ?? jid;
90
+ }
91
+ if (!isLidUser(jid))
92
+ return jid;
93
+ if (getPNForLID) {
94
+ const pn = await getPNForLID(jid);
95
+ return pn ?? jid;
96
+ }
97
+ return jid;
98
+ }
99
+ export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
100
+ try {
101
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
102
+ const tcTokenData = await authState.keys.get('tctoken', [storageJid]);
103
+ const entry = tcTokenData?.[storageJid];
104
+ const tcTokenBuffer = entry?.token;
105
+ if (!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
106
+ if (tcTokenBuffer) {
107
+ // Preserve senderTimestamp so shouldSendNewTcToken() keeps its dedupe state
108
+ // after we drop the unusable peer token. Only wipe the record entirely when
109
+ // there's nothing worth keeping.
110
+ const cleared = entry?.senderTimestamp !== undefined
111
+ ? { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }
112
+ : null;
113
+ await authState.keys.set({ tctoken: { [storageJid]: cleared } });
114
+ }
6
115
  return baseContent.length > 0 ? baseContent : undefined;
116
+ }
7
117
  baseContent.push({
8
118
  tag: 'tctoken',
9
119
  attrs: {},
@@ -14,4 +124,40 @@ export async function buildTcTokenFromJid({ authState, jid, baseContent = [] })
14
124
  catch (error) {
15
125
  return baseContent.length > 0 ? baseContent : undefined;
16
126
  }
17
- }
127
+ }
128
+ export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
129
+ const tokensNode = getBinaryNodeChild(result, 'tokens');
130
+ if (!tokensNode)
131
+ return;
132
+ const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
133
+ for (const tokenNode of tokenNodes) {
134
+ if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
135
+ continue;
136
+ }
137
+ // In notifications tokenNode.attrs.jid is your own device JID, not the sender's
138
+ const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
139
+ if (!isRegularUser(rawJid))
140
+ continue;
141
+ const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
142
+ const existingTcData = await keys.get('tctoken', [storageJid]);
143
+ const existingEntry = existingTcData[storageJid];
144
+ const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
145
+ const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
146
+ // 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
+ });
160
+ onNewJidStored?.(storageJid);
161
+ }
162
+ }
163
+ //# sourceMappingURL=tc-token-utils.js.map