fix(protocol): match the own account's hosted devices in isOwnAccountJid - #241
Conversation
WhatsApp delivers hosted sessions addressed as @HosteD / @hosted.lid instead of @s.whatsapp.net / @lid. isOwnAccountJid compared the preserved server, so <user>@hosted.lid never matched a meLid of <user>@lid: same account, different server. Every message the account authored from such a device came back with fromMe false. That cascades. selfSentChat is gated on fromMe, so the chat stayed unresolved and remoteJid fell back to the `from` attr, which is the account itself, while remoteJidAlt took sender_pn, which is the account's own number. Consumers then resolved the contact to the connection's own number and collapsed traffic addressed to many different peers into a single thread, stored as received. Both sides are now canonicalized before comparison, mirroring the web bundle: isMeAccount defers to isSameAccountAndAddressingMode, which maps hosted to c.us and hosted.lid to lid and then compares the user alone, ignoring the device. Note this only helps when meLid is populated - with an empty meLid there is nothing for a canonicalized LID to match against. buildIncomingMessageKey also stops gating the recipient promotion on the chat resolving. When a 1:1 stanza is self-authored the sender attrs describe the own account whether or not the chat resolved, so they must never reach remoteJidAlt. The msmsg and unavailable call sites pass no destinationJid, so that path is reachable. toUserJid keeps its zero-allocation fast path under canonicalization when the server is not hosted, since canonicalization rewrites nothing else - without it every identity comparison paid a parse and a slice. isHostedDeviceJid now shares that same in-place check instead of a second tail-anchored scan, which leaves the hosted-server detection in one place.
📝 WalkthroughWalkthroughHosted-device JID handling now detects and canonicalizes hosted variants for account matching. Self-sent direct messages now promote recipient addressing even when chat resolution fails. Tests cover hosted-device identity matching, detection, and incoming message fields. ChangesHosted-device identity handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/protocol/jid.ts`:
- Around line 214-230: Update the hosted-device JID validation around
isHostedServerAt so the server segment contains no additional @ characters after
the separator. Perform this check before returning true from the device branch,
ensuring malformed values such as hosted@extra are rejected while valid hosted
device JIDs retain their current behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c8ae6fc8-fcad-4ee7-b7e8-9093774c4359
📒 Files selected for processing (4)
src/message/primitives/__tests__/incoming.test.tssrc/message/primitives/incoming.tssrc/protocol/__tests__/protocol.test.tssrc/protocol/jid.ts
| /** | ||
| * Returns `true` when the server segment of `jid` starting at `from` is one of | ||
| * the hosted variants. Compares in place, so the common miss costs one length | ||
| * check and allocates nothing. Callers that already located the `@` pass its | ||
| * index rather than paying {@link isJidType}'s tail-anchored rescan. | ||
| */ | ||
| function isHostedServerAt(jid: string, from: number): boolean { | ||
| const length = jid.length - from | ||
| if (length === WA_DEFAULTS.HOSTED_SERVER.length) { | ||
| return jid.startsWith(WA_DEFAULTS.HOSTED_SERVER, from) | ||
| } | ||
| if (length === WA_DEFAULTS.HOSTED_LID_SERVER.length) { | ||
| return jid.startsWith(WA_DEFAULTS.HOSTED_LID_SERVER, from) | ||
| } | ||
| return false | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -t d '^(wa-web|wa-mob)$' . | while IFS= read -r directory; do
printf '\n== %s ==\n' "$directory"
rg -n -C 4 \
'isSameAccountAndAddressingMode|isMeAccount|hosted\.lid|`@hosted`|isHostedDevice' \
"$directory" || true
done
printf '\n== Current implementation and coverage ==\n'
rg -n -C 5 \
'isHostedServerAt|isOwnAccountJid|isHostedDeviceJid|canonicalizeSignalServer' \
src/protocol/jid.ts src/protocol/__tests__/protocol.test.tsRepository: vinikjkkj/zapo
Length of output: 15299
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== repository protocol files =='
git ls-files | rg '(^|/)(wa-web|wa-mob)(/|$)|src/protocol/(jid|constants|index)\.ts$|package\.json$' || true
printf '%s\n' '== JID definitions and constants =='
rg -n -C 8 \
'HOSTED_SERVER|HOSTED_LID_SERVER|HOST_DOMAIN|LID_SERVER|function isHostedServer|function isHostedDeviceId|function parseSignalAddressFromJid|function isOwnAccountJid|function isMeAccount' \
src/protocol src/message
printf '%s\n' '== repository references to hosted JIDs =='
rg -n -S -C 3 '`@hosted`|hosted\.lid|isOwnAccountJid|isHostedDeviceJid|canonicalizeSignalJid' . \
-g '!node_modules' -g '!dist' -g '!build' | head -n 500Repository: vinikjkkj/zapo
Length of output: 50370
🌐 Web query:
WhatsApp Web isSameAccountAndAddressingMode isMeAccount hosted.lid hosted JID
💡 Result:
In the context of WhatsApp Web and third-party libraries like Baileys or WhatsMeow, the terms mentioned relate to identity, addressing, and infrastructure routing within WhatsApp's multi-device and business architecture [1][2][3]. Hosted JIDs and Infrastructure WhatsApp uses specific JID (Jabber ID) suffixes to distinguish account types and their routing infrastructure [3][4]. - hosted: Used for phone-number-based (PN) business accounts hosted by Meta [3][5]. - hosted.lid: Used for accounts that have migrated to or natively use LID (Local Identifier) addressing while being hosted by Meta [3][5]. isMeAccount and isHostedMeAccount These are internal flags or utility functions used by WhatsApp Web and libraries to determine if a specific user JID belongs to the currently authenticated session or is a business account requiring special handling [1][2][6]. - isMeAccount (often implemented via helper functions like isMe or similar checks) verifies if a given JID matches the local user's JID [1]. - setIsHostedMeAccount is specifically invoked during the pairing process for business accounts hosted on Meta infrastructure, ensuring correct cryptographic signatures (like ADV signatures) are generated [2]. isSameAccountAndAddressingMode This concept relates to the normalization of JIDs across different addressing modes (PN vs. LID) [7]. Because a single WhatsApp user may be reachable via both a PN JID (e.g., 15551234567@s.whatsapp.net) and a LID JID (e.g., 1234567890@lid), systems must determine if two different identifiers refer to the same logical account [7][3]. - Logic like areJidsSameUser is used to abstract away the underlying addressing mode, ensuring that cryptographic sessions are not duplicated or mismanaged when a user migrates between or uses mixed addressing [7][3]. - When handling hosted accounts, systems must map these correctly to their respective LID or PN equivalents to maintain session consistency, often using shared caches for LID/PN associations [8][7]. These components together ensure that WhatsApp's privacy-preserving LID system functions seamlessly alongside legacy PN addressing and modern business-hosted infrastructure [7][3].
Citations:
- 1: https://github.com/WhiskeySockets/Baileys/blob/731cd6b5/src/Utils/decode-wa-message.ts
- 2: fix: pairing hosted business accounts tulir/whatsmeow#814
- 3: https://baileys.wiki/concepts/jids
- 4: https://mintlify.wiki/whiskeysockets/Baileys/concepts/whatsapp-ids
- 5: https://docs.rs/whatsapp-rust/latest/whatsapp_rust/enum.Server.html
- 6: https://github.com/WhiskeySockets/Baileys/blob/78e7e4e2cfcb1935629173427bc3292b95f61a95/src/WABinary/jid-utils.ts
- 7: https://deepwiki.com/WhiskeySockets/Baileys/8.2-lid-mapping-system
- 8: fix(lid_pn): WA Web compliant signal address for Hosted JIDs oxidezap/whatsapp-rust#605
🌐 Web query:
site:github.com/WhiskeySockets/Baileys "isSameAccountAndAddressingMode"
💡 Result:
The term isSameAccountAndAddressingMode does not appear to be a standard or public API function in common programming libraries or the Baileys WhatsApp library context [1][2][3][4]. It is likely a hypothetical or custom-defined utility function, or a misremembered name for logic related to handling WhatsApp addressing modes (PN/LID) [3][5]. In the context of the Baileys library (often associated with these types of identifiers), logic exists to manage and compare addressing modes—specifically between PN (Phone Number) and LID (Lid/User ID) identifiers [6][3][5]. Developers frequently use utility functions like extractAddressingContext to determine whether a message is being addressed via LID or PN, and compare these states when processing incoming stanzas [3][7]. If you are encountering this in a specific codebase, it is likely a local helper function designed to: 1. Verify if two JIDs (Jabber IDs) refer to the same account [1][4]. 2. Confirm that the addressing modes (e.g., LID vs PN) match or are compatible between two entities [3][8]. For exact usage, please check your project's local utility files or the specific repository where you encountered the term, as it is not part of the standard Baileys public API [1][2][3].
Citations:
- 1: https://github.com/WhiskeySockets/Baileys/blob/731cd6b5/src/Socket/messages-recv.ts
- 2: https://github.com/WhiskeySockets/Baileys/blob/731cd6b5/src/Socket/messages-send.ts
- 3: https://github.com/WhiskeySockets/Baileys/blob/731cd6b5/src/Utils/decode-wa-message.ts
- 4: https://github.com/WhiskeySockets/Baileys/blob/78e7e4e2/src/Socket/messages-recv.ts
- 5: [BUG] Inconsistency between key.remoteJid and key.remoteJidAlt WhiskeySockets/Baileys#1810
- 6: fix: route pn sends to mapped lid WhiskeySockets/Baileys#2692
- 7: https://github.com/WhiskeySockets/Baileys/blob/78e7e4e2/src/Utils/decode-wa-message.ts
- 8: https://github.com/WhiskeySockets/Baileys/blob/731cd6b5/src/Socket/groups.ts
🌐 Web query:
site:github.com/tulir/whatsmeow hosted.lid hosted device 99 JID
💡 Result:
In the context of the WhatsApp infrastructure—specifically within libraries like whatsmeow—hosted.lid is a JID (Jabber ID) server domain used for "Hosted LID" (LID stands for Linked Identifier) accounts [1]. Key details regarding this JID format: * Hosted Accounts: These refer to business accounts that are hosted on WhatsApp infrastructure, often associated with Business-on-behalf-of (coexistence/coex) scenarios [2][3]. * Role of hosted.lid: While standard WhatsApp users typically use s.whatsapp.net and hidden/privacy-focused users use lid as their server domain, hosted.lid is used specifically for devices associated with hosted business accounts that utilize LID-based addressing [1]. * JID Structure: A JID associated with this domain typically follows the format [identifier]:[device_id]@hosted.lid`` [1]. The device_id (e.g., `99` in your query) represents the specific device index connected to that account [1]. * Context: This server domain is used by client libraries to correctly route traffic and manage cryptographic sessions (like Signal sessions) for hosted business entities, distinguishing them from standard user accounts [1][4][5]. It is primarily a technical distinction used in the WhatsApp protocol for managing identity and encryption in a hosted business environment [3][4].
Citations:
- 1: https://github.com/tulir/whatsmeow/blob/main/types/jid.go
- 2: pair: fix coex pairing + clean up naming + support hosted devices tulir/whatsmeow#946
- 3: fix: pairing hosted business accounts tulir/whatsmeow#814
- 4: https://github.com/tulir/whatsmeow/blob/main/pair.go
- 5: Regression: Hosted/coexistent send failures after session prefetch change (#955) tulir/whatsmeow#960
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== focused call sites =='
rg -n -C 8 \
'isOwnAccountJid|isHostedDeviceJid|canonicalizeSignalJid|canonicalizeOwnAccountJid' \
src/message/primitives/incoming.ts src/client/WaClientFactory.ts src/retry/replay.ts \
src/signal src/protocol/jid.ts
printf '%s\n' '== complete hosted-device implementation =='
sed -n '338,372p' src/protocol/jid.ts
printf '%s\n' '== complete account comparison implementation =='
sed -n '268,322p' src/protocol/jid.tsRepository: vinikjkkj/zapo
Length of output: 29175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for url in \
'https://raw.githubusercontent.com/tulir/whatsmeow/main/types/jid.go' \
'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/WABinary/jid-utils.ts' \
'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Utils/decode-wa-message.ts'
do
printf '\n== %s ==\n' "$url"
curl -L --fail --silent --show-error "$url" |
rg -n -C 8 'hosted|HOSTED|isMe|same.*account|device.*99|99.*device|addressing' || true
doneRepository: vinikjkkj/zapo
Length of output: 9674
🏁 Script executed:
#!/bin/bash
set -euo pipefail
url='https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/WABinary/jid-utils.ts'
curl -L --fail --silent --show-error "$url" | sed -n '90,180p'
printf '\n== focused local behavior probe ==\n'
python3 - <<'PY'
HOSTED = ('hosted', 'hosted.lid')
HOSTED_ID = 99
def is_hosted_device_jid(jid):
at = jid.find('@')
if at < 1 or at >= len(jid) - 1:
return False
server = jid[at + 1:]
if server in HOSTED:
return True
colon = jid.find(':')
if colon < 0 or colon >= at - 1:
return False
device = jid[colon + 1:at]
return device.isdigit() and int(device) <= 2**53 - 1 and int(device) == HOSTED_ID
for jid in (
'5511@hosted',
'5511@hosted.lid',
'5511:99@lid',
'5511:1@lid',
'a@b@hosted',
'5511:99@hosted@extra',
'`@hosted`',
'5511@hosted.li'
):
print(f'{jid}: {is_hosted_device_jid(jid)}')
PYRepository: vinikjkkj/zapo
Length of output: 2021
Reject JIDs with extra @ characters before checking the device ID.
isHostedDeviceJid('5511:99@hosted@extra') returns true because the device branch parses only up to the first @. This can mark malformed requesters as hosted and add sender_intent: hosted. Reject additional @ characters before returning true.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/protocol/jid.ts` around lines 214 - 230, Update the hosted-device JID
validation around isHostedServerAt so the server segment contains no additional
@ characters after the separator. Perform this check before returning true from
the device branch, ensuring malformed values such as hosted@extra are rejected
while valid hosted device JIDs retain their current behavior.
Source: Coding guidelines
WhatsApp delivers hosted sessions addressed as
@hosted/ @hosted.lid instead of @s.whatsapp.net /@lid. isOwnAccountJid compared the preserved server, so @hosted.lid never matched a meLid of@lid: same account, different server. Every message the account authored from such a device came back with fromMe false.That cascades. selfSentChat is gated on fromMe, so the chat stayed unresolved and remoteJid fell back to the
fromattr, which is the account itself, while remoteJidAlt took sender_pn, which is the account's own number. Consumers then resolved the contact to the connection's own number and collapsed traffic addressed to many different peers into a single thread, stored as received.Both sides are now canonicalized before comparison, mirroring the web bundle: isMeAccount defers to isSameAccountAndAddressingMode, which maps hosted to c.us and hosted.lid to lid and then compares the user alone, ignoring the device. Note this only helps when meLid is populated - with an empty meLid there is nothing for a canonicalized LID to match against.
buildIncomingMessageKey also stops gating the recipient promotion on the chat resolving. When a 1:1 stanza is self-authored the sender attrs describe the own account whether or not the chat resolved, so they must never reach remoteJidAlt. The msmsg and unavailable call sites pass no destinationJid, so that path is reachable.
toUserJid keeps its zero-allocation fast path under canonicalization when the server is not hosted, since canonicalization rewrites nothing else - without it every identity comparison paid a parse and a slice. isHostedDeviceJid now shares that same in-place check instead of a second tail-anchored scan, which leaves the hosted-server detection in one place.
Summary by CodeRabbit