Skip to content

fix(signal): canonicalize PN/LID session addressing - #209

Closed
jlucaso1 wants to merge 5 commits into
vinikjkkj:masterfrom
jlucaso1:agent/fix-signal-pn-lid-addressing
Closed

fix(signal): canonicalize PN/LID session addressing#209
jlucaso1 wants to merge 5 commits into
vinikjkkj:masterfrom
jlucaso1:agent/fix-signal-pn-lid-addressing

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Signal sessions were keyed directly by the address on the outer stanza. When WhatsApp alternated the same device between a phone-number JID and a LID JID, the two forms could create independent ratchets: a reply advanced the LID session while a later PN-addressed message loaded stale state and failed authentication.

Mirror WhatsApp Web's ordering and address semantics: maybeProcesMsgInfoForLid() runs before decryptE2EPayload(), while the Signal address layer resolves a PN through its current LID (including hosted → hosted.lid). Learn conservative sender, participant, and recipient mappings plus authoritative peer and latest-recipient mappings, then canonicalize Signal sessions, identities, sender keys, retry paths, and fanout through one shared resolver. The one-to-one mapping follows the configured session provider and is persisted by all official stores.

Add a real Signal regression covering a PN-addressed prekey message with sender LID metadata, a LID-addressed reply, a fresh resolver, and a later PN message without repeated metadata. Provider tests cover replacement, reverse lookup, session isolation, and Redis TTL behavior.

AI-assisted contribution: implementation and tests were developed with Codex; protocol behavior was verified against the deobfuscated WhatsApp Web bundle.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added session-scoped PN↔LID mapping persistence (lidPnMapping) across MongoDB, MySQL, PostgreSQL, Redis, and SQLite.
    • Introduced PN↔LID address resolution that learns from message metadata and is now used for encryption/decryption, identity synchronization, and group sender-key processing.
  • Bug Fixes
    • Stored-state clearing now clears the PN/LID canonical mappings (only after successful ratchet/session clearing).
    • Mapping updates are replaceable within a session to preserve a single canonical owner.
  • Documentation
    • Updated Quick Start and Redis storage notes to reflect lidPnMapping persistence and TTL behavior.

@github-actions github-actions Bot added the fix Bug fix label Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6bfaf80d-dff1-4084-9a0e-4fee04493ec2

📥 Commits

Reviewing files that changed from the base of the PR and between 223ee3d and addea56.

📒 Files selected for processing (2)
  • src/client/WaClientFactory.ts
  • src/protocol/jid.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/protocol/jid.ts
  • src/client/WaClientFactory.ts

📝 Walkthrough

Walkthrough

Changes

PN/LID mappings are added as a session-scoped store domain across memory and persistent backends. SignalAddressResolver canonicalizes aliases and is wired through Signal sessions, encryption, sender keys, retries, incoming acknowledgements, identity synchronization, client notifications, and state clearing.

PN/LID canonicalization

Layer / File(s) Summary
Mapping storage and provider wiring
src/store/*, packages/store-{mongo,mysql,postgres,redis,sqlite}/...
Adds mapping contracts, bounded memory storage, persistent migrations, atomic replacement, session isolation, locking, factories, cleanup, TTL behavior, and provider validation.
Address resolution and Signal flows
src/signal/session/*, src/signal/api/*, src/signal/group/*
Resolves PN aliases to canonical LID addresses, caches lookups, deduplicates aliases, and applies canonical addresses to sessions, identities, encryption, decryption, and sender-key operations.
Client and message integration
src/client/*, src/message/*, src/protocol/*
Learns mappings from incoming metadata and injects the resolver into client coordinators, retry handling, notifications, group fanout, ICDC resolution, and hosted identity checks.
Documentation and release metadata
README.md, packages/store-redis/README.md, .changeset/*
Documents persistent PN/LID mapping behavior, Redis retention semantics, and patch releases for the store packages.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: canonicalizing Signal session addressing across PN/LID aliases.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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.

❤️ Share

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

@jlucaso1
jlucaso1 marked this pull request as ready for review July 20, 2026 18:19

@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: 6

🧹 Nitpick comments (2)
packages/store-mongo/src/__tests__/integration.test.ts (1)

1051-1073: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same lidPnMapping replaceability/session-scoping assertions duplicated verbatim across three provider integration suites.

All three tests exercise identical fixture values and assertion sequence (initial-null, set/replace, reverse-lookup consistency, cross-session isolation, clear), differing only by which provider's store.stores.lidPnMapping(...) instance they exercise.

  • packages/store-mongo/src/__tests__/integration.test.ts#L1051-L1073: extract the shared assertion sequence into a reusable helper (e.g. accepting the two mapping-store instances) and call it here.
  • packages/store-mysql/src/__tests__/integration.test.ts#L1084-L1106: call the same shared helper instead of duplicating the assertion sequence.
  • packages/store-redis/src/__tests__/integration.test.ts#L1049-L1071: call the same shared helper instead of duplicating the assertion sequence.
🤖 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 `@packages/store-mongo/src/__tests__/integration.test.ts` around lines 1051 -
1073, Extract the duplicated PN/LID mapping assertion sequence from the
integration tests into one shared helper that accepts the two mapping-store
instances. Update
packages/store-mongo/src/__tests__/integration.test.ts#L1051-L1073 to define or
use the helper, and replace the duplicated sequence in
packages/store-mysql/src/__tests__/integration.test.ts#L1084-L1106 and
packages/store-redis/src/__tests__/integration.test.ts#L1049-L1071 with calls to
it, preserving each provider’s session-scoped lidPnMapping setup and skip
behavior.
src/client/coordinators/WaMessageDispatchCoordinator.ts (1)

1385-1399: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove redundant address resolution.

pendingTargetAddresses are already resolved canonical addresses. They are extracted from pendingTargets, which were populated from the fanoutAddresses that underwent resolution at line 1313. Re-resolving them here is redundant and wastes a DB/memory call in the fallback path.

♻️ Proposed refactor
-            const resolvedPendingAddresses = this.deps.signalAddressResolver
-                ? await this.deps.signalAddressResolver.resolveMany(pendingTargetAddresses)
-                : pendingTargetAddresses
             const hasPendingSessions =
-                await this.deps.sessionStore.hasSessions(resolvedPendingAddresses)
+                await this.deps.sessionStore.hasSessions(pendingTargetAddresses)
             const nextAvailableTargets: {
                 readonly jid: string
                 readonly address: SignalAddress
             }[] = []
             for (let index = 0; index < pendingTargets.length; index += 1) {
                 if (hasPendingSessions[index]) {
-                    nextAvailableTargets.push({
-                        jid: pendingTargets[index].jid,
-                        address: resolvedPendingAddresses[index]
-                    })
+                    nextAvailableTargets.push(pendingTargets[index])
                 }
             }
🤖 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/client/coordinators/WaMessageDispatchCoordinator.ts` around lines 1385 -
1399, Remove the conditional signalAddressResolver.resolveMany call in the
pending-target fallback and use pendingTargetAddresses directly as the resolved
address list passed to sessionStore.hasSessions and indexed when constructing
nextAvailableTargets. Keep the existing pendingTargets iteration and JID mapping
unchanged.
🤖 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/client/WaClientFactory.ts`:
- Around line 1286-1291: Update the REMOVE branch in the device notification
handling around SignalDeviceSyncApi and deleteUserDevices so it derives and
reuses the canonical user JID before deleting device-list entries. Ensure
deleteUserDevices receives the same normalized key used by syncDeviceList,
including for hosted-server JIDs, while preserving the existing signal-address
resolution flow.
- Around line 1202-1204: Update the self-identity handling around signal address
resolution to use the existing isOwnAccountJid(...) account check instead of
comparing normalized user JIDs. Ensure `@lid` values from parsed.fromJid are
recognized as the own account so the existing disconnect path executes, while
preserving behavior for other identities.

In `@src/message/crypto/icdc.ts`:
- Around line 64-66: Update the address comparison in the surrounding function
to safely access localAddress.server and localAddress.device, using an explicit
localAddress truthiness guard or optional chaining while preserving the existing
user, server, and device matching behavior.

In `@src/message/primitives/incoming.ts`:
- Around line 617-618: Guard the non-critical learnMessageLidPnMappings call in
handleIncomingMessageAck so persistence failures do not interrupt message
decryption or acknowledgement. Catch and normalize the error with toError if
needed, then log it at warn level and allow the existing flow to continue; do
not change successful mapping behavior.

In `@src/signal/api/SignalIdentitySyncApi.ts`:
- Around line 127-143: Deduplicate canonical records before batch upserts: in
src/signal/api/SignalIdentitySyncApi.ts lines 127-143, update the identities
construction around setRemoteIdentities to retain one entry per resolved
address.user + ':' + address.device key; in src/signal/group/SenderKeyManager.ts
lines 193-199, update the distributions construction around
upsertSenderKeyDistributions to retain one entry per resolved sender address
key.

In `@src/signal/session/SignalAddressResolver.ts`:
- Around line 75-112: Update learnJidPairInternal to perform an optimistic cache
pre-check before mappingGate.runExclusive: when the cached PN-to-LID mapping
already equals mapping.lidUser, return false immediately without acquiring the
exclusive gate. Keep the existing gated validation and store mutation logic for
cache misses or mismatches, preserving cache consistency and replacement
behavior.

---

Nitpick comments:
In `@packages/store-mongo/src/__tests__/integration.test.ts`:
- Around line 1051-1073: Extract the duplicated PN/LID mapping assertion
sequence from the integration tests into one shared helper that accepts the two
mapping-store instances. Update
packages/store-mongo/src/__tests__/integration.test.ts#L1051-L1073 to define or
use the helper, and replace the duplicated sequence in
packages/store-mysql/src/__tests__/integration.test.ts#L1084-L1106 and
packages/store-redis/src/__tests__/integration.test.ts#L1049-L1071 with calls to
it, preserving each provider’s session-scoped lidPnMapping setup and skip
behavior.

In `@src/client/coordinators/WaMessageDispatchCoordinator.ts`:
- Around line 1385-1399: Remove the conditional
signalAddressResolver.resolveMany call in the pending-target fallback and use
pendingTargetAddresses directly as the resolved address list passed to
sessionStore.hasSessions and indexed when constructing nextAvailableTargets.
Keep the existing pendingTargets iteration and JID mapping unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6631aad7-628f-4f87-b356-e098e027ac1b

📥 Commits

Reviewing files that changed from the base of the PR and between af82dfa and 2a909a1.

📒 Files selected for processing (57)
  • .changeset/calm-lids-align.md
  • README.md
  • packages/store-mongo/src/__tests__/integration.test.ts
  • packages/store-mongo/src/createMongoStore.ts
  • packages/store-mongo/src/index.ts
  • packages/store-mongo/src/lid-pn-mapping.store.ts
  • packages/store-mysql/src/__tests__/integration.test.ts
  • packages/store-mysql/src/connection.ts
  • packages/store-mysql/src/createMysqlStore.ts
  • packages/store-mysql/src/index.ts
  • packages/store-mysql/src/lid-pn-mapping.store.ts
  • packages/store-mysql/src/types.ts
  • packages/store-postgres/src/__tests__/integration.test.ts
  • packages/store-postgres/src/connection.ts
  • packages/store-postgres/src/createPostgresStore.ts
  • packages/store-postgres/src/index.ts
  • packages/store-postgres/src/lid-pn-mapping.store.ts
  • packages/store-postgres/src/types.ts
  • packages/store-redis/README.md
  • packages/store-redis/src/__tests__/integration.test.ts
  • packages/store-redis/src/createRedisStore.ts
  • packages/store-redis/src/index.ts
  • packages/store-redis/src/lid-pn-mapping.store.ts
  • packages/store-sqlite/src/__tests__/contracts.test.ts
  • packages/store-sqlite/src/createSqliteStore.ts
  • packages/store-sqlite/src/index.ts
  • packages/store-sqlite/src/lid-pn-mapping.store.ts
  • packages/store-sqlite/src/migrations.ts
  • packages/store-sqlite/src/table-names.ts
  • packages/store-sqlite/src/types.ts
  • src/client/WaClient.ts
  • src/client/WaClientFactory.ts
  • src/client/__tests__/client.test.ts
  • src/client/coordinators/WaMessageDispatchCoordinator.ts
  • src/client/coordinators/WaRetryCoordinator.ts
  • src/client/coordinators/__tests__/retry-coordinator.test.ts
  • src/message/crypto/icdc.ts
  • src/message/primitives/__tests__/incoming.test.ts
  • src/message/primitives/incoming.ts
  • src/signal/api/SignalIdentitySyncApi.ts
  • src/signal/api/__tests__/api.test.ts
  • src/signal/group/SenderKeyManager.ts
  • src/signal/group/__tests__/sender-key.test.ts
  • src/signal/index.ts
  • src/signal/session/SignalAddressResolver.ts
  • src/signal/session/SignalProtocol.ts
  • src/signal/session/__tests__/address-resolver.test.ts
  • src/signal/session/__tests__/resolver.test.ts
  • src/signal/session/__tests__/session.test.ts
  • src/signal/session/resolver.ts
  • src/store/__tests__/create-store.test.ts
  • src/store/contracts/lid-pn-mapping.store.ts
  • src/store/createStore.ts
  • src/store/index.ts
  • src/store/locks/lid-pn-mapping.lock.ts
  • src/store/memory/lid-pn-mapping.store.ts
  • src/store/types.ts

Comment thread src/client/WaClientFactory.ts
Comment thread src/client/WaClientFactory.ts Outdated
Comment thread src/message/crypto/icdc.ts Outdated
Comment thread src/message/primitives/incoming.ts Outdated
Comment thread src/signal/api/SignalIdentitySyncApi.ts Outdated
Comment thread src/signal/session/SignalAddressResolver.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 57 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/signal/session/SignalAddressResolver.ts Outdated
Comment thread packages/store-mongo/src/lid-pn-mapping.store.ts Outdated
Comment thread src/message/primitives/incoming.ts Outdated
Comment thread src/client/coordinators/WaRetryCoordinator.ts
Comment thread src/signal/group/SenderKeyManager.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 15 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/signal/session/SignalAddressResolver.ts Outdated
Comment thread src/client/WaClientFactory.ts 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.

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 17-18: Rename the constant CANONICAL_SIGNAL_USER_JID_OPTIONS to
WA_CANONICAL_SIGNAL_USER_JID_OPTIONS in src/protocol/jid.ts lines 17-18, and
update every usage in src/protocol/jid.ts lines 228-240 to reference the new
name.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63abf035-ba92-4498-a1f4-f16ad1c6a2f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9ecbb9b and 223ee3d.

📒 Files selected for processing (4)
  • src/protocol/__tests__/protocol.test.ts
  • src/protocol/jid.ts
  • src/signal/session/SignalAddressResolver.ts
  • src/signal/session/__tests__/address-resolver.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/signal/session/SignalAddressResolver.ts

Comment thread src/protocol/jid.ts Outdated
@jlucaso1

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@vinikjkkj

Copy link
Copy Markdown
Owner

when I made zapo, I chose not to add LID mapping due to the high overhead on the database, in memory (if using a cache), and in hot paths; the only send paths that still use PNs in official clients are peer ones (e.g PDO), which zapo already maps or those from unofficial libraries that still send via PN. What do you think?

Since the PR is marked as a fix, is there a problematic scenario I can reproduce?

Note: zapo already has a tiny lid map inside device list cache

@jlucaso1

Copy link
Copy Markdown
Contributor Author

when I made zapo, I chose not to add LID mapping due to the high overhead on the database, in memory (if using a cache), and in hot paths; the only send paths that still use PNs in official clients are peer ones (e.g PDO), which zapo already maps or those from unofficial libraries that still send via PN. What do you think?

Interesting point. I created the PR with compliance in mind, but I agree that in this case, performance matters more.

I'll close this soon and thank you for the feedback

@jlucaso1 jlucaso1 closed this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants