Detail Bug Report
https://app.detail.dev/org_ee14aa77-b24a-40b2-b22d-66bd31931f4a/bugs/bug_996785d0-a4d7-4308-b731-1757ff807073
Introduced in #359 by @setkyar on Sep 10, 2026
Summary
- Context:
getEnrichedGroupParticipants in apps/api/src/services/group.service.ts builds the per-member displayName rendered on the group info panel. Commit 3403183 ("fix(groups): mask unnamed LID participants in group info panel") added getLidDisplayName(jid) so that @lid/@hosted.lid members resolve to the privacy-safe label WhatsApp user (ID …XXXX) instead of their opaque identity digits.
- Bug: The mask only fires at the terminal fallback. The earlier identity-name terms of the
displayName chain are read raw — none routed through getSafeIdentityName, the LID-leak guard that the sibling profile path (getContactDisplayName) applies to the very same fields. The demonstrated leak is the sender?.sender_name term: migration 034 backfills messages.sender_name with the bare LID local-part digits for unmapped LID senders, and the panel chain short-circuits on that value, never reaching getLidDisplayName.
- Actual vs. expected: For an
@lid member whose newest group message was backfilled by migration 034, the panel shows the bare LID digits. The commit's own per-row invariant formatPhoneLikeText(displayName) === formatPhoneLikeText(jid) is violated. The same member's opened contact profile produces WhatsApp user (ID …XXXX).
- Impact: A privacy regression that leaks the opaque LID token the commit was written to hide. The leak window is bounded: migration
034 backfills sender_name with split_part(sender_jid, '@', 1) for unmapped LID senders, but the live message writer's terminal fallback is extractPhoneFromJid(lidJid) which returns null for @lid (message-handlers.ts:270). So the leak heals the moment any post-migration live message arrives from that member.
Code with Bug
// apps/api/src/services/group.service.ts — getEnrichedGroupParticipants (line 386)
const displayName =
contact?.custom_name ||
contact?.push_name ||
storedNameByJid.get(jid) ||
sender?.sender_name || // <-- BUG 🔴 raw backfilled LID digits short-circuit before LID mask
(phoneNumber ? `+${phoneNumber}` : null) ||
getLidDisplayName(jid) || // intended mask — unreachable when sender?.sender_name is truthy
"Unknown participant";
Explanation
- For
@lid/@hosted.lid JIDs, phoneNumber is null, so the chain relies on earlier fallbacks.
- Migration
034 can persist messages.sender_name as the LID local-part digits for unmapped LID senders; when that row is the most recent message for the participant, sender?.sender_name becomes truthy and getLidDisplayName(jid) is never reached.
- This diverges from the profile rendering path (
getContactDisplayName), which applies getSafeIdentityName(value, contact.jid) to reject “names” that simply repeat an LID identity.
Codebase Inconsistency
- Profile path (
packages/shared/src/contact.ts) guards these same identity-name fields via getSafeIdentityName, but the group panel path does not.
- Another guard exists (
resolveMemberPushName in apps/api/src/services/group-participant-contacts.ts) that explicitly rejects candidates repeating the member’s opaque identity; the group panel displayName chain applies neither.
Recommended Fix
- Export
getSafeIdentityName from @wateaminbox/shared (or add an equivalent exported helper) and wrap the panel’s identity-name candidates (custom_name, push_name, storedNameByJid, sender?.sender_name) so LID-identity restatements fall through to getLidDisplayName(jid).
- Consider a follow-up cleanup migration to re-backfill
messages.sender_name for @lid/@hosted.lid senders with a digit-repetition filter, so the opaque digits are not persisted.
History
This bug was introduced in commit 3403183. The change ("fix(groups): mask unnamed LID participants in group info panel") set out to hide opaque LID tokens from the group info panel by adding getLidDisplayName(jid) as the terminal fallback of the displayName chain, replacing the previously-leaky jid.split("@")[0]. The bug slipped in because the fix only replaced the terminal fallback — it did not route the earlier identity-name terms (sender?.sender_name, and custom_name/push_name/storedNameByJid) through getSafeIdentityName, the LID-leak guard that the sibling profile path (getContactDisplayName) already applied to those same fields. The accompanying integration test only constructed LID participants with no messages rows, so sender?.sender_name was never exercised and the gap went undetected.
Detail Bug Report
https://app.detail.dev/org_ee14aa77-b24a-40b2-b22d-66bd31931f4a/bugs/bug_996785d0-a4d7-4308-b731-1757ff807073
Introduced in #359 by @setkyar on Sep 10, 2026
Summary
getEnrichedGroupParticipantsinapps/api/src/services/group.service.tsbuilds the per-memberdisplayNamerendered on the group info panel. Commit3403183("fix(groups): mask unnamed LID participants in group info panel") addedgetLidDisplayName(jid)so that@lid/@hosted.lidmembers resolve to the privacy-safe labelWhatsApp user (ID …XXXX)instead of their opaque identity digits.displayNamechain are read raw — none routed throughgetSafeIdentityName, the LID-leak guard that the sibling profile path (getContactDisplayName) applies to the very same fields. The demonstrated leak is thesender?.sender_nameterm: migration034backfillsmessages.sender_namewith the bare LID local-part digits for unmapped LID senders, and the panel chain short-circuits on that value, never reachinggetLidDisplayName.@lidmember whose newest group message was backfilled by migration034, the panel shows the bare LID digits. The commit's own per-row invariantformatPhoneLikeText(displayName) === formatPhoneLikeText(jid)is violated. The same member's opened contact profile producesWhatsApp user (ID …XXXX).034backfillssender_namewithsplit_part(sender_jid, '@', 1)for unmapped LID senders, but the live message writer's terminal fallback isextractPhoneFromJid(lidJid)which returnsnullfor@lid(message-handlers.ts:270). So the leak heals the moment any post-migration live message arrives from that member.Code with Bug
Explanation
@lid/@hosted.lidJIDs,phoneNumberisnull, so the chain relies on earlier fallbacks.034can persistmessages.sender_nameas the LID local-part digits for unmapped LID senders; when that row is the most recent message for the participant,sender?.sender_namebecomes truthy andgetLidDisplayName(jid)is never reached.getContactDisplayName), which appliesgetSafeIdentityName(value, contact.jid)to reject “names” that simply repeat an LID identity.Codebase Inconsistency
packages/shared/src/contact.ts) guards these same identity-name fields viagetSafeIdentityName, but the group panel path does not.resolveMemberPushNameinapps/api/src/services/group-participant-contacts.ts) that explicitly rejects candidates repeating the member’s opaque identity; the group panel displayName chain applies neither.Recommended Fix
getSafeIdentityNamefrom@wateaminbox/shared(or add an equivalent exported helper) and wrap the panel’s identity-name candidates (custom_name,push_name,storedNameByJid,sender?.sender_name) so LID-identity restatements fall through togetLidDisplayName(jid).messages.sender_namefor@lid/@hosted.lidsenders with a digit-repetition filter, so the opaque digits are not persisted.History
This bug was introduced in commit 3403183. The change ("fix(groups): mask unnamed LID participants in group info panel") set out to hide opaque LID tokens from the group info panel by adding
getLidDisplayName(jid)as the terminal fallback of thedisplayNamechain, replacing the previously-leakyjid.split("@")[0]. The bug slipped in because the fix only replaced the terminal fallback — it did not route the earlier identity-name terms (sender?.sender_name, andcustom_name/push_name/storedNameByJid) throughgetSafeIdentityName, the LID-leak guard that the sibling profile path (getContactDisplayName) already applied to those same fields. The accompanying integration test only constructed LID participants with nomessagesrows, sosender?.sender_namewas never exercised and the gap went undetected.