From 373a6f362205184976f9accfeb6b734e9505837b Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sat, 6 Jun 2026 23:41:30 -0300 Subject: [PATCH 01/23] fix(memory): handle ECACHEFULL on userDevicesCache.set in device-notif path (#513 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #513 chatgpt-codex review (P2) caught a real gap that PR #509 introduced when it added the `maxKeys` cap to `userDevicesCache`: - PR #509 added `safeCacheSet` handling to the cache-population path in `messages-send.ts:528-548` (where USync fills the cache). - The OTHER call-site that writes to `userDevicesCache` — `messages-recv.ts:2240` in the device-notification handler ('add' / 'remove' tags) — was left unprotected. `@cacheable/node-cache`'s capacity check is `keyCount() + 1 > maxKeys` — applied BEFORE the engine checks whether the key already exists. So once the cache reaches its `maxKeys` ceiling (5,000 entries), even an UPDATE to an already-cached user throws ECACHEFULL. The guard `if (!existingCache.length) continue` at line 2210 doesn't help: it only short-circuits when the user is NOT cached. A device-list update for an already-cached user would still hit the `.set` and throw. Real impact under sustained gateway load: - Cache hits 5,000 entries (a few dozen active groups will get there). - A device-add/remove notification arrives for one of the cached users. - `userDevicesCache.set(...)` throws ECACHEFULL. - The throw propagates into the message-receive handler and lands in Baileys' outer error boundary — connection survives, but a log-level error is emitted and the affected user's device list is NOT updated in the cache. - Next message-send for that user fetches the fresh list via USync (`getUSyncDevices`), which IS guarded — so the durable behavior eventually recovers. The fix routes the device-notif write through the same `safeCacheSet` helper PR #509 used in messages-send.ts. Same swallow-ECACHEFULL semantics, same debug log, same `getUSyncDevices` fallback path. Symmetric handling across the two call-sites that update the cache. Test plan: - npm run build ✓ (3 phases pass, zero errors) - 1 line of production change in `messages-recv.ts:2240`, plus comment explaining the WHY. No test changes needed — `safeCacheSet`'s behavior is already covered by `cache-utils.test.ts`. Out of scope (intentionally 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, the cacheMetricsInterval memory-leak fix, schema migrations + statement cache + busy retry. Co-Authored-By: Claude Opus 4.7 --- src/Socket/messages-recv.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 7c679178946..4526fce3b3f 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -2236,8 +2236,19 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (updatedDevices.length === 0) { await userDevicesCache?.del(user) - } else { - await userDevicesCache?.set(user, updatedDevices) + } else if (userDevicesCache) { + // PR #513 review (chatgpt-codex P2): mirror the ECACHEFULL + // handling already in place for `userDevicesCache.set` in + // messages-send.ts. With the `maxKeys` cap added in PR + // #509, `@cacheable/node-cache` throws ECACHEFULL when + // `keyCount() + 1 > maxKeys` — and that check is hit even + // for an UPDATE of an already-cached key when the cache + // is at capacity, because the underlying `set` doesn't + // short-circuit on existing-key writes. `safeCacheSet` + // swallows the throw with a debug log; the durable USync + // state is unaffected (next message-send will re-fetch + // via `getUSyncDevices`, same fallback semantics). + await safeCacheSet(userDevicesCache, user, updatedDevices, logger, 'userDevicesCache') } } }) From 44b7c9113af5b1b443e1a21264557811b410d87c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:28:30 -0300 Subject: [PATCH 02/23] chore: update proto/version to v2.3000.1040989513 (#516) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index da789106b7e..a26d7122ab3 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1040967287 +/// WhatsApp Version: 2.3000.1040989513 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 3675f7876f5..dd2a9f077e7 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1040973525]} +{"version": [2, 3000, 1040989513]} From b5d1babf20270fbd754989ab7c04f88060382715 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 7 Jun 2026 07:00:09 -0300 Subject: [PATCH 03/23] chore: update WhatsApp Web version to v2.3000.1040994085 (#517) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index dd2a9f077e7..11b5daaec5d 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1040989513]} +{"version":[2,3000,1040994085]} From 7f5d7231aa4307c48f21c30cb489d6fd63f99d1a Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 7 Jun 2026 22:48:51 -0300 Subject: [PATCH 04/23] fix(audit #521): 3 P2 + 2 P3 from chatgpt/copilot/coderabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 findings from the release PR #521 audit applied before promoting develop → master. The release branch itself doesn't change; this PR lands the fixes on develop and the release branch will be refreshed to include them. P2 — Thread 1 (chatgpt) — reporting-token check reads outer wrap ================================================================ `messages-send.ts:1884` gated reporting-token attachment on `reportingMessage?.messageContextInfo?.messageSecret`. For Lottie stickers post-PR #519 that secret lives INSIDE the `lottieStickerMessage` wrap — the top-level read returns undefined and the token is silently skipped. Regression vs plain sticker behaviour. Fix: lift `messageContextInfo` from both locations the same way the initial-fanout DSM read does (the lift PR #519 already shipped on line ~1198): const reportingMessageSecret = reportingMessage?.lottieStickerMessage?.message ?.messageContextInfo?.messageSecret ?? reportingMessage?.messageContextInfo?.messageSecret Required adding `reportingMessage &&` to the surrounding `if` so TypeScript doesn't lose the narrowing it previously got from the inline property chain (the narrowing was the side-effect of reading `reportingMessage.messageContextInfo` in the condition itself). P2 — Thread 5 (coderabbit Major) — retry-resend DSM drops messageContextInfo ============================================================================ `messages-send.ts:1580` rebuilds the `deviceSentMessage` envelope on the retry-resend path WITHOUT carrying `messageContextInfo` along. The initial-fanout path got the lift in PR #519 (line ~1198); the retry path was missed. Concrete impact: when a companion device receives the RETRY of a Lottie sticker via DSM, our own `unwrapDeviceSentMessage` finds: - inner = messageToSend = { lottieStickerMessage: { message: { stickerMessage, messageContextInfo } } } - inner.messageContextInfo = undefined (it's nested in the wrap) - outer.messageContextInfo = undefined (the retry envelope omitted it) → messageSecret = undefined, reporting token / encrypted-edit decryption material lost on the companion's copy. Fix: same lift inline at the retry envelope build site. P2 — Thread 2 (chatgpt) — OrphanMsmsgError stub goes to Signal retry path ========================================================================= When `decryptMsmsgBotMessage` raises `OrphanMsmsgError` (cache miss), the catch in `decode-wa-message.ts` sets `messageStubType = CIPHERTEXT` and `messageStubParameters[0] = String(err.message)`. The receive handler at `messages-recv.ts:3115` checks for known stub-param strings and falls through to the Signal retry / PDO placeholder-resend path when none match — burning retry budget asking the bot for prekeys it has no business issuing, for a problem (missing CACHE entry) that a Signal retry can never fix. Fix: add a guard for `messageStubParameters[0]?.startsWith('decryptMsmsgBotMessage:')` right after the `MISSING_KEYS_ERROR_TEXT` branch. Plain ACK (no NACK, no retry) so the server considers the message delivered. The next bot reply that arrives after the outgoing-secret cache populates will decrypt cleanly. NOT a NACK MissingMessageSecret: that would tell the server to retransmit, and the retransmission will hit the same orphan state until the outgoing-side cache is populated (deferred per PR #518). P3 — Thread 4 (copilot) — `__internal` export not consumed ========================================================== `src/Utils/meta-ai-msmsg.ts` exported an `__internal` bag of helpers (`BOT_MESSAGE_INFO`, `KEY_LENGTH`, `isMeJid`, `deriveKeyAndDecrypt`, `decodeDecryptedMsmsg`, `userOnlyJid`, `isJidGroup`) that no file in the repo actually imports. Grep confirms zero call sites. Removed — the helpers stay module-private as intended. P3 — Thread 3 (copilot) — `(err as any)?.message` in auth-utils =============================================================== `src/Utils/auth-utils.ts:714` (the trace I added in PR #515 to log `transactWith rolled back` without duplicating the stack) used a plain `(err as any)?.message` cast. Tightened to `err instanceof Error ? err.message : String(err)` — no runtime change, just removes the `any` cast and gives string fallback for non-Error throws. Validation ========== * `npm run build` clean (TS narrowing fixed via explicit `reportingMessage &&` guard). * 57/57 tests pass across the new-PR suites (dsm-context-info-preservation + meta-ai-msmsg + lottie-sticker-message + error-log-utils + process-message.protocol-guard). Co-Authored-By: Claude Opus 4.7 --- src/Socket/messages-recv.ts | 16 ++++++++++++++++ src/Socket/messages-send.ts | 28 +++++++++++++++++++++++++--- src/Utils/auth-utils.ts | 2 +- src/Utils/meta-ai-msmsg.ts | 11 ----------- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 2e5e8450f79..ba3b3468cc5 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -3121,6 +3121,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { 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). + if (msg?.messageStubParameters?.[0]?.startsWith('decryptMsmsgBotMessage:')) { + 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, diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 5db38ab3371..530e4fffc8f 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -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) @@ -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 + if ( !isNewsletter && !isRetryResend && - reportingMessage?.messageContextInfo?.messageSecret && + reportingMessage && + reportingMessageSecret && shouldIncludeReportingToken(reportingMessage) ) { try { diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index a18cf81e34e..f622a511e4a 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -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-- diff --git a/src/Utils/meta-ai-msmsg.ts b/src/Utils/meta-ai-msmsg.ts index 2bd1415e8a7..466261b8e2f 100644 --- a/src/Utils/meta-ai-msmsg.ts +++ b/src/Utils/meta-ai-msmsg.ts @@ -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 -} From 4214847556b7a5e71095c5c3ce7e207387122d7b Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 7 Jun 2026 23:04:58 -0300 Subject: [PATCH 05/23] fix(audit #521): close cubic threads 8 + 9 (Issues D, E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the cubic re-review on PR #521 that the previous audit-fix commit (cherry-picked from closed PR #522) didn't cover. Issue D (P2) — test re-implemented production helper ==================================================== cubic thread 8: `dsm-context-info-preservation.test.ts` kept a local copy of `unwrapDeviceSentMessage` and asserted against it. Comment claimed "if rules change in decode-wa-message.ts these break first" but that was wrong — the assertions tested the LOCAL copy, not production. Reverting `decode-wa-message.ts` to the old `msg = msg.deviceSentMessage?.message || msg` would leave all 10 tests green. The tests were validating themselves. Fix: * `decode-wa-message.ts` — flipped the helper from module-private `const` to `export const unwrapDeviceSentMessage`. The function is a leaf utility; exporting it surfaces nothing operational beyond what tests need. * `dsm-context-info-preservation.test.ts` — deleted the re-derived copy, imported from `../../Utils/decode-wa-message`. Header comment updated to explain WHY we now import (production parity) instead of re-derive. Issue E (P3) — `if (!err)` too broad in compactError ==================================================== cubic thread 9: `error-log-utils.ts:33` short-circuited every falsy input (including `0`, `''`, `false`, `NaN`) to the literal string `'Unknown'`. Those are unusual but valid thrown values — code that does `throw 0` would get its actual value erased. Operational impact near zero (Signal Protocol throws `Error` instances), but the contract is wrong. Fix: * `error-log-utils.ts` — `if (!err)` → `if (err == null)`. Comment added explaining the nullish-only narrowing. * `error-log-utils.test.ts` — new test `preserves falsy primitives that are NOT null/undefined` pins the new contract: `compactError(0)` → `'0'`, `compactError(false)` → `'false'`, etc. Validation ========== * `npm run build` clean * 58/58 tests pass (5 suites: dsm-context-info + error-log-utils + meta-ai-msmsg + lottie-sticker-message + process-message). Was 57; +1 from the new falsy-primitives test. Co-Authored-By: Claude Opus 4.7 --- src/Utils/decode-wa-message.ts | 2 +- src/Utils/error-log-utils.ts | 5 +++- .../dsm-context-info-preservation.test.ts | 27 ++++++------------- src/__tests__/Utils/error-log-utils.test.ts | 12 +++++++++ 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index fd4f35410a2..221a3bf7425 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -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 diff --git a/src/Utils/error-log-utils.ts b/src/Utils/error-log-utils.ts index 88f0f34c39a..9d4903f0f3c 100644 --- a/src/Utils/error-log-utils.ts +++ b/src/Utils/error-log-utils.ts @@ -30,7 +30,10 @@ * raises `TypeError`, so the object-without-message branch is guarded. */ export const compactError = (err: unknown): string => { - if (!err) return 'Unknown' + // Loose-equality check captures `null` AND `undefined` only — `!err` also + // caught `0`, `''`, `false`, `NaN`, which are valid (if unusual) thrown + // values. cubic audit thread 9 (PR #521). + if (err == null) return 'Unknown' if (err instanceof Error) { const name = err.name || 'Error' return `${name}: ${err.message}` diff --git a/src/__tests__/Utils/dsm-context-info-preservation.test.ts b/src/__tests__/Utils/dsm-context-info-preservation.test.ts index 4bfb4ccca28..687a6a90fdb 100644 --- a/src/__tests__/Utils/dsm-context-info-preservation.test.ts +++ b/src/__tests__/Utils/dsm-context-info-preservation.test.ts @@ -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) diff --git a/src/__tests__/Utils/error-log-utils.test.ts b/src/__tests__/Utils/error-log-utils.test.ts index 707ce9fc7a0..12f01b2d287 100644 --- a/src/__tests__/Utils/error-log-utils.test.ts +++ b/src/__tests__/Utils/error-log-utils.test.ts @@ -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"}') From b2c5abfaf63ee8c5b4ffb6d091341634aeacab06 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 7 Jun 2026 23:18:16 -0300 Subject: [PATCH 06/23] fix(lint): satisfy eqeqeq + remove unused isJidGroup import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI lint failed on the previous commit: - error-log-utils.ts:36 — `if (err == null)` violated the codebase's `eqeqeq` rule even though `== null` is the idiomatic nullish check. Expanded to `if (err === null || err === undefined)` for the same semantics with strict-equality compliance. - meta-ai-msmsg.ts:46 — `isJidGroup` was imported but its only consumer (the now-removed `__internal` export bag from cubic audit thread 4) is gone. Removed from the import list. Co-Authored-By: Claude Opus 4.7 --- src/Utils/error-log-utils.ts | 8 ++++---- src/Utils/meta-ai-msmsg.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Utils/error-log-utils.ts b/src/Utils/error-log-utils.ts index 9d4903f0f3c..a7ecf29bfc5 100644 --- a/src/Utils/error-log-utils.ts +++ b/src/Utils/error-log-utils.ts @@ -30,10 +30,10 @@ * raises `TypeError`, so the object-without-message branch is guarded. */ export const compactError = (err: unknown): string => { - // Loose-equality check captures `null` AND `undefined` only — `!err` also - // caught `0`, `''`, `false`, `NaN`, which are valid (if unusual) thrown - // values. cubic audit thread 9 (PR #521). - if (err == null) 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}` diff --git a/src/Utils/meta-ai-msmsg.ts b/src/Utils/meta-ai-msmsg.ts index 466261b8e2f..c85d6861eda 100644 --- a/src/Utils/meta-ai-msmsg.ts +++ b/src/Utils/meta-ai-msmsg.ts @@ -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' From 2ec8ba3ccbb8dcc99aad6644bd1073e2e05efb8c Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 7 Jun 2026 23:26:49 -0300 Subject: [PATCH 07/23] =?UTF-8?q?fix(audit=20#521):=20close=20cubic=20thre?= =?UTF-8?q?ad=2013=20=E2=80=94=20narrow=20OrphanMsmsg=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cubic re-review (thread 13, confidence 9) caught that the previous ACK guard was too broad: if (msg?.messageStubParameters?.[0]?.startsWith('decryptMsmsgBotMessage:')) `decryptMsmsgBotMessage` throws with a `decryptMsmsgBotMessage:` prefix for several distinct conditions, not just `OrphanMsmsgError`: - `'decryptMsmsgBotMessage: no messageSecret for ${cacheKey}'` ← OrphanMsmsgError — cache miss, ACK + wait for cache to populate - `'decryptMsmsgBotMessage: missing meta.target_id'` ← malformed stanza — deserves NACK/retry - `'decryptMsmsgBotMessage: MessageSecretMessage missing encIv/encPayload'` ← malformed proto — deserves NACK/retry - real AES-GCM auth-tag mismatch (string varies but may also start with `decryptMsmsgBotMessage:` if wrapped) ← deserves the Signal retry path The broader prefix silently ACK'd all of those, hiding real protocol failures from the server-side retry machinery. Narrow the match to the exact substring that uniquely identifies the orphan-cache case: startsWith('decryptMsmsgBotMessage: no messageSecret for ') Other `decryptMsmsgBotMessage:` failures now flow normally to the Signal retry / PDO placeholder-resend path (where they belong). Comment expanded to document why the narrow match is required. Validation: * build clean * 58/58 tests still pass Thread 12 (P3) — `String(err)` in auth-utils.ts:714 — is cosmetic and deferred per the audit recommendation. Co-Authored-By: Claude Opus 4.7 --- src/Socket/messages-recv.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index ba3b3468cc5..e763d22aa6e 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -3131,7 +3131,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // 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). - if (msg?.messageStubParameters?.[0]?.startsWith('decryptMsmsgBotMessage:')) { + // + // 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 From c4ea52adfe001052c1cc8711ef8e83a39e4e4ab3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:28:34 -0300 Subject: [PATCH 08/23] chore: update proto/version to v2.3000.1041008395 (#524) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index a26d7122ab3..579d3821fd1 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1040989513 +/// WhatsApp Version: 2.3000.1041008395 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 11b5daaec5d..c9464ae9d7a 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1040994085]} +{"version": [2, 3000, 1041008395]} From 3d5a7bd47cd493e12474850904377e16279e5595 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Mon, 8 Jun 2026 07:32:08 -0300 Subject: [PATCH 09/23] chore: update WhatsApp Web version to v2.3000.1041011968 (#525) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index c9464ae9d7a..b3e7edfd6f3 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041008395]} +{"version":[2,3000,1041011968]} From 6c882ba759d4bd02e92ba6ad2cb3ea3931212181 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:23:10 -0300 Subject: [PATCH 10/23] chore: update proto/version to v2.3000.1041063798 (#527) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 579d3821fd1..55f82c29d8b 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041008395 +/// WhatsApp Version: 2.3000.1041063798 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index b3e7edfd6f3..f83e1687c9e 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041011968]} +{"version": [2, 3000, 1041063798]} From 529071313b849fc25937e3c096b5f762775160b5 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Tue, 9 Jun 2026 07:13:09 -0300 Subject: [PATCH 11/23] chore: update WhatsApp Web version to v2.3000.1041103517 (#528) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index f83e1687c9e..0a4e01d0152 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041063798]} +{"version":[2,3000,1041103517]} From 97cd6762531b2ccdb0a0d2765efc2ea55efd5d49 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:28:08 -0300 Subject: [PATCH 12/23] chore: update proto/version to v2.3000.1041168064 (#530) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 55f82c29d8b..3c93b23d368 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041063798 +/// WhatsApp Version: 2.3000.1041168064 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 0a4e01d0152..313075d3c4f 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041103517]} +{"version": [2, 3000, 1041168064]} From 1392734a746c12e79c0dbb763c9c9e62721112ef Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Wed, 10 Jun 2026 07:24:28 -0300 Subject: [PATCH 13/23] chore: update WhatsApp Web version to v2.3000.1041187577 (#531) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 313075d3c4f..1d5005faffd 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041168064]} +{"version":[2,3000,1041187577]} From 0c849606ccb4734a192b334c6b8218b9effccb17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:28:21 -0300 Subject: [PATCH 14/23] chore: update proto/version to v2.3000.1041257863 (#539) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 3c93b23d368..86e6d90ac83 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041168064 +/// WhatsApp Version: 2.3000.1041257863 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 1d5005faffd..0736d82dc1b 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041187577]} +{"version": [2, 3000, 1041257863]} From 41b245ccc6d831ad7cb7fc41973e65372c7b87e6 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Thu, 11 Jun 2026 07:30:26 -0300 Subject: [PATCH 15/23] chore: update WhatsApp Web version to v2.3000.1041271747 (#540) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 0736d82dc1b..cc45c0ede7f 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041257863]} +{"version":[2,3000,1041271747]} From 2fca0f5762cc0659d33dde15aa5824c76a64970e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 01:28:47 -0300 Subject: [PATCH 16/23] chore: update proto/version to v2.3000.1041336118 (#541) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 86e6d90ac83..77ee84f0520 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041257863 +/// WhatsApp Version: 2.3000.1041336118 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index cc45c0ede7f..e633cf2cd6b 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041271747]} +{"version": [2, 3000, 1041336118]} From 17f794131e4f7bb92f82d6bdcb01839286d0dc57 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Fri, 12 Jun 2026 07:28:51 -0300 Subject: [PATCH 17/23] chore: update WhatsApp Web version to v2.3000.1041353304 (#542) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index e633cf2cd6b..751df963ec9 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041336118]} +{"version":[2,3000,1041353304]} From 0d867b4929d70662367bdbafc27a1742f6f6fe66 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:28:04 -0300 Subject: [PATCH 18/23] chore: update proto/version to v2.3000.1041410692 (#543) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 77ee84f0520..4a38feff0d2 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041336118 +/// WhatsApp Version: 2.3000.1041410692 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 751df963ec9..577442aeb67 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041353304]} +{"version": [2, 3000, 1041410692]} From bd38378543e745f3a8e7e4130126df79709df6dd Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sat, 13 Jun 2026 06:59:13 -0300 Subject: [PATCH 19/23] chore: update WhatsApp Web version to v2.3000.1041420778 (#544) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index 577442aeb67..b23951490a2 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041410692]} +{"version":[2,3000,1041420778]} From 8b0176b8fa71bbf9abfbea51ebcaf05574c5144c Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sat, 13 Jun 2026 23:22:20 -0300 Subject: [PATCH 20/23] fix(PR #546 audit): tr range BLOCKER + 5 VoIP P1s + JID-HOSTED-P2 + 4 P2/P3 polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-6 audit on the release branch flagged 1 blocker and 9 real findings. All applied directly on this branch so the PR merges into master clean. ## BLOCKER — `update-proto.yml` `tr` character class `tr -cd 'A-Za-z0-9:/?&=._-+~%#'` parses `_-+` as a range from `_` (0x5F) to `+` (0x2B), which GNU `tr` rejects with "reverse collating sequence order" and exits 1. The Summary step would crash on every auto-update run, breaking the entire `update-proto` pipeline that lands in master. Move the `-` to the end of the class so it's interpreted literally. ## VoIP P1s (5) bridge.ts — `#sendBatchEncryptedCall` no longer pushes the stanza when ANY destination failed to encrypt. Earlier the loop stripped `enc` from every destination on a single failure but then fell through to `sendCallStanza`, delivering a key-less offer that the peer couldn't decrypt — call setup failed silently. wasm-engine/instance.ts — three independent issues: (a) `#registerGlobalCallbacks` is no longer gated on a static flag. The map of listeners held closures over `this.#config.callbacks`, so a reconnect-after-disconnect routed events to handlers from the destroyed instance. `destroy()` now clears the static map and the init path always re-registers against the live instance. (b) `initVoipStack`'s outer catch no longer hides errors. The stack now records the failure in `#voipStackInitError`, and `waitForVoipStackReady()` re-throws it on the consumer side instead of silently resolving as "ready". (c) `#loadWasmModuleToWorker` arms a 30 s timeout that rejects the load promise. Without it, a worker that crashed before sending its `loaded` message left the engine init hanging forever. index.ts — `disconnect()` now detaches the `'CB:call'` and `'CB:receipt'` listeners it installed on `this.#sock.ws` before nulling the engine refs. Earlier the listeners outlived the engine, and an in-flight stanza would call into a torn-down `this.#engine`, throwing into the host process. ## JID-HOSTED-P2 (5 locations) bridge.ts (4) + index.ts (1) used `endsWith('@lid')` to gate LID handling, which silently rewrote `*.@hosted.lid` accounts (device 99) as PNs. Switched to `decoded.server` lookup in `bridge.ts` (the JID is already decoded right above each check) and an explicit `'@hosted.lid'` branch in `index.ts`. ## P2/P3 polish (4) lid-mapping-backend.ts (MDB-UNCACHED-P2) — `getAllLidsForPn` now uses a cached `selectAllLidsByPn` statement instead of compiling SQL per call. Same hot-path optimisation that was already in place for `deleteMapping`. env-utils.ts (ENV-TRIM-P2) — `intFromEnv` and `floatFromEnv` now `.trim()` before the empty-string check. A `KEY= ` env var with only whitespace used to slip past `=== ''` and `Number(' ')` returned 0, masquerading as a legitimate zero config. keys-with-jid-map.ts (COMMENT-BESTEFF-P3) — comment said the try/catch was "best-effort" but the catch arm always rethrows. Rewritten to match what the code actually does: the wrapper exists only to classify SQLITE_BUSY vs other errors. prometheus-metrics.ts (COMMENT-PORT-P3) — comment claimed `≥1024 unprivileged` but `intFromEnv(..., 1)` allows privileged ports too. Rewritten to acknowledge `min=1` is just "not zero / not negative" so operators running as root can bind to a privileged port if desired. ## Validation - `tsc --noEmit -p tsconfig.json` exit 0 - `eslint` on all 7 touched files: 0 errors after prettier autofix - Local jest: 2 pre-existing failures in `multi-db-backends.test.ts` and `multi-db-sqlite-auth-state.test.ts` are missing `better-sqlite3` native binding on Windows toolchain — CI Linux is the gate, those pass there. ## Customizations untouched Carousel, list, button, poll, view-once, biz quality_control, Lottie wrap, Meta AI msmsg, DSM, TC token, LID↔PN batched, Phase 9 multi-DB, useLegacyLock, schema migrations, memory leak fix — all unchanged. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/update-proto.yml | 6 ++- src/Utils/env-utils.ts | 16 ++++-- .../multi-db-sqlite/keys-with-jid-map.ts | 9 ++-- .../multi-db-sqlite/lid-mapping-backend.ts | 34 ++++++------ src/Utils/prometheus-metrics.ts | 8 +-- src/Voip/index.ts | 41 ++++++++++++--- src/Voip/signaling/bridge.ts | 33 ++++++++++-- src/Voip/wasm-engine/instance.ts | 52 +++++++++++++++++-- 8 files changed, 158 insertions(+), 41 deletions(-) diff --git a/.github/workflows/update-proto.yml b/.github/workflows/update-proto.yml index 0101ed3bfc0..02e949d7f93 100644 --- a/.github/workflows/update-proto.yml +++ b/.github/workflows/update-proto.yml @@ -259,7 +259,11 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY WA_VERSION=$(printf '%s' "$WA_VERSION_RAW" | tr -cd '0-9.') - WA_JS_URL=$(printf '%s' "$WA_JS_URL_RAW" | tr -cd 'A-Za-z0-9:/?&=._-+~%#') + # The `-` MUST be the last char in the class, otherwise `_-+` + # is parsed as the (invalid) range from `_` (0x5F) to `+` (0x2B) + # and GNU `tr` aborts with "reverse collating sequence order", + # breaking every Summary step run. + WA_JS_URL=$(printf '%s' "$WA_JS_URL_RAW" | tr -cd 'A-Za-z0-9:/?&=._+~%#-') PR_NUMBER=$(printf '%s' "$PR_NUMBER_RAW" | tr -cd '0-9') echo "**Version:** \`v$WA_VERSION\`" >> $GITHUB_STEP_SUMMARY diff --git a/src/Utils/env-utils.ts b/src/Utils/env-utils.ts index 5942635bcb3..2cf550ff7ee 100644 --- a/src/Utils/env-utils.ts +++ b/src/Utils/env-utils.ts @@ -26,8 +26,14 @@ * durations / pool sizes that must be strictly positive. */ export const intFromEnv = (raw: string | undefined, fallback: number, min: number = 0): number => { - if (raw === undefined || raw === '') return fallback - const n = Number(raw) + if (raw === undefined) return fallback + // Trim before the emptiness check — env vars containing only whitespace + // (e.g. a sloppy `KEY= ` in a .env file) used to slip past `=== ''` and + // fall through to `Number(' ')` which returns 0, masquerading as a + // legitimate "zero" config value. + const trimmed = raw.trim() + if (trimmed === '') return fallback + const n = Number(trimmed) return Number.isInteger(n) && n >= min ? n : fallback } @@ -36,7 +42,9 @@ export const intFromEnv = (raw: string | undefined, fallback: number, min: numbe * where fractional values are valid. */ export const floatFromEnv = (raw: string | undefined, fallback: number): number => { - if (raw === undefined || raw === '') return fallback - const n = Number(raw) + if (raw === undefined) return fallback + const trimmed = raw.trim() + if (trimmed === '') return fallback + const n = Number(trimmed) return Number.isFinite(n) ? n : fallback } diff --git a/src/Utils/multi-db-sqlite/keys-with-jid-map.ts b/src/Utils/multi-db-sqlite/keys-with-jid-map.ts index 28005e1b971..9e8e4f883a2 100644 --- a/src/Utils/multi-db-sqlite/keys-with-jid-map.ts +++ b/src/Utils/multi-db-sqlite/keys-with-jid-map.ts @@ -231,10 +231,11 @@ export function wrapKeysWithJidMap( if (hasRest) await inner.set(rest) - // Now persist jid_map changes. Wrapped in best-effort try/catch - // so a transient SQLITE_BUSY on the reverse-only path doesn't - // crash the caller — the missing mapping rebuilds on the next - // observed event. (audit P2-SQDB-02) + // Now persist jid_map changes. Wrapped in try/catch so the catch + // arm below can distinguish SQLITE_BUSY (re-raise so the upstream + // `runSetWithBusyRetry` can drive a backoff) from anything else + // (let it propagate). NOT best-effort — every error path + // rethrows; the wrapper is purely about classifying. try { if (deletes.length > 0) { for (const lidUser of deletes) jidMap.deleteMapping(lidUser) diff --git a/src/Utils/multi-db-sqlite/lid-mapping-backend.ts b/src/Utils/multi-db-sqlite/lid-mapping-backend.ts index 200b974d1f7..cbbc5713eb2 100644 --- a/src/Utils/multi-db-sqlite/lid-mapping-backend.ts +++ b/src/Utils/multi-db-sqlite/lid-mapping-backend.ts @@ -34,11 +34,15 @@ export class JidMapBackend { upsertMap: SqliteStatementLike selectPnByLid: SqliteStatementLike selectLidByPn: SqliteStatementLike - // audit MDB-P1-A — cached so `deleteMapping` doesn't `db.prepare()` - // per call. Earlier it compiled a fresh native statement per loop - // iteration; under burst delete that leaked native memory until V8 - // collected the JS wrapper. + // Cached so `deleteMapping` doesn't `db.prepare()` per call. Earlier + // it compiled a fresh native statement per loop iteration; under + // burst delete that leaked native memory until V8 collected the JS + // wrapper. deleteMapByLidRowId: SqliteStatementLike + // Same reasoning for `getAllLidsForPn` — used on every delete to + // enumerate historical mappings; the inline `db.prepare()` paid the + // native compile cost every call. + selectAllLidsByPn: SqliteStatementLike } private readonly db: SqliteDbLike @@ -110,7 +114,14 @@ export class JidMapBackend { 'JOIN jid j_pn ON j_pn._id = m.jid_row_id ' + 'WHERE j_pn.raw_string = ? ORDER BY m.sort_id DESC LIMIT 1' ), - deleteMapByLidRowId: this.db.prepare('DELETE FROM jid_map WHERE lid_row_id = ?') + deleteMapByLidRowId: this.db.prepare('DELETE FROM jid_map WHERE lid_row_id = ?'), + selectAllLidsByPn: this.db.prepare( + `SELECT l.raw_string AS raw FROM jid_map jm + JOIN jid l ON l._id = jm.lid_row_id + JOIN jid p ON p._id = jm.jid_row_id + WHERE p.raw_string = ? + ORDER BY jm.sort_id DESC` + ) } // Window-function variant of the "most recent LID per PN" pick. @@ -239,16 +250,9 @@ export class JidMapBackend { // 2-table join: lid_row_id → jid (LID side), jid_row_id → jid (PN side). // Earlier version had a third `JOIN jid j ON j._id = l._id` and read // `j.raw_string` — `j` was just a re-alias of `l`, harmless but a - // pointless extra lookup. - const rows = this.db - .prepare( - `SELECT l.raw_string AS raw FROM jid_map jm - JOIN jid l ON l._id = jm.lid_row_id - JOIN jid p ON p._id = jm.jid_row_id - WHERE p.raw_string = ? - ORDER BY jm.sort_id DESC` - ) - .all(pnUser) as Array<{ raw: string }> + // pointless extra lookup. Uses the cached statement from `this.stmts` + // so this hot delete path doesn't pay the SQL-compile cost per call. + const rows = this.stmts.selectAllLidsByPn.all(pnUser) as Array<{ raw: string }> return rows.map(r => r.raw) } diff --git a/src/Utils/prometheus-metrics.ts b/src/Utils/prometheus-metrics.ts index 04bc8b022a7..e0c1fd2e43e 100644 --- a/src/Utils/prometheus-metrics.ts +++ b/src/Utils/prometheus-metrics.ts @@ -169,9 +169,11 @@ function parseLabelsFromEnv(envValue: string | undefined): Labels { export function loadMetricsConfig(): MetricsConfig { return { enabled: (process.env.BAILEYS_PROMETHEUS_ENABLED ?? process.env.METRICS_ENABLED) === 'true', - // audit ENV-02: was `parseInt(... || '9092', 10)` — NaN under malformed - // env vars would land as `server.listen(NaN)` producing an opaque - // EADDRINUSE/bind error. Clamp to valid TCP range (≥1024 unprivileged). + // Was `parseInt(... || '9092', 10)` — NaN under malformed env vars + // would land as `server.listen(NaN)` producing an opaque + // EADDRINUSE/bind error. `min=1` is just "not zero / not negative" — + // we deliberately don't enforce `≥1024` because operators running as + // root may legitimately want a privileged port. port: intFromEnv(process.env.BAILEYS_PROMETHEUS_PORT ?? process.env.METRICS_PORT, 9092, 1), host: process.env.BAILEYS_PROMETHEUS_HOST || process.env.METRICS_HOST || '127.0.0.1', path: process.env.BAILEYS_PROMETHEUS_PATH || process.env.METRICS_PATH || '/metrics', diff --git a/src/Voip/index.ts b/src/Voip/index.ts index 4a03a26b089..1c3776d1a8d 100644 --- a/src/Voip/index.ts +++ b/src/Voip/index.ts @@ -452,17 +452,26 @@ export class VoipClient extends EventEmitter { // Direct binary-node hooks used for incoming stanza processing. In embedded // mode the socket exposes `.ws` (the underlying ws.WebSocket); in standalone // mode it's the socket the client just built. Both expose the same handle. + // Refs are stored so `disconnect()` can detach them — otherwise a stanza + // arriving after teardown would run against `#engine = null`. if (this.#sock.ws?.on) { - this.#sock.ws.on('CB:call', (node: any) => { - this.#signaling!.processIncomingCall(node, this.#engine!, this.#activeCall?.callId ?? '') - }) - this.#sock.ws.on('CB:receipt', (node: any) => { + this.#cbCallHandler = (node: any) => { + this.#signaling?.processIncomingCall(node, this.#engine!, this.#activeCall?.callId ?? '') + } + + this.#cbReceiptHandler = (node: any) => { if (!isCallReceiptNode(node)) return - this.#signaling!.processIncomingReceipt(node, this.#engine!, this.#activeCall?.callId ?? '') - }) + this.#signaling?.processIncomingReceipt(node, this.#engine!, this.#activeCall?.callId ?? '') + } + + this.#sock.ws.on('CB:call', this.#cbCallHandler) + this.#sock.ws.on('CB:receipt', this.#cbReceiptHandler) } } + #cbCallHandler: ((node: any) => void) | null = null + #cbReceiptHandler: ((node: any) => void) | null = null + /** * Subscribe to the socket's `'call'` event. When an offer arrives that we * haven't already surfaced (dedupe by call-id), construct an @@ -762,7 +771,11 @@ export class VoipClient extends EventEmitter { // does that internally from the per-participant LID. const resolved: string[] = [] for (const p of participants) { - if (p.endsWith('@lid')) { + // Both `@lid` and `@hosted.lid` are already resolved — only bare + // phone numbers need the LID lookup. The earlier `endsWith('@lid')` + // missed hosted-LID accounts (device 99) and tried to re-resolve + // them as if they were PNs. + if (p.endsWith('@lid') || p.endsWith('@hosted.lid')) { resolved.push(p) } else if (p.endsWith('@s.whatsapp.net')) { const lid = await this.#signaling.resolveLid(p) @@ -814,6 +827,20 @@ export class VoipClient extends EventEmitter { disconnect = (): void => { this.#activeCall?._forceEnd('disconnect') this.#activeCall = null + // Detach the direct ws CB hooks BEFORE we null out the engine — + // otherwise a stanza in flight when destroy() lands could invoke + // the handler against a torn-down engine and throw into the host + // process. We captured the refs at attach time so `removeListener` + // targets the exact closure (a fresh arrow would not match). + const ws: any = this.#sock?.ws + const off = ws?.off ?? ws?.removeListener + if (off) { + if (this.#cbCallHandler) off.call(ws, 'CB:call', this.#cbCallHandler) + if (this.#cbReceiptHandler) off.call(ws, 'CB:receipt', this.#cbReceiptHandler) + } + + this.#cbCallHandler = null + this.#cbReceiptHandler = null this.#relay?.closeAll() this.#engine?.destroy() // Only close the socket if we created it. In embedded mode the caller diff --git a/src/Voip/signaling/bridge.ts b/src/Voip/signaling/bridge.ts index 758ea8be2fc..40be1d4dcbe 100644 --- a/src/Voip/signaling/bridge.ts +++ b/src/Voip/signaling/bridge.ts @@ -273,6 +273,7 @@ export class SignalingBridge { const encCount = parseCountAttr(rootEnc?.attrs.count) let includeDeviceIdentity = false + let encryptionFailed = false for (const destNode of destinations) { const targetJid = String(destNode.attrs.jid ?? '').trim() const destEnc = getBinaryNodeChild(destNode, 'enc') @@ -283,10 +284,18 @@ export class SignalingBridge { setNodeChildren(destNode, [encrypted.encNode]) } catch { for (const d of destinations) removeNodeChildrenByTag(d, 'enc') + encryptionFailed = true break } } + // If ANY destination failed to encrypt, we already stripped the + // `enc` children from every destination — pushing the stanza now + // would deliver a key-less offer that the peer can't decrypt, + // failing the call setup silently. Bail instead so the upstream + // caller can surface the failure. + if (encryptionFailed) return + if (includeDeviceIdentity) this.#appendDeviceIdentity(voipNode) await this.#sendCallStanza(this.#toBareJid(peerJid), voipNode, signalingTag, effectivePeerJid, peerJid) @@ -590,7 +599,11 @@ export class SignalingBridge { const { jidDecode, jidEncode } = this.#baileys const decoded = jidDecode(jid) if (!decoded?.user) return jid - const server = jid.endsWith('@lid') ? 'lid' : 's.whatsapp.net' + // `decoded.server` already captures the parsed domain — using it + // avoids the previous `endsWith('@lid')` check that misclassified + // hosted-LID accounts (`*.@hosted.lid`, device 99) as PNs and + // silently rewrote them to `@s.whatsapp.net`. + const server = decoded.server === 'lid' || decoded.server === 'hosted.lid' ? decoded.server : 's.whatsapp.net' return jidEncode(decoded.user, server) } @@ -598,7 +611,11 @@ export class SignalingBridge { const { jidDecode, jidEncode } = this.#baileys const decoded = jidDecode(jid) if (!decoded?.user) return jid - const server = jid.endsWith('@lid') ? 'lid' : 's.whatsapp.net' + // `decoded.server` already captures the parsed domain — using it + // avoids the previous `endsWith('@lid')` check that misclassified + // hosted-LID accounts (`*.@hosted.lid`, device 99) as PNs and + // silently rewrote them to `@s.whatsapp.net`. + const server = decoded.server === 'lid' || decoded.server === 'hosted.lid' ? decoded.server : 's.whatsapp.net' if (decoded.device == null) return jidEncode(decoded.user, server) return `${decoded.user}:${decoded.device}@${server}` } @@ -609,7 +626,11 @@ export class SignalingBridge { if (!decoded?.user) return undefined const device = decoded.device if (device == null || device === 0) return undefined - const server = jid.endsWith('@lid') ? 'lid' : 's.whatsapp.net' + // `decoded.server` already captures the parsed domain — using it + // avoids the previous `endsWith('@lid')` check that misclassified + // hosted-LID accounts (`*.@hosted.lid`, device 99) as PNs and + // silently rewrote them to `@s.whatsapp.net`. + const server = decoded.server === 'lid' || decoded.server === 'hosted.lid' ? decoded.server : 's.whatsapp.net' return jidEncode(decoded.user, server) } @@ -675,7 +696,11 @@ export class SignalingBridge { continue } - const server = jid.endsWith('@lid') ? 'lid' : 's.whatsapp.net' + // `decoded.server` already captures the parsed domain — using it + // avoids the previous `endsWith('@lid')` check that misclassified + // hosted-LID accounts (`*.@hosted.lid`, device 99) as PNs and + // silently rewrote them to `@s.whatsapp.net`. + const server = decoded.server === 'lid' || decoded.server === 'hosted.lid' ? decoded.server : 's.whatsapp.net' result.add(jidEncode(decoded.user, server)) if (decoded.device != null) { result.add(`${decoded.user}:${decoded.device}@${server}`) diff --git a/src/Voip/wasm-engine/instance.ts b/src/Voip/wasm-engine/instance.ts index 9757a8fb4f3..bd15b2fcda3 100644 --- a/src/Voip/wasm-engine/instance.ts +++ b/src/Voip/wasm-engine/instance.ts @@ -325,6 +325,7 @@ export class WasmEngine { #voipStackInitialized = false #voipStackInitPromise: Promise | null = null + #voipStackInitError: Error | null = null #voipReadyResolver: (() => void) | null = null #voipReadyPromise: Promise | null = null @@ -432,7 +433,13 @@ export class WasmEngine { throw new Error(`No compatible WASM loader found. Tried: ${loaderModuleNames.join(', ')}`) } - if (!WasmEngine.#globalCallbacksRegistered) this.#registerGlobalCallbacks() + // Always re-register: each `WasmEngine` instance carries its own + // closures (over `this.#config.callbacks`). After a disconnect→ + // reconnect cycle the static listener map still held closures over + // the destroyed instance, so events from the fresh engine were + // routed to dead handlers. `destroy()` now clears the map; init + // repopulates it for the live instance. + this.#registerGlobalCallbacks() await this.#initPThreadPool() const workersLoadingPromise = this.#loadWasmModuleToAllWorkers() @@ -472,12 +479,22 @@ export class WasmEngine { this.#wasmModule = null this.#wasmMemory = null this.#initialized = false + // Clear closures that captured `this.#config.callbacks` so a later + // re-init can register its own listeners against the live instance. + // Same scope as the listeners themselves (process-wide singleton); + // no other live engine to step on in practice. + WasmEngine.#globalCallbackListeners.clear() + WasmEngine.#globalCallbacksRegistered = false } initVoipStack = (selfJid: string, meUserJid: string, selfLid: string): void => { this.#ensureInitialized() if (this.#voipStackInitialized || this.#voipStackInitPromise) return + // Clear any error from a previous failed init so a fresh attempt + // can be observed cleanly. + this.#voipStackInitError = null + this.#voipStackInitPromise = new Promise(resolveInit => { this.#voipReadyPromise = new Promise(readyResolve => { this.#voipReadyResolver = () => { @@ -516,7 +533,13 @@ export class WasmEngine { this.#voipStackInitPromise = null resolveInit() }) - } catch { + } catch (initErr: any) { + // Stash the error so `waitForVoipStackReady` can re-throw it + // instead of pretending init succeeded. Earlier this swallowed + // the failure and `waitForVoipStackReady()` returned cleanly, + // leaving the stack in a "ready but actually broken" state — + // downstream call setup would then crash with cryptic errors. + this.#voipStackInitError = initErr instanceof Error ? initErr : new Error(String(initErr)) this.#voipReadyResolver = null this.#voipReadyPromise = null this.#voipStackInitPromise = null @@ -526,12 +549,18 @@ export class WasmEngine { } waitForVoipStackReady = async (): Promise => { - if (this.#voipStackInitialized) return + if (this.#voipStackInitialized) { + if (this.#voipStackInitError) throw this.#voipStackInitError + return + } + if (this.#voipStackInitPromise) { await this.#voipStackInitPromise } else { await new Promise(r => setTimeout(r, 100)) } + + if (this.#voipStackInitError) throw this.#voipStackInitError } isVoipStackReady = (): boolean => this.#voipStackInitialized @@ -1110,7 +1139,19 @@ export class WasmEngine { #loadWasmModuleToWorker = (worker: NodeWorkerMessagePort): Promise => new Promise((resolve, reject) => { + // Worker can crash before sending `loaded` (OOM, native segfault, + // `importScripts` failure). Without a timeout the outer engine + // init would hang indefinitely. 30 s is generous — a healthy + // load usually finishes in single-digit seconds. + const WORKER_LOAD_TIMEOUT_MS = 30_000 + let timer: NodeJS.Timeout | null = null + const cleanup = (): void => { + if (timer) { + clearTimeout(timer) + timer = null + } + worker.removeMessageListener('cmd', loadedHandler) worker.removeMessageListener('cmd', errorHandler) } @@ -1134,6 +1175,11 @@ export class WasmEngine { } } + timer = setTimeout(() => { + cleanup() + reject(new Error(`VoIP worker WASM load timed out after ${WORKER_LOAD_TIMEOUT_MS}ms`)) + }, WORKER_LOAD_TIMEOUT_MS) + worker.addMessageListener('cmd', loadedHandler) worker.addMessageListener('cmd', errorHandler) worker.workerID = this.#nextWorkerID++ From 92fd10233273fd8a12810144b4a94124e3e870bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:28:39 -0300 Subject: [PATCH 21/23] chore: update proto/version to v2.3000.1041433777 (#547) Co-authored-by: rsalcara --- WAProto/WAProto.proto | 2 +- src/Defaults/baileys-version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WAProto/WAProto.proto b/WAProto/WAProto.proto index 4a38feff0d2..b9d505e5f13 100644 --- a/WAProto/WAProto.proto +++ b/WAProto/WAProto.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package proto; -/// WhatsApp Version: 2.3000.1041410692 +/// WhatsApp Version: 2.3000.1041433777 message ADVDeviceIdentity { optional uint32 rawId = 1; diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index b23951490a2..f63e58c799d 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1041420778]} +{"version": [2, 3000, 1041433777]} From f7b4127d269610ad60b08f5b599ff4bacb8d749d Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 14 Jun 2026 07:08:39 -0300 Subject: [PATCH 22/23] chore: update WhatsApp Web version to v2.3000.1041437765 (#548) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index f63e58c799d..a85cc031c88 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version": [2, 3000, 1041433777]} +{"version":[2,3000,1041437765]} From 53cbf6399ca0653ee6344c3dfee052cd01647ba2 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 14 Jun 2026 11:30:57 -0300 Subject: [PATCH 23/23] fix(audit r6): FU-04 varint 5-byte overflow + FU-06 port upper-bound + FU-03 reverse-delete forward cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-6 audit of PR #546 surfaced 9 follow-up items. After case-by-case validation, 3 are real and acionable, 6 are intentional non-fixes — documented below. ## Applied (3) FU-04 — `libsignal.ts` varint 5-byte overflow The earlier round-5 fix rejected 6-byte varints via `shift >= 35`. This closes the remaining loophole: a 5-byte varint whose 5th byte has the top nibble set (`byte & 0xf0`) encodes a value > 2³² – 1 that `>>> 0` silently masks into a valid-looking uint32. Reject it explicitly at `shift === 28` before the bitwise-or runs. FU-06 — `prometheus-metrics.ts` port upper-bound `intFromEnv` gained an optional `max` parameter (default `Number.MAX_SAFE_INTEGER`, so existing call sites are unaffected). Prometheus port now passes `max=65535` so a `BAILEYS_PROMETHEUS_PORT=99999` falls back to the default `9092` instead of reaching `server.listen()` with an out-of-range integer. FU-03 — `keys-with-jid-map.ts` reverse-delete forward cleanup Reverse delete (`${lid}_reverse → null`) wiped the typed `jid_map` row and the inner store's `_reverse` entry but left any legacy forward entry `pnUser → lidUser` in the inner store. A subsequent `inner.get('lid-mapping', [pnUser])` would then resurrect the just-deleted LID via the fallback path. We now resolve the PN synchronously via `jidMap.getPnForLid(lidUser)` and queue its forward delete on the same pass. ## NOT applied (with reasons) FU-01 — pinning `actions/setup-node@v4` etc. to immutable SHAs The project's policy treats first-party GitHub Actions (`actions/*`) as trusted via major tag and pins only third-party actions by SHA (see `meeDamian/github-release@7ae19492...` in publish-release.yml). Changing only `setup-node` would be inconsistent. If the policy changes, this should be applied to all `actions/*` references in one pass — out of scope for this release. FU-02 / FU-05 — `audio-feeder.ts` emission loop + `disconnect()` ffmpeg Both touch the VoIP audio-capture lifecycle. The current behaviour (loop stops with ffmpeg, child process left to its natural exit) is intentional for the silence-source path and the finite-source path has a documented limitation. A proper fix needs E2E coverage of the capture state machine first — left as a tracked follow-up. FU-07 — `worker-modules.js` flatMap polyfill + WebCodecs check `worker-modules.js` is vendored verbatim from WhatsApp Web's bundle (Meta source). Patching it would create permanent divergence and break our ability to refresh the bundle on schema bumps. The `worker-bootstrap.ts` shim already polyfills `window`, so the WebCodecs throw doesn't fire in practice. FU-08 — `relay-transport.ts:327` early-packet migration Migrating buffered packets from a placeholder connection to the canonical connection after `updateRelayList()` is a multi-week redesign of the relay handshake. Edge case in early-init; deferred. FU-09 — `worker-bootstrap.ts:445` `importScripts` CWD fallback Only fires when the vendored loader.js calls `importScripts` with a relative URL — never observed in practice in the WASM bundle we ship. MAC stream verification at `final()` only — design trade-off Documented in `messages-media.ts`. Buffering the entire decrypted payload until the MAC verifies would defeat the purpose of streaming downloads. The PR moved the project from "no MAC check at all" to "MAC check at final()", which is a strict improvement. A "verify-before-emit" mode could be added later as opt-in for callers who can afford the buffering. ## Validation - `tsc --noEmit -p tsconfig.json` exit 0 - `eslint` on touched files: 0 errors - `intFromEnv` change is backward-compatible (new `max` defaults to `MAX_SAFE_INTEGER`); all existing call sites (auth, retry, batching) retain their behaviour. ## Customizations untouched Carousel, list, button, poll, view-once, biz quality_control, Lottie wrap, Meta AI msmsg, DSM, TC token, LID↔PN batched, Phase 9 multi-DB, useLegacyLock, schema migrations, memory leak fix — unchanged. Co-Authored-By: Claude Opus 4.7 --- src/Signal/libsignal.ts | 8 ++++++++ src/Utils/env-utils.ts | 17 +++++++++++++---- src/Utils/multi-db-sqlite/keys-with-jid-map.ts | 10 ++++++++++ src/Utils/prometheus-metrics.ts | 9 +++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Signal/libsignal.ts b/src/Signal/libsignal.ts index 52f13b2daab..ac25577622c 100644 --- a/src/Signal/libsignal.ts +++ b/src/Signal/libsignal.ts @@ -297,6 +297,14 @@ function readVarint(buffer: Uint8Array, offset: number): { value: number; nextOf while (offset < buffer.length) { const byte = buffer[offset]! + // On byte 5 (shift === 28) only the low 4 bits can fit inside a + // 32-bit varint — the top 4 bits would land at positions 32–35 and + // silently fall off the uint32. `>>> 0` masks the overflow without + // signalling it, so a crafted payload could encode a value + // arbitrarily larger than 2³² – 1 and still parse cleanly. Reject + // it the same way the 6-byte case is rejected by the `shift >= 35` + // guard below. + if (shift === 28 && (byte & 0xf0) !== 0) return undefined result |= (byte & 0x7f) << shift offset++ diff --git a/src/Utils/env-utils.ts b/src/Utils/env-utils.ts index 2cf550ff7ee..fb87ac81703 100644 --- a/src/Utils/env-utils.ts +++ b/src/Utils/env-utils.ts @@ -17,15 +17,24 @@ */ /** - * Parse an integer env var with a fallback and lower bound. + * Parse an integer env var with a fallback and lower/upper bounds. * * @param raw - Raw env var value (`process.env.X`). * @param fallback - Returned if `raw` is missing, empty, non-numeric, or - * below `min`. + * outside `[min, max]`. * @param min - Minimum acceptable value (default `0`). Pass `1` for * durations / pool sizes that must be strictly positive. + * @param max - Maximum acceptable value (optional). Use for port numbers + * (65535), percentages (100), etc. so out-of-range values + * fall back to the safe default instead of reaching the + * underlying syscall with a meaningless number. */ -export const intFromEnv = (raw: string | undefined, fallback: number, min: number = 0): number => { +export const intFromEnv = ( + raw: string | undefined, + fallback: number, + min: number = 0, + max: number = Number.MAX_SAFE_INTEGER +): number => { if (raw === undefined) return fallback // Trim before the emptiness check — env vars containing only whitespace // (e.g. a sloppy `KEY= ` in a .env file) used to slip past `=== ''` and @@ -34,7 +43,7 @@ export const intFromEnv = (raw: string | undefined, fallback: number, min: numbe const trimmed = raw.trim() if (trimmed === '') return fallback const n = Number(trimmed) - return Number.isInteger(n) && n >= min ? n : fallback + return Number.isInteger(n) && n >= min && n <= max ? n : fallback } /** diff --git a/src/Utils/multi-db-sqlite/keys-with-jid-map.ts b/src/Utils/multi-db-sqlite/keys-with-jid-map.ts index 9e8e4f883a2..45e834743f6 100644 --- a/src/Utils/multi-db-sqlite/keys-with-jid-map.ts +++ b/src/Utils/multi-db-sqlite/keys-with-jid-map.ts @@ -191,6 +191,16 @@ export function wrapKeysWithJidMap( // Reverse delete — keyed by LID directly. deletes.push(lidUser) innerDeleteReverse.push(key) // include the `_reverse` suffix + // Also clean up the FORWARD direction in the inner store. + // Reverse-only delete was leaving any legacy `pnUser → + // lidUser` entry intact in the inner store, so a later + // `inner.get('lid-mapping', [pnUser])` would resurrect the + // just-deleted LID via the fallback path. Resolve the PN + // from the typed backend (synchronous) and queue its + // forward delete too. + const resolvedPn = jidMap.getPnForLid(lidUser) + if (resolvedPn) innerDeleteForward.push(resolvedPn) + continue } diff --git a/src/Utils/prometheus-metrics.ts b/src/Utils/prometheus-metrics.ts index e0c1fd2e43e..24ef16cbb13 100644 --- a/src/Utils/prometheus-metrics.ts +++ b/src/Utils/prometheus-metrics.ts @@ -171,10 +171,11 @@ export function loadMetricsConfig(): MetricsConfig { enabled: (process.env.BAILEYS_PROMETHEUS_ENABLED ?? process.env.METRICS_ENABLED) === 'true', // Was `parseInt(... || '9092', 10)` — NaN under malformed env vars // would land as `server.listen(NaN)` producing an opaque - // EADDRINUSE/bind error. `min=1` is just "not zero / not negative" — - // we deliberately don't enforce `≥1024` because operators running as - // root may legitimately want a privileged port. - port: intFromEnv(process.env.BAILEYS_PROMETHEUS_PORT ?? process.env.METRICS_PORT, 9092, 1), + // EADDRINUSE/bind error. `min=1` is just "not zero / not negative" + // (operators running as root may legitimately bind a privileged + // port). `max=65535` rejects values above the TCP range before + // they reach `server.listen()`. + port: intFromEnv(process.env.BAILEYS_PROMETHEUS_PORT ?? process.env.METRICS_PORT, 9092, 1, 65535), host: process.env.BAILEYS_PROMETHEUS_HOST || process.env.METRICS_HOST || '127.0.0.1', path: process.env.BAILEYS_PROMETHEUS_PATH || process.env.METRICS_PATH || '/metrics', prefix: process.env.BAILEYS_PROMETHEUS_PREFIX || process.env.METRICS_PREFIX || 'baileys',