- 3841abca35 / ad5ea817f7: WA version updates — applied separately via our own version bumps
- d077902695: WA version update — same as above
- c4e5d1262a: Fix ack handling — high-risk 2718-line diff in messages-recv.ts, intentionally skipped
- 802d5f6efa: Fix mentions regression — already covered by existing rsalcara contextInfo logic
- 6afde71691: feat groups mentionAll — applied in previous commit
- Add mentionAll?: boolean to Mentionable type (upstream 6afde71691)
- Set contextInfo.nonJidMentions=1 for @everyone group mentions
- Use viewOnceMessageV2 (field 55) wrapper instead of legacy viewOnceMessage (field 37)
- Set viewOnce=true on inner imageMessage/videoMessage/audioMessage
- Route view-once media uploads to /o1/mms/* CDN via mediaTypeOverride
Co-Authored-By: Renato Alcará
Full rewrite covering all 8 interactive message types:
1. Menu Texto
2. Botões Quick Reply (tested up to 16 buttons)
3. Botões CTA — URL, Copy, Call
4. Lista Dropdown (10 sections × 3 rows = 30 items max)
5. Enquete/Poll
6. Apenas Botões Reply sem CTA
7. Apenas CTAs sem Quick Reply
8. Carrossel com Imagens (up to 10 cards)
Each type includes:
- Complete curl example with realistic data
- All parameters documented with types and limits
- Webhook payload structure received on click/select
- JavaScript helpers to parse each response type
Also includes:
- Platform compatibility table (Android / iOS / WA Web)
- Universal webhook router for all interactive response types
- Complete limits table per message type
- Account restrictions (hosted vs non-hosted)
Co-Authored-By: Renato Alará
Accounts registered as hosted (Meta Business Cloud API) have view-once
blocked by the WA server for all clients (Android, iOS, Web).
This is a server-side policy with no code workaround.
Added prominent warning block in section 1 and dedicated row in the
limitations table.
Co-Authored-By: Renato Alcará
Complete guide for the view-once (visualização única) feature:
- Send examples (curl) for image, video and audio
- Webhook payload structure with key.isViewOnce detection
- JavaScript helpers to identify and access view-once media
- Frontend/CRM rendering guide with placeholder after open
- Implemented code with inline explanations
- Full send/receive flow diagram
- Known limitations and validation checklist
Co-Authored-By: Renato Alcara
* 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.
* fix: detect view-once media (image/video/audio) on stanza 1 for linked devices
When a view-once arrives at a linked device (InfiniteAPI), the server sends:
- Stanza 1: enc payload with full media metadata (mediaKey, directPath) wrapped in
viewOnceMessage > imageMessage/videoMessage/audioMessage with viewOnce: true
- Stanza 2: unavailable view_once fanout placeholder (already handled)
Previously only stanza 2 set key.isViewOnce = true. Stanza 1 was emitted as
plain media with no view-once indicator, making it indistinguishable from regular
media on the consumer side (messages.upsert).
This fix inspects the decrypted proto for viewOnceMessage (v1/v2/v2Ext) wrappers
containing a media message with viewOnce=true and propagates key.isViewOnce=true.
The viewOnceMessage wrapper is also used for interactive messages (carousel,
buttons, lists) but those carry interactiveMessage/listMessage inside -- never
imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce.
The distinction is unambiguous and does not affect interactive message handling.
Verified via WA Desktop
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>
- history.ts: move CDN DELETE from downloadHistory() to
downloadAndProcessHistorySyncNotification(), after processHistoryMessage()
succeeds — prevents permanent history loss if processing throws post-download
- history.ts: fix fetch spread order: { ...options, method: 'DELETE' } so
options.method cannot shadow the intended DELETE verb
- socket.ts: add early return after void end() in onKeepAliveTick to prevent
scheduleNextKeepAlive() from leaking a timer while connection is closing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. USyncQuery: extract raw_id attr from node attrs into rawId field; implement
side_list parsing (was TODO/commented) reusing shared parseNodeList helper
2. getUSyncDevices: add 4th LID→PN source via raw_id pairing from device-list
WA Business sends zero phoneNumberToLidMappings in HistorySync — raw_id is
the only way to resolve LID↔PN for those accounts during message send
3. MIN_PREKEY_COUNT: 5 → 25 (WA Business maintains ~812; 5 was too low a buffer
before triggering replenishment upload)
4. startKeepAliveRequest: replace fixed setInterval with recursive setTimeout
+ ±15% jitter, matching WA Desktop's ~25-30s variable heartbeat pattern;
clearInterval → clearTimeout for the handle
5. downloadHistory: fire-and-forget DELETE to CDN after successful download,
mirroring WA Desktop behaviour (server-side one-time file cleanup)
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.
Two bugs fixed compared to prior implementation:
1. history.ts — normalize userJid LID→PN
pastParticipants[].userJid may arrive as a LID identifier (e.g. "46802258641027@lid").
The consumer expects a phone number. After building the lidPnMap from conversations
and phoneNumberToLidMappings, each userJid is resolved to its PN equivalent.
If the mapping is not available the original value is preserved (no data loss).
2. event-buffer.ts — deduplicate by groupJid + userJid across chunks
Multiple HistorySync chunks can contain the same group. The previous approach
concatenated blindly, producing duplicate entries. Now a keyed map
{ [groupJid]: IPastParticipant[] } is used so each group appears once and
each participant within a group appears at most once.
Types updated (Events.ts):
- messaging-history.set event includes pastParticipants?: IPastParticipants[]
- BufferedEventData.historySets.pastParticipants typed as keyed map
- 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