Skip to content

fix(decode): preserve outer messageContextInfo when unwrapping deviceSentMessage#520

Merged
rsalcara merged 3 commits into
developfrom
fix/preserve-dsm-context-info
Jun 8, 2026
Merged

fix(decode): preserve outer messageContextInfo when unwrapping deviceSentMessage#520
rsalcara merged 3 commits into
developfrom
fix/preserve-dsm-context-info

Conversation

@rsalcara

@rsalcara rsalcara commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

When WhatsApp delivers a fromMe:true message via a linked device, the stanza arrives wrapped in a deviceSentMessage envelope whose OUTER Message carries the operationally important messageContextInfo fields — including messageContextInfo.messageSecret. The previous unwrap

msg = msg.deviceSentMessage?.message || msg

dropped that outer context entirely. Two concrete consequences in our fork:

  1. Encrypted edit envelopes (upstream PR #2554) need the original messageContextInfo.messageSecret to derive the edit key. Without it on the fromMe-via-linked-device delivery, getMessage later returns the cached message without the secret and the edit decrypt fails.

  2. Meta AI / FBID bot msmsg cache shipped in PR feat(meta-ai): decrypt msmsg via empirically-validated WA Web algorithm #518 (cacheMessageSecretIfPresent, called immediately AFTER this unwrap on the same msg). Without preserving the outer context info, single-device sends and the first linked-device delivery both miss the cache — the exact "outgoing-side secret not cached" gap codex flagged on PR feat(meta-ai): decrypt msmsg via empirically-validated WA Web algorithm #518.

How this differs from upstream PR WhiskeySockets#2566

Upstream PR #2566 fixes the same bug with a generic {...outer, ...inner} spread. That covers the common case but LOSES outer fields when the inner ships a partial messageContextInfo (e.g. a thread reply with just threadId set — outer's messageSecret is dropped silently).

WA Web's actual implementation (WAWebDeviceSentMessageProtoUtils.l(e) — extracted from the live web client via the CDP source dump during the msmsg investigation, not via runtime hooking) merges field-by-field:

Field Rule Why
messageSecret inner preferred, outer fallback Message-local intent wins; envelope is fallback
messageAssociation inner preferred, outer fallback Same
threadId inner preferred, outer fallback, default [] Same, matches WA Web default
botMetadata inner preferred, outer fallback Same
limitSharingV2 ONLY outer Sharing policy attached at envelope fanout level; inner has no authoritative voice
(any future field) ...innerCtx spread Forward-compatible

Validation methodology

CDP live runtime hooking was deliberately NOT used here. The relevant module (WAWebDeviceSentMessageProtoUtils) was already captured to disk in script-395.js during the PR WhiskeySockets#2592 (msmsg) investigation. The full function l(e) was extracted via grep and matched line-for-line against this implementation.

Live hooking would have added zero structural information — the algorithm is purely static proto-object manipulation, no dynamic crypto inputs, no per-call state. Reserved CDP hooking for cases where the inputs themselves drive the algorithm (e.g. HKDF info bytes for msmsg).

Changes

File Change
src/Utils/decode-wa-message.ts NEW unwrapDeviceSentMessage(msg) helper at module scope. Call site collapses to msg = unwrapDeviceSentMessage(msg)
src/__tests__/Utils/dsm-context-info-preservation.test.ts NEW — 9 tests including SHAPE-PIN that re-derives the naive {...outer, ...inner} approach and asserts it loses messageSecret on the partial-inner case

Validation

  • npm run build clean
  • 9/9 new tests pass
  • 47/47 across DSM + msmsg + Lottie sticker + protocol-guard suites
  • No regressions

Customizations preserved (NOT touched)

Carousel, lists, buttons, polls, view-once, biz quality_control, useLegacyLock, TC token custom flow, LID↔PN batched, Phase 9 multi-DB, lidDbMigrated:false, cacheMetricsInterval memory-leak fix, schema migrations + statement cache + busy retry, recoverable-error compact logging, Meta AI msmsg decryption, Lottie sticker wrap/unwrap.

🤖 Generated with Claude Code


Summary by cubic

Preserves the outer messageContextInfo when unwrapping deviceSentMessage so linked-device fromMe messages keep messageSecret and related fields. Fixes encrypted edit decrypts and ensures the msmsg secret cache is populated on first delivery.

  • Bug Fixes
    • Added unwrapDeviceSentMessage to merge outer/inner context like WA Web: inner wins for messageSecret, messageAssociation, botMetadata; threadId falls back to outer when inner is empty, else inner wins; limitSharingV2 is outer-only; threadId defaults to [].
    • Replaced the naive unwrap in decryptMessageNode with the helper to prevent dropping outer context.
    • Expanded tests to 10, including the protobuf threadId: [] default case; kept the SHAPE-PIN for the naive spread and silenced its intentional unused-var lint.

Written for commit aef7ac5. Summary will update on new commits.

Review in cubic

…SentMessage

When WhatsApp delivers a `fromMe:true` message via a linked device, the
stanza arrives wrapped in a `deviceSentMessage` envelope whose OUTER
`Message` carries the operationally important `messageContextInfo`
fields — including `messageContextInfo.messageSecret`. The previous
unwrap

    msg = msg.deviceSentMessage?.message || msg

dropped that outer context entirely. Two concrete consequences in our
fork:

  1. Encrypted edit envelopes (the upstream PR WhiskeySockets#2554 path) need the
     original `messageContextInfo.messageSecret` to derive the edit
     key. With it lost on the fromMe-via-linked-device delivery,
     `getMessage` later returns the cached message without the secret
     and the edit decrypt fails.

  2. The Meta AI / FBID bot msmsg cache shipped in PR #518
     (`cacheMessageSecretIfPresent`, called immediately AFTER this
     unwrap on the same `msg`) needs the secret to land against the
     outgoing message's id so subsequent bot replies decrypt. Without
     preserving the outer context info, single-device sends and the
     first linked-device delivery both miss the cache — the exact
     "outgoing-side secret not cached" gap codex flagged on PR #518.

Why a named per-field merge instead of a generic `{...outer, ...inner}`
spread (upstream PR WhiskeySockets#2566's approach):

PR WhiskeySockets#2566 fixes the common case with a spread that lets the inner
message's `messageContextInfo` ENTIRELY override the outer's. If the
inner ships a PARTIAL `messageContextInfo` (e.g. a thread reply with
just `threadId` set), the outer's `messageSecret` is dropped silently.
WA Web's `WAWebDeviceSentMessageProtoUtils.l(e)` — extracted from
the live web client via CDP source dump during the msmsg
investigation — merges field-by-field with explicit per-field rules:

  - `messageSecret`, `messageAssociation`, `threadId`, `botMetadata`:
    inner preferred when present, outer fallback. Inner expresses
    message-local intent; outer is the envelope's best knowledge.
  - `limitSharingV2`: ONLY from outer. The sharing policy attached to
    the linked-device fanout is set by the originator at the envelope
    level; the inner has no authoritative voice.
  - All other (future) messageContextInfo fields: pulled in via
    `...innerCtx` spread so adding a new field to the proto doesn't
    require touching this code.

Helper:
  * NEW `unwrapDeviceSentMessage(msg)` at module scope. The wider
    decrypt switch had no business knowing about
    `messageContextInfo` field semantics — the helper isolates the
    rules in one place and keeps the call site a single expression
    (`msg = unwrapDeviceSentMessage(msg)`).
  * Returns the input unchanged when there is no `deviceSentMessage`
    envelope (matches the previous fallthrough semantics).

Tests:
  * NEW `src/__tests__/Utils/dsm-context-info-preservation.test.ts`
    with 9 cases:
      - no-op when no deviceSentMessage envelope
      - the case that motivated upstream PR WhiskeySockets#2566 (outer-only
        messageSecret) — works under both our impl AND PR WhiskeySockets#2566
      - the edge case PR WhiskeySockets#2566 FAILS on (inner partial
        messageContextInfo + outer messageSecret) — works only with
        per-field merge
      - inner-preferred semantics for shared fields
      - `limitSharingV2` sourced ONLY from outer
      - `threadId` defaults to `[]` to match WA Web
      - inner precedence for botMetadata + messageAssociation
      - result drops the deviceSentMessage envelope
      - SHAPE-PIN: explicitly re-derives the naive
        `{...outer, ...inner}` approach and asserts it loses
        messageSecret on the partial-inner case. If a future
        refactor swaps our helper for the naive spread, this
        assertion breaks first and points at the divergence.

Validation:
  * `npm run build` clean
  * 47/47 tests pass across DSM (9 new) + msmsg (21) + Lottie sticker
    (9) + protocol-guard (8) suites.

Customizations preserved (NOT touched):
  Carousel, lists, buttons, polls, view-once, biz `quality_control`,
  `useLegacyLock`, TC token custom flow, LID↔PN batched, Phase 9
  multi-DB, `lidDbMigrated:false`, `cacheMetricsInterval` memory-leak
  fix, schema migrations + statement cache + busy retry, recoverable-
  error compact logging, Meta AI msmsg decryption, Lottie sticker
  wrap/unwrap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 00:50
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c64c962-bcd6-4dee-8126-52344f58a3d0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/preserve-dsm-context-info

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Thanks for opening this pull request and contributing to the project!

The next step is for the maintainers to review your changes. If everything looks good, it will be approved and merged into the main branch.

In the meantime, anyone in the community is encouraged to test this pull request and provide feedback.

✅ How to confirm it works

If you’ve tested this PR, please comment below with:

Tested and working ✅

This helps us speed up the review and merge process.

📦 To test this PR locally:

# NPM
npm install @whiskeysockets/baileys@rsalcara/InfiniteAPI#fix/preserve-dsm-context-info

# Yarn (v2+)
yarn add @whiskeysockets/baileys@rsalcara/InfiniteAPI#fix/preserve-dsm-context-info

# PNPM
pnpm add @whiskeysockets/baileys@rsalcara/InfiniteAPI#fix/preserve-dsm-context-info

If you encounter any issues or have feedback, feel free to comment as well.

@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: 69902b7ac2

ℹ️ 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/Utils/decode-wa-message.ts Outdated
…E-PIN

CI lint failed on `dsm-context-info-preservation.test.ts:202` with
`'deviceSentMessage' is assigned a value but never used`. The
destructure is intentional — it re-derives the naive
`{...outer, ...inner}` approach from upstream PR WhiskeySockets#2566 by pulling
`deviceSentMessage` out of the outer object and discarding it. The
discarded binding name matches the proto field by design; renaming it
to `_deviceSentMessage` would also work but would obscure the parallel
to PR WhiskeySockets#2566's exact code.

Disable the rule for that one line and add a comment explaining why
the throwaway is named the way it is.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Fixes linked-device (deviceSentMessage) unwrapping so fromMe:true messages retain the outer messageContextInfo (notably messageSecret) needed by downstream features like encrypted edits and the Meta AI msmsg secret cache.

Changes:

  • Added a new unwrapDeviceSentMessage(msg) helper that merges outer/inner messageContextInfo with WA Web–matching precedence rules.
  • Updated decryptMessageNode to use the helper instead of msg.deviceSentMessage?.message || msg.
  • Added a new regression test suite intended to pin the merge behavior for partial inner messageContextInfo.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Utils/decode-wa-message.ts Introduces unwrapDeviceSentMessage and applies it during message decode to preserve outer messageContextInfo.
src/__tests__/Utils/dsm-context-info-preservation.test.ts Adds regression tests around DSM unwrap + context merge behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Utils/decode-wa-message.ts
Comment thread src/__tests__/Utils/dsm-context-info-preservation.test.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.

3 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/__tests__/Utils/dsm-context-info-preservation.test.ts">

<violation number="1" location="src/__tests__/Utils/dsm-context-info-preservation.test.ts:30">
P2: Duplicated production logic: test re-implements `unwrapDeviceSentMessage` instead of importing it, creating a maintenance hazard where tests won't detect production changes</violation>
</file>

<file name="src/Utils/decode-wa-message.ts">

<violation number="1" location="src/Utils/decode-wa-message.ts:87">
P2: Outer-only `messageContextInfo` fields are still discarded because the merge starts from `innerCtx` instead of combining outer + inner.</violation>

<violation number="2" location="src/Utils/decode-wa-message.ts:91">
P2: Depending on how the WAProto types are generated, decoded `MessageContextInfo` may have `threadId` set to `[]` (rather than `undefined`) even when the field was absent on the wire. If so, `innerCtx?.threadId ?? outerCtx?.threadId` will always pick the inner empty array whenever any `messageContextInfo` exists on the inner message, silently dropping the outer thread association. Consider checking for a non-empty inner array before falling back:
```ts
threadId: (innerCtx?.threadId?.length ? innerCtx.threadId : null) ?? outerCtx?.threadId ?? [],
```</violation>
</file>

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

Re-trigger cubic

Comment thread src/__tests__/Utils/dsm-context-info-preservation.test.ts
Comment thread src/Utils/decode-wa-message.ts
Comment thread src/Utils/decode-wa-message.ts Outdated
cubic + chatgpt audit (Threads 1 and 6) caught a real bug that the
previous spec-pin tests did not exercise:

  `proto.MessageContextInfo.decode(...)` initializes the `repeated`
  `threadId` field to `[]` (proto3 default), NOT `undefined`. The
  per-field merge expression

      threadId: innerCtx?.threadId ?? outerCtx?.threadId ?? []

  therefore NEVER falls through to the outer side when the inner
  message has a `messageContextInfo` instance — `[]` isn't nullish.
  Any thread context the outer envelope carried is silently dropped
  for thread replies delivered via a linked device.

Empirical verification:

  proto.MessageContextInfo.decode(encode({ messageSecret: X }))
    .threadId
    → []                           ← not undefined
    → [] ?? outerCtx?.threadId
    → []                           ← outer threadId lost

Fix mirrors `WAWebDeviceSentMessageProtoUtils.l(e)`'s implicit
length-check (the JS source ships a similar guard via the runtime
`Array.length` truthiness chain):

  threadId: (innerCtx?.threadId?.length ? innerCtx.threadId : null)
              ?? outerCtx?.threadId
              ?? []

An empty inner array is now treated as "inner didn't set it" so the
outer wins. A non-empty inner array (sender explicitly set a thread
context) keeps inner-preferred semantics. Both sides empty falls to
the `[]` default that matches WA Web.

Test added:
  - `outer threadId WINS when inner messageContextInfo exists without
    a threadId in the wire (protobuf [] vs ?? trap)`. Uses an actual
    `proto.MessageContextInfo.decode(...)` to produce the inner
    `messageContextInfo` so the `[]` default comes from real protobuf
    behaviour, not a hand-constructed object. Asserts both outer
    threadId is preserved AND inner messageSecret still wins.

10/10 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rsalcara rsalcara merged commit d588037 into develop Jun 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants