fix(tctoken): address all 7 PR #386 review findings

All 7 inline review comments validated as real bugs (zero false
positives). Fixes applied:

#1 Codex P1 (CRITICAL) — process-message.ts: storeTcTokensFromHistorySync
   ran BEFORE storeLIDPNMappings. On a fresh device with empty mapping
   cache, resolveTcTokenJid(PN) returned null for every chat → tokens
   stored under PN keys → send path resolves PN→LID, misses them →
   error 463 on first multi-device send. Defeated the purpose of the
   whole TIER 1.3 backport. Reordered: mappings first, tctokens after.

#3 Copilot (MAJOR) — messages-send.ts: PSA/bot gating relied on
   `destinationJid === PSA_WID` (which is '0@c.us') and isJidBot
   (also @c.us-only), but destinationJid arrives normalized to
   @s.whatsapp.net. Issuance was leaking to PSA/bot contacts.
   Replaced with `!isRegularUser(destinationJid)` — the same Wid.
   isRegularUser() port the store path already uses, which handles
   @c.us / @s.whatsapp.net / @hosted / @hosted.lid / @lid uniformly.

#4 Copilot (MAJOR) — tc-token-utils.ts: resolveIssuanceJid used
   strict isLidUser() so hosted LIDs (@hosted.lid) skipped resolution,
   and passed un-normalized JIDs to getLIDForPN (which early-returns
   unless isAnyPnUser, breaking @c.us inputs). Fixed by normalizing
   upfront with jidNormalizedUser and using isAnyLidUser/isAnyPnUser.
   Added 3 new tests covering @c.us → normalize → resolve, hosted.lid
   passthrough on issueToLid=true, and hosted.lid → getPNForLID call
   on issueToLid=false.

#6 Copilot (MAJOR) — messages-recv.ts: scheduleTcTokenIndexSave wrote
   the index from the in-memory tcTokenKnownJids set, OVERWRITING any
   JIDs added by cross-layer paths (messages-send issuance,
   process-message history sync) that wrote via
   buildMergedTcTokenIndexWrite without updating the in-memory set.
   Each debounced flush silently dropped those JIDs. Same bug in the
   connection.update flush path. Both now use buildMergedTcTokenIndexWrite
   so the persisted index is preserved.

#7 CodeRabbit Minor — process-message.ts: storeTcTokensFromHistorySync's
   loop captured `existing` once before the loop, so when two
   candidates resolved to the same storageJid (PN+LID alias collision
   through resolveTcTokenJid, or duplicate chunks across syncs), the
   second iteration read the original persisted entry instead of the
   in-progress entries[storageJid] from iter 1. A lower-ts entry could
   overwrite a higher-ts one. Fixed with
   `entries[c.storageJid] ?? existing[c.storageJid]`.

#5 Copilot (DEFENSIVE) — identity-change-handler.ts: onBeforeSessionRefresh
   was called outside the try/catch around assertSessions. A throwing
   consumer callback would abort identity-change recovery and prevent
   the session refresh entirely. Wrapped in try/catch with warn-log;
   assertSessions still runs.

#2 Copilot (TYPO) — messages-recv.ts: 'racets' → 'races' in the
   reissueTcTokenAfterIdentityChange docstring.

Tests: 35/35 suites, 824/824 (+3 new for resolveIssuanceJid normalization).
Zero diff in src/Utils/messages.ts (carousel/buttons/lists),
src/Socket/groups.ts, WAProto/* — customizations untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Renato Alcara
2026-04-25 18:28:13 -03:00
parent 7333eccded
commit 8b5818be31
6 changed files with 102 additions and 27 deletions
+17 -6
View File
@@ -114,7 +114,11 @@ async function storeTcTokensFromHistorySync(
const entries: Record<string, { token: Buffer; timestamp?: string; senderTimestamp?: number }> = {}
for (const c of candidates) {
const existingEntry = existing[c.storageJid]
// Same-batch dedup: when two chats resolve to the same storageJid (e.g. PN+LID
// aliases collapsing through resolveTcTokenJid, or duplicate chunks across
// retries), prefer the value already written by an earlier iteration so a
// lower-ts entry can't overwrite a higher-ts one captured from `existing`.
const existingEntry = entries[c.storageJid] ?? existing[c.storageJid]
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
// Strict > guard: equal timestamps are skipped so we never clobber
// senderTimestamp written by other layers (issuance after send, etc).
@@ -496,13 +500,14 @@ const processMessage = async (
const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger)
// Persist tctokens carried by history-sync chats BEFORE emitting messaging-history.set
// — listeners may immediately fire outbound sends that need the tctoken, and the store
// has to be populated first to avoid an error 463 on the first multi-device send.
await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
// Emit LID-PN mappings from history sync
// This is how WhatsApp Web learns mappings for chats with non-contacts
//
// MUST run BEFORE storeTcTokensFromHistorySync — otherwise resolveTcTokenJid()
// can't resolve PN→LID for fresh-device chats (mapping cache is empty), tokens
// get persisted under PN keys, and the send path (which resolves to LID first)
// misses them — exactly the error 463 scenario this whole change is meant to
// prevent. Catches Codex P1 review on PR #386.
if (data.lidPnMappings?.length) {
logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync')
// eslint-disable-next-line max-depth
@@ -527,6 +532,12 @@ const processMessage = async (
}
}
// Persist tctokens carried by history-sync chats BEFORE emitting messaging-history.set
// — listeners may immediately fire outbound sends that need the tctoken, and the store
// has to be populated first to avoid an error 463 on the first multi-device send.
// Runs AFTER storeLIDPNMappings (see comment above) so LID resolution works.
await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
ev.emit('messaging-history.set', {
...data,
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,