* fix: unwrap viewOnceMessage in getMediaType so enc node gets mediatype attribute
Without this fix, view-once media (image/video/audio) was sent without
mediatype="image/video/audio" on the enc node because getMediaType only
checked the top-level message fields. The viewOnceMessage wrapper hides
the inner imageMessage, causing mediatype to be empty and WA servers to
silently drop the message on the recipient side.
Address remaining Copilot review comments on PR #306:
- Import RetryReason enum from Utils (already re-exported via Utils/index.ts)
- Replace all numeric literals (0/1/2/3/4/7) in retryErrorCode with the
corresponding enum members (UnknownError / SignalErrorNoSession / etc.)
- Removes inline comments that were only needed to explain magic numbers
- If RetryReason values ever change, the compiler will surface the drift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Address Copilot review comments on PR #306:
1. Replace ad-hoc regexes with the canonical DECRYPTION_RETRY_CONFIG error
lists from decode-wa-message.ts (single source of truth). Any future
additions to those lists are automatically picked up here.
2. Add coverage for MessageCounterError ('Key used already or never filled')
which was previously returning code 0. It now correctly maps to code 4
(SignalErrorInvalidMessage) via corruptedSessionErrors.
3. Broaden session-error matching to also catch libsignal variants like
'No sessions', 'No open session', 'No sessions available' via the
/no (open )?sessions?/ regex alongside sessionRecordErrors.
4. Fix comment: clarify that code 7 = BadMac and code 4 = InvalidMessage
are distinct codes (previous comment conflated them).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three issues corrected in sendRetryRequest (receiver side):
1. BUG — error code hardcoded as '0' (UnknownError) in retry receipt.
The peer (especially another InfiniteAPI instance) could not detect
the failure type and would fall back to the 1-hour session recreation
timeout instead of recreating immediately.
Fix: derive error code from the actual libsignal decryption error
message stored in messageStubParameters[0].
2. BUG — shouldRecreateSession block was reading the wrong node: it
called getBinaryNodeChild(node, 'retry') on the incoming bad-MAC
message, which never has a <retry> child. errorCode was always
undefined, so MAC_ERROR_CODES never matched — the logic was dead.
3. BUG (consequence of #2) — when shouldRecreateSession accidentally
did fire (no-session or 1-hour timeout), it deleted the receiver's
session BEFORE the sender's pkmsg arrived. This opened a race window
where concurrent messages would fail with "No Session".
Fix: remove the entire session-deletion block from sendRetryRequest.
The Signal Protocol pkmsg automatically overwrites the corrupted
session — no explicit delete needed on the receiver side.
WA Desktop reference (CDP capture 2026-03-19):
- Retry receipt sent ~99ms after Bad MAC with specific error code
- pkmsg arrives ~304ms later, new session established automatically
- Old session is never deleted; pkmsg overwrites it implicitly
- Full recovery in ~1.3s without any race window
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- history.ts: add Origin: DEFAULT_ORIGIN to CDN DELETE request headers —
the download path injects it internally but options does not carry it
- socket.ts: fix lowServerCount comparison — was preKeyCount <= topUpAmount
which triggered uploads even when above MIN_PREKEY_COUNT (e.g. 250 prekeys
→ topUp=550 → 250<=550=true → unnecessary upload). Now uses correct
threshold: preKeyCount < MIN_PREKEY_COUNT
- socket.ts: guard scheduleNextKeepAlive() with !closed check to prevent
orphan timers when end() was called concurrently on a different path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uploadPreKeys() waited for a concurrent upload but then proceeded to upload
again. With top-up logic and count=0, both handleEncryptNotification and
uploadPreKeysToServerIfRequired fire simultaneously, both see count=0, first
wins and uploads 800, second waits then uploads another 800 → 1600 prekeys.
Fix: return immediately after awaiting the concurrent upload — it already
replenished the pool.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: histsync LID improvements — raw_id mapping, prekeys, keepalive jitter, CDN DELETE
* fix: prekey pool strategy — 800 initial, top-up to 800 when below 200
- INITIAL_PREKEY_COUNT: 812 → 800 (rounded, matches WA Business ~812 from CDP capture)
- MIN_PREKEY_COUNT: 25 → 200 (replenishment trigger threshold)
- uploadPreKeysToServerIfRequired: top-up to INITIAL_PREKEY_COUNT instead of
uploading a flat MIN_PREKEY_COUNT — restores full 800-key pool on each replenish
- handleEncryptNotification: same top-up logic (INITIAL_PREKEY_COUNT - count)
so server notification path also restores to 800, not just adds 200
uploadPreKeys(5) in error recovery path intentionally left unchanged.
- Add interactive message detection helpers (getButtonType, isCarouselMessage, etc.)
- Inject biz node with interactive/native_flow for non-carousel interactive messages
- Inject biz + quality_control with decision_id for carousel messages
- Skip bot node for native_flow/carousel/catalog (breaks Web rendering)
- Force device-identity inclusion for carousel messages
- Append deferred biz/bot/quality_control nodes after device-identity and tctoken
- Store tctoken under both LID and PN for reliable lookup
Two fixes:
1. Stanza type="media" for carousel messages — WhatsApp Web needs this
to render carousel in real-time. Without it, only shows header until
page refresh (F5).
2. Fix jimp detection: typeof Jimp === 'function' (not just 'object')
so jpegThumbnail is generated for card images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7 corrections based on reverse-engineering of WhatsApp Business Android:
1. resolveCanonicalJid: PN→LID resolution for transaction lock keys (prevent race conditions)
2. Retain PN session during LID migration (avoid No Session errors)
3. Delayed PreKey deletion with 5-min grace period (prevent Invalid PreKey ID races)
4. Surgical session cleanup: only delete the specific corrupted device, not all devices
5. Identity dual storage: save identity key in both LID and PN addresses
6. MAC error cooldown reduced 10s→1s (faster recovery, aligned with WABA)
7. Allow session recreation on first retry (retryCount >= 1 instead of > 1)
* fix: remove unused jidDecode import from decode-wa-message.ts
* fix: add try/catch to delayed PreKey deletion to prevent unhandled rejection
The setTimeout async callback in removePreKey could cause unhandled promise
rejection if the keystore was destroyed (connection closed) before the
5-min grace period expired.
The web protocol (WA\x06\x03) requires a web-compatible UserAgent.
Using SMB_ANDROID in the UserAgent caused pair code registration to
fail with "cannot connect device" even though the phone confirmation
appeared. The Android identity is now only set in DeviceProps.platformType
(ANDROID_PHONE) in the registration node, which correctly determines
the display name in "Linked Devices" as "Android (14)".
Changes:
- getUserAgent(): always returns MACOS platform and Desktop device
- getClientPayload(): always includes webInfo (no longer skipped for Android)
- Remove unused isAndroidBrowser import from validate-connection.ts
- Update pair code comments documenting tested platform IDs
- Add structured logging to pair code request
Single-line logs showing how the lib presents to WhatsApp:
- 📱/🖥️ Connection: phone number, platform, device, type
- 🔗 Pair code: companion platform, android override status
* fix: move connection log to CB:success and rename type to platformType
Move the presentation log from pre-handshake (validateConnection) to
CB:success where the connection is actually confirmed. Rename ambiguous
'type' label to 'platformType' matching DeviceProps.PlatformType.
* feat: add Android browser preset for companion device registration
Add Browsers.android(apiLevel) preset that identifies as an Android
companion device (SMB_ANDROID platform, ANDROID_PHONE device type).
Validated against Frida captures of real WhatsApp Android protocol.
* feat: support BAILEYS_BROWSER env var for browser selection
Allows selecting Android companion mode via environment variable
without code changes. Set BAILEYS_BROWSER=android (or android:15
for a specific API level) to register as an Android device.
* fix: auto-detect Android browser in pair code and fall back to Chrome
Pair code companion registration fails with Android browser because the
WA server rejects the ANDROID_PHONE companion_platform_id over the web
Noise protocol. When Android is detected, requestPairingCode() now
transparently uses Chrome/macOS platform values for the pair code stanza
while QR code pairing continues to work as Android (SMB_ANDROID).
* docs: add android to isValidBrowserPreset JSDoc
* fix: break infinite Bad MAC + session recreation loop
Remove aggressive session cleanup from the decryption hot path that caused
cascading failures when multiple messages from the same contact arrived
simultaneously. The Signal Protocol naturally recovers via retry+pkmsg flow.
Changes:
- Remove cleanupCorruptedSession() from per-message error handler (hot path)
- Move session cleanup to retry-exhausted handler as safety net
- Add per-JID retry request deduplication (5s window)
- Add 10s cooldown on MAC error session recreation
- Export cleanupCorruptedSession/getDecryptionJid for use in messages-recv
Tested: corrupted session with fake keys → Bad MAC → retry receipt →
pkmsg recovery → all 7 messages delivered, zero infinite loop.
* fix: address PR review — lint, dedup scope, and cleanup targeting
- Remove unused `autoCleanCorrupted` param from decryptMessageNode (lint fix)
- Scope retry dedup by participant JID in group chats (not group JID)
- Move dedup registration after early-return checks to avoid blocking
subsequent messages when max retries reached
- Use participant JID for session cleanup in groups (Signal sessions
are per-participant, not per-group)
* feat: centralized LID→PN normalization for all emitted events
WhatsApp's server increasingly uses LID (Linked ID) as the primary
addressing format. Consumers expect phone numbers (PN),
not opaque LID identifiers. This adds comprehensive LID→PN resolution
across all ev.emit() paths so downstream consumers always receive PN.
Key changes:
- Add resolveLidToPn() and normalizeKeyLidToPn() centralized helpers
in process-message.ts for consistent LID→PN resolution
- normalizeMessageJids() fast path: use alt JID directly from stanza
attributes (zero I/O, eliminates race condition with LIDMappingStore)
- Await storeLIDPNMappings() before normalization in message receipt
flow (was fire-and-forget, could race with subsequent getPNForLID)
- Normalize LID→PN in all event emission points:
* messages.upsert (keys, nested reaction/poll keys, participantAlt)
* presence.update (jid + participant)
* message-receipt.update (key + userJid)
* contacts.update (picture notifications)
* blocklist.update (blocklist JIDs)
* call events (chatId, from)
* group metadata (participants, owner, subjectOwner)
* group notifications (acting participant, add/remove/promote/demote)
* newsletter notifications (author, user JIDs)
* sync actions (mutation index normalization)
- Make handlePresenceUpdate and handleGroupNotification async to
support await on LID resolution
- Add normalizeGroupMetadata() helper in groups.ts for all
extractGroupMetadata call sites
Performance: ~0.01ms per message (LRU cache hit). No impact on
message sending. First-contact resolution ~2-5ms (one-time per contact).
* fix: normalize LID→PN in handleBadAck, media retry, and PDO recovery
Additional leak points found during final audit:
- handleBadAck: key.remoteJid from ack stanza could be LID
- messages.media-update: media retry key JIDs not normalized
- CTWA PDO recovery: webMessageInfo.key from phone response not normalized
* fix: address PR review — parallelize LID resolution, use helper consistently
- normalizeGroupMetadata: resolve participant LIDs with Promise.all
instead of sequential loop (perf on large groups)
- handleCall: resolve participant JIDs with Promise.all
- handleBadAck: use normalizeKeyLidToPn() helper instead of manual
resolveLidToPn + assignment (consistency with other code paths)
- CB:relay: resolve callCreator LID→PN before emitting