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>
Backports the 4 critical/major gaps the audit identified vs Baileys
PR #2339, surgically and without touching carousel, buttons, lists,
LID/PN normalization, Bad MAC handling or app-state-sync code.
TIER 1 — eliminates production error 463 in two scenarios:
- onBeforeSessionRefresh callback (identity-change-handler.ts):
invoked BEFORE assertSessions for an existing-session identity
change. Skipped for self / companion / debounced / offline paths.
- reissueTcTokenAfterIdentityChange (messages-recv.ts): fire-and-
forget, runs IN PARALLEL with the session refresh (not after) and
preserves the existing senderTimestamp so the contact gets a fresh
token in the same bucket window. Replaces the fork's previous
post-refresh reissue, which raced with the next outbound send.
- storeTcTokensFromHistorySync (process-message.ts): extracts
tcToken / tcTokenTimestamp / tcTokenSenderTimestamp from history-
sync chats and persists them BEFORE messaging-history.set fires,
so multi-device login doesn't lose tokens. Strict > monotonicity
(matches upstream).
TIER 2 — server AB-prop gating:
- serverProps in chats.ts: parses 10518 (privacy_token_sending_on_
all_1_on_1_messages), 9666 (profile_scraping_privacy_token_in_
photo_iq), 14303 (lid_trusted_token_issue_to_lid). Defaults match
WA Web (true / true / false). Exported in the socket return so
messages-send/messages-recv can read it.
- profilePictureUrl gates inclusion on serverProps.profilePicPrivacyToken.
- 1:1 send gates attach on serverProps.privacyTokenOn1to1.
- resolveIssuanceJid (tc-token-utils.ts): routes issuance to LID
vs PN based on AB prop 14303. Used by both the post-send fire-
and-forget and the identity-change reissue.
TIER 3 — defensive hardening:
- isRegularUser (tc-token-utils.ts): Wid.isRegularUser() port that
rejects PSA WID '0', bot phone patterns (1313555XXXX / 131655500XX)
and MetaAI (@bot). storeTcTokensFromIqResult drops malformed
notifications before writing. Send-side issuance also gates on
PSA / bot / protocol-message exclusions (matches WA Web's
TcTokenChatAction).
- inFlightTcTokenIssuance Set (messages-send.ts): dedupes
fire-and-forget issuance when several rapid sends to the same
contact happen before senderTimestamp persists. Distinct from
the existing tcTokenFetchingJids (which dedupes inbound fetches).
- TC_TOKEN_INDEX_KEY exported from tc-token-utils.ts and re-used
in messages-recv.ts (was previously inlined as a separate local
const — risk of divergence on rename). Same value '__index'.
- readTcTokenIndex / buildMergedTcTokenIndexWrite: cross-session
prune index helpers so issuance / history-sync / pruner all
merge with the persisted set instead of clobbering each other.
Audit findings explicitly addressed:
- TC_TOKEN_INDEX_KEY duplication: unified via import.
- Index write race (prune vs issuance): documented; worst case is a
one-cycle delay of pruning, no data loss.
- Identity-change reissue + post-send issuance double-fire: bounded
by bucket coalescing — both use the same senderTimestamp window so
even if both IQs go out, the persisted state converges.
Tests:
- identity-change-handling: 2 new cases covering onBeforeSessionRefresh
ordering (fires BEFORE assertSessions) and skip behavior on
no_identity / offline / self.
- tc-token: 32 new cases across isRegularUser (PSA / bot / MetaAI /
hosted), resolveIssuanceJid (AB prop 14303 ON/OFF, missing mappings),
TC_TOKEN_INDEX_KEY ('__index' frozen), readTcTokenIndex (corruption /
empty / sentinel filter), buildMergedTcTokenIndexWrite (merge /
sentinel drop / empty), storeTcTokensFromIqResult gating (PSA / bot /
MetaAI rejection, fallbackJid storage routing, monotonicity).
Suite: 35/35, 821/821 passing (+115 vs baseline 706).
Customizations untouched: zero diff in src/Utils/messages.ts (carousel
generators), src/Socket/groups.ts, WAProto/*. Confirmed via grep for
carousel/button/interactive/nativeFlow/list/biz/album in the modified
files — no matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add interactive message detection helpers (getButtonType, isCarouselMessage, etc.)
- Inject biz node with interactive/native_flow for non-carousel interactive messages
- Inject biz + quality_control with decision_id for carousel messages
- Skip bot node for native_flow/carousel/catalog (breaks Web rendering)
- Force device-identity inclusion for carousel messages
- Append deferred biz/bot/quality_control nodes after device-identity and tctoken
- Store tctoken under both LID and PN for reliable lookup
- Wire onNewJidStored callback in messages-send.ts so proactively
fetched tokens are registered in the pruning index
- Batch keystore reads in pruneExpiredTcTokens (1 query instead of N)
- Persist lastTcTokenPruneTs in keystore so 24h throttle survives restarts
- Replace parseInt() with Number() for safer timestamp parsing
- Normalize JIDs via jidNormalizedUser() before LID lookup in
resolveTcTokenJid to prevent device-suffix mismatches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>