Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/message/primitives/__tests__/incoming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,58 @@ test('1:1 message from my lid identity is detected as fromMe', async () => {
assert.equal(key.remoteJid, '144400000000000@lid')
})

test('1:1 message from my hosted device is detected as fromMe', async () => {
const emitted: WaIncomingMessageEvent[] = []
await handleIncomingMessageAck(
{
tag: 'message',
attrs: {
id: 'msg-self-hosted',
from: '133300000000000:99@hosted.lid',
recipient: '144400000000000@lid',
sender_pn: '5511999999999@s.whatsapp.net',
t: '123'
},
content: [{ tag: 'enc', attrs: { type: 'msg' }, content: new Uint8Array([1]) }]
},
createDecryptingOptions(emitted, {
getMeJid: () => '5511999999999@s.whatsapp.net',
getMeLid: () => '133300000000000@lid'
})
)

assert.equal(emitted.length, 1)
const { key } = emitted[0]
assert.equal(key.fromMe, true)
assert.equal(key.remoteJid, '144400000000000@lid')
assert.equal(key.remoteJidAlt, undefined)
})

test('self-sent 1:1 message with an unresolved chat keeps the own number out of remoteJidAlt', async () => {
const emitted: WaIncomingMessageEvent[] = []
await handleIncomingMessageAck(
{
tag: 'message',
attrs: {
id: 'msg-self-no-chat',
from: '133300000000000:99@hosted.lid',
sender_pn: '5511999999999@s.whatsapp.net',
t: '123'
},
content: [{ tag: 'enc', attrs: { type: 'msg' }, content: new Uint8Array([1]) }]
},
createDecryptingOptions(emitted, {
getMeJid: () => '5511999999999@s.whatsapp.net',
getMeLid: () => '133300000000000@lid'
})
)

assert.equal(emitted.length, 1)
const { key } = emitted[0]
assert.equal(key.fromMe, true)
assert.equal(key.remoteJidAlt, undefined)
})

test('1:1 incoming message from a peer stays fromMe false with the peer as remoteJid', async () => {
const emitted: WaIncomingMessageEvent[] = []
await handleIncomingMessageAck(
Expand Down
15 changes: 9 additions & 6 deletions src/message/primitives/incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ type MessageKeyIdentity = Omit<MessageIdentityAttrs, 'pushName'>
/**
* Self-authored 1:1 chat is the recipient, so its alternate addressing is the
* `recipient*` attrs (the `sender*` attrs describe me). Promotes `recipientAlt`
* to `remoteJidAlt` and drops the stale sender/recipient fields.
* to `remoteJidAlt` and drops the stale sender/recipient fields. Applied
* whenever the stanza is self-authored 1:1, even when the chat itself did not
* resolve: the sender attrs always describe the own account there, so letting
* them through would address the message to the connection's own number.
*/
function promoteRecipientAddressing(identity: MessageKeyIdentity): MessageKeyIdentity {
return {
Expand Down Expand Up @@ -116,13 +119,13 @@ function buildIncomingMessageKey(
const fromMe = sender
? isOwnAccountJid(sender.userJid, options.getMeJid?.(), options.getMeLid?.())
: false
const selfSentChat =
fromMe && !isGroup && !isBroadcast
? (node.attrs.recipient ?? destinationJid ?? undefined)
: undefined
const isSelfSentDirect = fromMe && !isGroup && !isBroadcast
const selfSentChat = isSelfSentDirect
? (node.attrs.recipient ?? destinationJid ?? undefined)
: undefined
const chatJid = selfSentChat ? toUserJid(selfSentChat) : fromUserJid
const { pushName, ...identity } = extractMessageIdentityAttrs(node.attrs)
const keyIdentity = selfSentChat ? promoteRecipientAddressing(identity) : identity
const keyIdentity = isSelfSentDirect ? promoteRecipientAddressing(identity) : identity
return {
pushName,
key: {
Expand Down
14 changes: 14 additions & 0 deletions src/protocol/__tests__/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ test('jid type detection and device handling', () => {
assert.equal(isOwnAccountJid('5599@s.whatsapp.net', '5511@s.whatsapp.net', '1330@lid'), false)
assert.equal(isOwnAccountJid('5511@s.whatsapp.net', null, null), false)

// A hosted device of the account is the account (wa-web isSameAccountAndAddressingMode).
assert.equal(isOwnAccountJid('1330:99@hosted.lid', '5511@s.whatsapp.net', '1330@lid'), true)
assert.equal(isOwnAccountJid('5511:99@hosted', '5511@s.whatsapp.net', '1330@lid'), true)
assert.equal(isOwnAccountJid('1330:99@hosted.lid', '5511@s.whatsapp.net', null), false)
assert.equal(isOwnAccountJid('9999:99@hosted.lid', '5511@s.whatsapp.net', '1330@lid'), false)
assert.equal(isOwnAccountJid('1330@lid', '5511:99@hosted', '1330:99@hosted.lid'), true)

assert.equal(normalizeDeviceJid('5511:0@s.whatsapp.net'), '5511@s.whatsapp.net')
assert.equal(normalizeDeviceJid('5511:5@s.whatsapp.net'), '5511:5@s.whatsapp.net')

Expand Down Expand Up @@ -187,6 +194,13 @@ test('jid type detection and device handling', () => {
assert.equal(isHostedDeviceJid('5511:99@hosted.lid'), true)
assert.equal(isHostedDeviceJid('5511:99@lid'), true)
assert.equal(isHostedDeviceJid('5511:1@lid'), false)
// Detected by server alone, and malformed shapes still rejected.
assert.equal(isHostedDeviceJid('5511@hosted'), true)
assert.equal(isHostedDeviceJid('5511@hosted.lid'), true)
assert.equal(isHostedDeviceJid('a@b@hosted'), false)
assert.equal(isHostedDeviceJid('@hosted'), false)
assert.equal(isHostedDeviceJid('5511@hostedd'), false)
assert.equal(isHostedDeviceJid('5511@hosted.li'), false)

assert.equal(
buildDeviceJid('6116570308623', 'lid', 99, {
Expand Down
55 changes: 38 additions & 17 deletions src/protocol/jid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ export function canonicalizeSignalJid(
return `${address.user}:${address.device}@${server}`
}

/**
* 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
}

Comment on lines +214 to +230

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.ts

Repository: 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 500

Repository: 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:


🌐 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:


🌐 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:


🏁 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.ts

Repository: 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
done

Repository: 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)}')
PY

Repository: 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

/**
* Strips the `:device` segment from a JID, returning the bare `user@server`
* form. Set `options.canonicalizeSignalServer` to also rewrite hosted servers
Expand All @@ -224,13 +241,17 @@ export function toUserJid(
} = {}
): string {
const canonicalize = options.canonicalizeSignalServer === true
if (!canonicalize) {
const atIndex = jid.indexOf('@')
if (atIndex >= 1 && atIndex < jid.length - 1) {
const colonIndex = jid.indexOf(':', 0)
if (colonIndex === -1 || colonIndex > atIndex) {
return jid
}
const atIndex = jid.indexOf('@')
if (atIndex >= 1 && atIndex < jid.length - 1) {
const colonIndex = jid.indexOf(':', 0)
// Canonicalization only rewrites the hosted servers, so a deviceless JID
// on any other server already is its own target form and can skip the
// parse (which would slice the user out and allocate an address).
if (
(colonIndex === -1 || colonIndex > atIndex) &&
(!canonicalize || !isHostedServerAt(jid, atIndex + 1))
) {
return jid
}
}
const address = parseSignalAddressFromJid(jid)
Expand All @@ -244,20 +265,25 @@ export function toUserJid(
return `${address.user}@${server}`
}

const CANONICAL_USER_JID_OPTIONS = Object.freeze({ canonicalizeSignalServer: true } as const)

/**
* True when `jid` is the account's own user, matching the `meJid` (pn) or
* `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
* `isMeAccount`.
* `isMeAccount`, including its addressing-mode equivalence: a hosted device of
* the account (`<user>@hosted` / `<user>@hosted.lid`) is the same account as
* `<user>@s.whatsapp.net` / `<user>@lid`, so both sides are canonicalized
* before comparison.
*/
export function isOwnAccountJid(
jid: string,
meJid: string | null | undefined,
meLid: string | null | undefined
): boolean {
const candidateUser = toUserJid(jid)
const candidateUser = toUserJid(jid, CANONICAL_USER_JID_OPTIONS)
return (
(!!meJid && toUserJid(meJid) === candidateUser) ||
(!!meLid && toUserJid(meLid) === candidateUser)
(!!meJid && toUserJid(meJid, CANONICAL_USER_JID_OPTIONS) === candidateUser) ||
(!!meLid && toUserJid(meLid, CANONICAL_USER_JID_OPTIONS) === candidateUser)
)
}

Expand Down Expand Up @@ -328,14 +354,9 @@ export function isHostedServer(server: string): boolean {
* (`@hosted` / `@hosted.lid`) or by a `:HOSTED_DEVICE_ID@…` device segment.
*/
export function isHostedDeviceJid(jid: string): boolean {
if (
isJidType(jid, WA_DEFAULTS.HOSTED_SERVER) ||
isJidType(jid, WA_DEFAULTS.HOSTED_LID_SERVER)
) {
return true
}
const atIndex = jid.indexOf('@')
if (atIndex < 1 || atIndex >= jid.length - 1) return false
if (isHostedServerAt(jid, atIndex + 1)) return true
const colonIndex = jid.indexOf(':')
if (colonIndex < 0 || colonIndex >= atIndex - 1) return false
let deviceId = 0
Expand Down
Loading