Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
373a6f3
fix(memory): handle ECACHEFULL on userDevicesCache.set in device-noti…
rsalcara Jun 7, 2026
424260a
release: develop → master 2026-06-06 (v2) — #509 + #510 + #511 + #512…
rsalcara Jun 7, 2026
44b7c91
chore: update proto/version to v2.3000.1040989513 (#516)
github-actions[bot] Jun 7, 2026
b5d1bab
chore: update WhatsApp Web version to v2.3000.1040994085 (#517)
rsalcara Jun 7, 2026
bb61401
release: develop → master 2026-06-08 — #514 + #515 + #518 + #519 + #520
rsalcara Jun 8, 2026
7f5d723
fix(audit #521): 3 P2 + 2 P3 from chatgpt/copilot/coderabbit review
rsalcara Jun 8, 2026
4214847
fix(audit #521): close cubic threads 8 + 9 (Issues D, E)
rsalcara Jun 8, 2026
b2c5abf
fix(lint): satisfy eqeqeq + remove unused isJidGroup import
rsalcara Jun 8, 2026
2ec8ba3
fix(audit #521): close cubic thread 13 — narrow OrphanMsmsg guard
rsalcara Jun 8, 2026
285db59
release: develop → master 2026-06-08 (5 PRs)
rsalcara Jun 8, 2026
20585f9
sync: master → develop 2026-06-08 — backfill release #521 fixes
rsalcara Jun 8, 2026
c4ea52a
chore: update proto/version to v2.3000.1041008395 (#524)
github-actions[bot] Jun 8, 2026
3d5a7bd
chore: update WhatsApp Web version to v2.3000.1041011968 (#525)
rsalcara Jun 8, 2026
6c882ba
chore: update proto/version to v2.3000.1041063798 (#527)
github-actions[bot] Jun 9, 2026
5290713
chore: update WhatsApp Web version to v2.3000.1041103517 (#528)
rsalcara Jun 9, 2026
296cfc8
Merge remote-tracking branch 'origin/master' into sync/master-to-deve…
rsalcara Jun 10, 2026
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
2 changes: 1 addition & 1 deletion WAProto/WAProto.proto
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;

/// WhatsApp Version: 2.3000.1040967287
/// WhatsApp Version: 2.3000.1041063798

message ADVDeviceIdentity {
optional uint32 rawId = 1;
Expand Down
2 changes: 1 addition & 1 deletion src/Defaults/baileys-version.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":[2,3000,1040973525]}
{"version":[2,3000,1041103517]}
27 changes: 27 additions & 0 deletions src/Socket/messages-recv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@
return
}

let data: any

Check warning on line 629 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Unexpected any. Specify a different type

Check warning on line 629 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Unexpected any. Specify a different type
try {
// Narrow payload content type before JSON.parse:
// content can be string (UTF-16 internally), Uint8Array, Buffer, or
Expand Down Expand Up @@ -839,7 +839,7 @@
case 'update': {
const settingsNode = getBinaryNodeChild(child, 'settings')
if (settingsNode) {
const update: Record<string, any> = {}

Check warning on line 842 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Unexpected any. Specify a different type

Check warning on line 842 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Unexpected any. Specify a different type
const nameNode = getBinaryNodeChild(settingsNode, 'name')
if (nameNode?.content) update.name = nameNode.content.toString()

Expand Down Expand Up @@ -3121,6 +3121,33 @@
return
}

// `OrphanMsmsgError` from `decryptMsmsgBotMessage` (Meta AI / FBID bot
// reply that arrived before we cached the matching outgoing
// `messageContextInfo.messageSecret`). Without this guard the stub
// string falls through to the Signal retry/PDO path below — pointless
// because a missing CACHE entry can't be recovered by a Signal retry,
// and we'd burn the retry budget asking the bot for prekeys it has
// no business issuing. Send a plain ACK (no NACK) so the server
// considers the message delivered; the next bot reply that arrives
// after the outgoing-secret cache populates will decrypt cleanly
// (audit thread 2 / chatgpt P2 on release PR #521).
//
// Narrow match on `'no messageSecret for '` (not the broader
// `'decryptMsmsgBotMessage:'` prefix): `decryptMsmsgBotMessage`
// also raises for real decryption failures — missing
// `meta.target_id`, missing `encIv`/`encPayload`, AES-GCM auth-tag
// mismatch — which deserve the Signal retry path, NOT a silent
// ACK. cubic audit thread 13 (PR #521).
if (
msg?.messageStubParameters?.[0]?.startsWith(
'decryptMsmsgBotMessage: no messageSecret for '
)
) {
await sendMessageAck(node)
acked = true
return
}

// Audit RETRY-A2 — sender-key stale em skmsg de grupo: o counter
// avançou no remetente mas a chain local não acompanhou. Reenviar
// o mesmo skmsg via sendRetryRequest sempre falha (mesma sender-key,
Expand Down Expand Up @@ -3772,7 +3799,7 @@
tcTokenRetriedMsgIds.add(retryKey)
// Each entry auto-expires after 60s — naturally bounded under normal use
setTimeout(() => tcTokenRetriedMsgIds.delete(retryKey), 60_000)
;(async () => {

Check warning on line 3802 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 3802 in src/Socket/messages-recv.ts

View workflow job for this annotation

GitHub Actions / check-lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
try {
await delay(1500)
const msg =
Expand Down
28 changes: 25 additions & 3 deletions src/Socket/messages-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1575,14 +1575,27 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
}

// Send DSM for all message types including carousel
// Send DSM for all message types including carousel.
// Mirrors the same `messageContextInfo` lift done on the initial-
// fanout DSM build (line ~1198 — release PR #519 fix): Lottie
// stickers wrap their `messageContextInfo` inside
// `lottieStickerMessage.message`, so the retry-resend envelope
// needs to lift it back to the DSM top-level just like the initial
// send does. Without this, the sender's other devices receive the
// retry without `messageSecret` / reporting token and the next
// edit / msmsg reply can't be decrypted on those devices.
// (audit thread 5 / coderabbit Major on release PR #521)
const retryDsmContextInfo =
messageToSend.lottieStickerMessage?.message?.messageContextInfo ??
messageToSend.messageContextInfo
const usesDSM = isMe
const encodedMessageToSend = usesDSM
? encodeWAMessage({
deviceSentMessage: {
destinationJid,
message: messageToSend
}
},
messageContextInfo: retryDsmContextInfo
})
: encodeWAMessage(messageToSend)

Expand Down Expand Up @@ -1878,10 +1891,19 @@ export const makeMessagesSocket = (config: SocketConfig) => {
logger.debug({ jid }, 'adding device identity')
}

// Lottie stickers (`{ lottieStickerMessage: { message: outer }}`) carry
// their `messageContextInfo` INSIDE the wrap — read both spots so the
// reporting-token attachment works the same for wrapped and unwrapped
// stickers (audit thread 1 / chatgpt P2 on release PR #521).
const reportingMessageSecret =
reportingMessage?.lottieStickerMessage?.message?.messageContextInfo?.messageSecret ??
reportingMessage?.messageContextInfo?.messageSecret

Comment thread
rsalcara marked this conversation as resolved.
if (
!isNewsletter &&
!isRetryResend &&
reportingMessage?.messageContextInfo?.messageSecret &&
reportingMessage &&
reportingMessageSecret &&
Comment thread
rsalcara marked this conversation as resolved.
shouldIncludeReportingToken(reportingMessage)
Comment thread
rsalcara marked this conversation as resolved.
) {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/auth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ export const addTransactionCapability = (
// with the message context attached (key/sender/decryptionJid). This
// site has none of that context — it would just duplicate the stack
// without anything actionable on top.
logger.trace({ err: (err as any)?.message }, 'transactWith rolled back')
logger.trace({ err: err instanceof Error ? err.message : String(err) }, 'transactWith rolled back')
throw err
} finally {
activeTransactions--
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/decode-wa-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const EMPTY_UINT8_ARRAY = new Uint8Array(0)
*
* Returns the input unchanged when there is no `deviceSentMessage` envelope.
*/
const unwrapDeviceSentMessage = (msg: proto.IMessage): proto.IMessage => {
export const unwrapDeviceSentMessage = (msg: proto.IMessage): proto.IMessage => {
const inner = msg.deviceSentMessage?.message
if (!inner) return msg

Expand Down
5 changes: 4 additions & 1 deletion src/Utils/error-log-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
* raises `TypeError`, so the object-without-message branch is guarded.
*/
export const compactError = (err: unknown): string => {
if (!err) return 'Unknown'
// Nullish-only check captures `null` AND `undefined` only — the previous
// `!err` also caught `0`, `''`, `false`, `NaN`, which are valid (if
// unusual) thrown values. cubic audit thread 9 (PR #521).
if (err === null || err === undefined) return 'Unknown'
if (err instanceof Error) {
const name = err.name || 'Error'
return `${name}: ${err.message}`
Expand Down
13 changes: 1 addition & 12 deletions src/Utils/meta-ai-msmsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import NodeCache from '@cacheable/node-cache'
import { proto } from '../../WAProto/index.js'
import { DEFAULT_CACHE_MAX_KEYS, DEFAULT_CACHE_TTLS } from '../Defaults'
import type { BinaryNode } from '../WABinary'
import { isJidGroup, isJidMetaAI, jidDecode, jidNormalizedUser } from '../WABinary/jid-utils'
import { isJidMetaAI, jidDecode, jidNormalizedUser } from '../WABinary/jid-utils'
import { isNodeCacheFullError } from './cache-utils'
import { aesDecryptGCM, hkdf } from './crypto'
import { compactError } from './error-log-utils'
Expand Down Expand Up @@ -519,14 +519,3 @@ export const isMsmsgBotConversation = (chatOrAuthorJid: string | undefined): boo
if (!chatOrAuthorJid) return false
return !!isJidMetaAI(chatOrAuthorJid)
}

/** Re-export for the test suite + decoder. */
export const __internal = {
BOT_MESSAGE_INFO,
KEY_LENGTH,
isMeJid,
deriveKeyAndDecrypt,
decodeDecryptedMsmsg,
userOnlyJid,
isJidGroup
}
27 changes: 8 additions & 19 deletions src/__tests__/Utils/dsm-context-info-preservation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,15 @@
* are caught too.
*/
import { proto } from '../../../WAProto/index.js'
import { unwrapDeviceSentMessage } from '../../Utils/decode-wa-message'

// The helper is intentionally not exported (it's a leaf utility used by
// `decryptMessageNode`). Re-derive its behaviour here to pin the exact
// per-field merge rules. If the rules change in `decode-wa-message.ts`,
// these expectations break first and point at the divergence.
const unwrapDeviceSentMessage = (msg: proto.IMessage): proto.IMessage => {
const inner = msg.deviceSentMessage?.message
if (!inner) return msg
const innerCtx = inner.messageContextInfo
const outerCtx = msg.messageContextInfo
const messageContextInfo: proto.IMessageContextInfo = {
...innerCtx,
messageSecret: innerCtx?.messageSecret ?? outerCtx?.messageSecret,
messageAssociation: innerCtx?.messageAssociation ?? outerCtx?.messageAssociation,
limitSharingV2: outerCtx?.limitSharingV2,
threadId: (innerCtx?.threadId?.length ? innerCtx.threadId : null) ?? outerCtx?.threadId ?? [],
botMetadata: innerCtx?.botMetadata ?? outerCtx?.botMetadata
}
return { ...inner, messageContextInfo }
}
// Import the REAL helper from production (not a re-derivation). Earlier
// version of this file kept a local copy because the helper wasn't exported,
// but that broke the contract these tests claim to enforce: a regression
// in `decode-wa-message.ts` would not surface here. cubic audit thread 8
// (PR #521) caught it — exported the helper as a named export and import
// it now, so any change to the production implementation is reflected in
// the assertions below.

const OUTER_SECRET = Buffer.alloc(32, 0xaa)
const INNER_SECRET = Buffer.alloc(32, 0xbb)
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/Utils/error-log-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ describe('compactError', () => {
expect(compactError(true)).toBe('true')
})

// cubic audit thread 9 (PR #521): the original `if (!err)` short-circuit
// captured `0`, `''`, `false`, `NaN` as "Unknown" even though those are
// (unusual but valid) thrown values. The narrowed `if (err == null)`
// nullish check keeps `undefined`/`null` → "Unknown" but lets the falsy
// primitives reach the `String(err)` fallback below.
it('preserves falsy primitives that are NOT null/undefined', () => {
expect(compactError(0)).toBe('0') // not 'Unknown'
expect(compactError(false)).toBe('false') // not 'Unknown'
expect(compactError('')).toBe('') // empty string is still a string
expect(compactError(NaN)).toBe('NaN') // not 'Unknown'
})

it('falls back to JSON.stringify on a plain object without .message', () => {
const err = { name: 'CustomBag', code: 'X1', details: 'no message field' }
expect(compactError(err)).toBe('CustomBag: {"name":"CustomBag","code":"X1","details":"no message field"}')
Expand Down
Loading