Skip to content

fix(lid_pn): WA Web compliant signal address for Hosted JIDs - #605

Merged
jlucaso1 merged 4 commits into
mainfrom
fix/hosted-signal-address-compliance
Apr 28, 2026
Merged

fix(lid_pn): WA Web compliant signal address for Hosted JIDs#605
jlucaso1 merged 4 commits into
mainfrom
fix/hosted-signal-address-compliance

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Background

Audit of the Hosted/HostedLid path triggered by a question on the encoder `domain_type` bug (#391) surfaced a deeper Signal-address divergence. WA Web's `SignalAddress.toString()` in `WAWeb/Signal/Address.js`:

```js
if (this.wid.isHosted()) {
if (bizHostedDevicesEnabled()) {
if (t !== ":99") throw "Hosted jid with wrong device id";
var n = asUserWidOrThrow(this.wid),
a = !n.isLid() && !n.isHostedLid() && n.isUser(),
i = a ? getCurrentLid(n) : n;
return i == null
? [this.wid.user, t, "@HosteD"].join("")
: [i.user, t, "@hosted.lid"].join("");
}
throw "Unexpected hosted jid";
}
```

Same pattern for non-hosted JIDs lower in the function: prefer LID-keyed addresses, fall back to the original namespace if no LID mapping is known.

What was wrong

`resolve_encryption_jid` only handled the PN → LID upgrade. Hosted JIDs fell through the catch-all `_ => target.clone()`, so a hosted device whose LID was known was keyed locally at `{user}@hosted` while WA Web would key it at `{lid_user}@hosted.lid`.

Practical impact today: nil — current send paths filter `is_hosted()` before reaching the resolver, so the divergent key was never built or looked up. The gap was dormant but real, and any future hosted support would have inherited an incompatible keyspace.

Fix

`resolve_encryption_jid` mirrors WA Web in a single match:

```rust
let lid_server = match target.server {
Server::Pn => Server::Lid,
Server::Hosted => Server::HostedLid,
_ => return target.clone(),
};
match self.lid_pn_cache.get_current_lid(&target.user).await {
Some(lid_user) => Jid { user: lid_user.into(), server: lid_server, .. },
None => target.clone(),
}
```

Input Output Source
Lid / HostedLid unchanged already canonical
Pn Lid when mapping known, else Pn matches existing behavior
Hosted HostedLid when mapping known, else Hosted new
anything else unchanged matches existing behavior

The Hosted upgrade reuses the same `lid_pn_cache.get_current_lid()` lookup as the PN upgrade. PN/LID dispatch cost is unchanged.

Allocation profile

Branch Heap allocs
Lid / HostedLid / other 0 (CompactString user inline ≤24 bytes — always for phone/lid lengths)
Pn / Hosted with mapping 1 wasted (String temp from `lid_pn_cache`, dropped after `.into()` to inline CompactString)
Pn / Hosted no mapping 0 (cache miss + inline clone)

The wasted `String` is a property of `lid_pn_cache.get_current_lid() -> Option`. Eliminating it requires changing `LidPnEntry.lid: String → CompactString` cross-crate; out of scope here, dormant in hot paths today (LID groups don't go through the upgrade — devices already are LID).

Tests

  • `test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid` — loops over `[99, 7]` device ids to prove the input device round-trips and is not coerced to 99.
  • `test_resolve_encryption_jid_hosted_no_mapping_keeps_hosted`
  • `test_resolve_encryption_jid_preserves_hosted_lid`

Plus a wire-encoding regression in `wacore/binary/src/encoder.rs`: direct-constructed Hosted JIDs (`Jid::new(_, Server::Hosted)` defaults `agent=0`) now emit the correct `domain_type` byte (128 / 129) because the encoder derives it from the server enum, not from `agent`. Pre-#391 the same input would have written `0`.

What's not in this PR

  • Hosted send-path activation. Group SKDM still filters hosted (matches WA Web's `!t.includes("hosted")` filter in `getGroupSenderKeyListFromParticipantRecord`). DM hosted filters remain in place. Activating hosted as an active recipient is a feature, not part of address compliance.
  • Device=99 enforcement at the JID boundary. WA Web throws on hosted JIDs with other device ids; we preserve the input and let the wire layer route it. Soft-reject is a separate hardening.
  • Hosted ↔ HostedLid swap in `swap_pn_lid_namespace`. That helper handles cross-alias message lookup which is dormant for hosted (no stored messages keyed under hosted today).
  • `LidPnEntry.lid` switch from `String` to `CompactString` to drop the wasted String alloc on the upgrade path.

Test plan

  • `cargo test --workspace --lib` — 1370+ tests pass
  • `cargo clippy --all --tests` — clean
  • `cargo fmt --all`

`resolve_encryption_jid` previously fell through Hosted JIDs untouched,
leaving Signal session keys at `{user}@hosted` while WA Web's
`SignalAddress.toString()` (`WAWeb/Signal/Address.js`) keys them at
`{lid_user}@hosted.lid` once a LID mapping is known. Same shape as the
existing PN → LID upgrade — a single `lid_pn_cache` lookup mirrored
across Hosted → HostedLid.

Branches:
  Lid / HostedLid → already canonical, returned as-is.
  Pn      → upgrade to Lid when mapping known.
  Hosted  → upgrade to HostedLid when mapping known.

Hot path is unchanged: PN/LID dispatch costs the same as before; the
new Hosted branch is dormant for current send paths because they
filter `is_hosted` out before reaching the resolver, but the logic is
in place for any future hosted support and matches WA Web's intent.

Also pins down the post-#391 wire encoding for direct-constructed
Hosted/HostedLid JIDs in a regression test — `Jid::new(_, Hosted)`
defaults `agent=0`, but `server_to_domain_type` derives the correct
byte (128/129) from the server enum regardless.
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d76266c7-9a1e-4ac8-8686-4b7fbea3345b

📥 Commits

Reviewing files that changed from the base of the PR and between 1cbe5ad and 1ce12a0.

📒 Files selected for processing (1)
  • src/client/lid_pn.rs

📝 Walkthrough

Summary by CodeRabbit

  • Tests

    • Added test coverage for binary encoding validation of device identifiers.
  • Refactor

    • Updated internal JID resolution logic to improve server namespace handling and preserve identifier integrity.

Walkthrough

I changed resolve_encryption_jid to derive the upgraded server namespace from the incoming JID’s server, consult the lid_pn cache by user, and reconstruct a LID-style Jid on cache hit (preserving device/agent/integrator); no-op on cache miss. Added encoder unit test for Hosted/HostedLid AD_JID domain-type encoding.

Changes

Cohort / File(s) Summary
JID Canonicalization Logic
src/client/lid_pn.rs
Refactored resolve_encryption_jid to compute upgraded server from target.server (Pn→Lid, Hosted→HostedLid), look up lid_pn_cache by target.user, and return a rebuilt Jid with mapped LID user while preserving device/agent/integrator. Removed PN-specific branches and related debug logging. Tests added/updated for Hosted→HostedLid upgrade, cache-miss noop, and HostedLid passthrough.
Binary Encoder Test
wacore/binary/src/encoder.rs
Added unit test verifying direct-constructed Hosted/HostedLid device JIDs default agent = 0 and that encoding emits the server-derived AD_JID domain-type byte (128 for Hosted, 129 for HostedLid) immediately after AD_JID token.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: making WA Web compliant signal address handling for Hosted JIDs by implementing the Hosted→HostedLid upgrade logic.
Description check ✅ Passed The description comprehensively explains the background, root cause, fix implementation, tests added, and allocation profile—directly aligned with the changeset across both files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hosted-signal-address-compliance

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/lid_pn.rs`:
- Around line 307-308: Update the documentation comment that currently reads
"misformed JID" in the hosted-device note to use the correct word "malformed"
instead; locate the doc comment containing the phrase "hosted; we preserve
whatever device the input carries so a misformed JID is still routable rather
than silently dropped." and replace "misformed" with "malformed" to fix the
typo.
- Around line 566-583: The test
test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid currently
only verifies device=99 which could mask regressions; add an additional
assertion case (or duplicate the test logic) using a different hosted.device
value (e.g., device=7) when creating Jid::new(user, Server::Hosted) and call
client.resolve_encryption_jid(&hosted).await, then assert resolved.user == lid,
resolved.server == Server::HostedLid, and resolved.device == 7 to ensure the
input device is preserved and not hardcoded to 99.
🪄 Autofix (Beta)

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 65c1c303-1116-4815-979b-4db688e1b626

📥 Commits

Reviewing files that changed from the base of the PR and between 4205ca8 and f0a0ee4.

📒 Files selected for processing (2)
  • src/client/lid_pn.rs
  • wacore/binary/src/encoder.rs

Comment thread src/client/lid_pn.rs Outdated
Comment thread src/client/lid_pn.rs
Single match maps Pn → Lid and Hosted → HostedLid; cache lookup runs
once with target.clone() as the no-mapping fallback. Drops the
upgrade_to_lid_namespace helper (one caller) and trims the doc to the
WA Web reference. Same allocation profile (one Jid clone on the
fallback path, one CompactString conversion on upgrade — both
unavoidable given the Option<String> return from lid_pn_cache).
Loop over [99, 7] in test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid
to prove the input device is preserved, not coerced to 99. Same
fixture, same upgrade path; the asymmetric value catches a regression
that hardcodes :99 (which would still pass on the original :99 case).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0a0ee4d15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/lid_pn.rs Outdated
Comment thread wacore/binary/src/encoder.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/client/lid_pn.rs (1)

538-556: ⚠️ Potential issue | 🟡 Minor

Add one non-99 Hosted-device upgrade case to harden this contract.

Right now Line 550 only validates device = 99. Add one more case (for example device = 7) so future regressions that pin device values can’t slip through.

Suggested test addition
 #[tokio::test]
 async fn test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid() {
@@
     let mut hosted = Jid::new(user, Server::Hosted);
     hosted.device = 99;
     let resolved = client.resolve_encryption_jid(&hosted).await;
@@
     assert_eq!(resolved.server, Server::HostedLid);
     assert_eq!(resolved.device, 99);
+
+    let mut hosted_non_default = Jid::new(user, Server::Hosted);
+    hosted_non_default.device = 7;
+    let resolved_non_default = client.resolve_encryption_jid(&hosted_non_default).await;
+    assert_eq!(resolved_non_default.user, lid);
+    assert_eq!(resolved_non_default.server, Server::HostedLid);
+    assert_eq!(resolved_non_default.device, 7);
 }

Based on learnings, WA Web address derivation is device-scoped (:device is preserved when device != 0), so non-default device preservation should be explicitly locked by tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/lid_pn.rs` around lines 538 - 556, Add a second non-99 device case
to the test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid test:
after creating the client, mapping lid->user and asserting device 99 upgrades to
Server::HostedLid, also create another Jid::new(user, Server::Hosted) with
device = 7 (or another non-zero/non-99 value), call
client.resolve_encryption_jid(&hosted) and assert resolved.user == lid,
resolved.server == Server::HostedLid and resolved.device == 7; use the same
helpers (create_test_client, add_lid_pn_mapping, resolve_encryption_jid) so the
contract that non-default devices are preserved is explicitly verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/client/lid_pn.rs`:
- Around line 538-556: Add a second non-99 device case to the
test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid test: after
creating the client, mapping lid->user and asserting device 99 upgrades to
Server::HostedLid, also create another Jid::new(user, Server::Hosted) with
device = 7 (or another non-zero/non-99 value), call
client.resolve_encryption_jid(&hosted) and assert resolved.user == lid,
resolved.server == Server::HostedLid and resolved.device == 7; use the same
helpers (create_test_client, add_lid_pn_mapping, resolve_encryption_jid) so the
contract that non-default devices are preserved is explicitly verified.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f27890b6-6a10-4c05-b1ac-a7f4a64bccd3

📥 Commits

Reviewing files that changed from the base of the PR and between f0a0ee4 and c5917ed.

📒 Files selected for processing (2)
  • src/client/lid_pn.rs
  • wacore/binary/src/encoder.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/lid_pn.rs`:
- Around line 549-559: The test loop constructing Jid and calling
client.resolve_encryption_jid currently only asserts device round-trips; extend
the assertions to also verify that the agent and integrator fields are
preserved. After obtaining resolved from client.resolve_encryption_jid(&hosted),
add assertions that resolved.agent == hosted.agent and resolved.integrator ==
hosted.integrator (or the expected preserved values) alongside the existing
user/server/device asserts so the full Jid contract (user, server, device,
agent, integrator) is locked in the test.
🪄 Autofix (Beta)

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56be7e94-454e-439d-8ddc-9401b7ad9cca

📥 Commits

Reviewing files that changed from the base of the PR and between c5917ed and 1cbe5ad.

📒 Files selected for processing (1)
  • src/client/lid_pn.rs

Comment thread src/client/lid_pn.rs
Set non-default agent (0xAB) and integrator (0xBEEF) on the input and
assert both round-trip alongside the existing user/server/device
checks. Closes the gap where a regression that drops or rewrites these
fields would slip through with all-zero defaults.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant