feat(chat-media): archive media this account sent, behind CHAT_MEDIA_ARCHIVE_OUTBOUND - #1182
Merged
Merged
Conversation
…ARCHIVE_OUTBOUND The chat-media archive only ever wrote inbound media: archive() had a single call site on the inbound persist path, so a sent attachment had no durable copy, no S3 portability and no TTL retention. It now runs for outbound rows too when CHAT_MEDIA_ARCHIVE_OUTBOUND is set alongside CHAT_MEDIA_ARCHIVE_ENABLED (off by default, and a sub-flag rather than a mode of its own so the retention purge and orphan sweep are guaranteed to be maintaining whatever it writes). Three guards make that safe, all inside archive() so every caller inherits them: - A URL-based send stores the URL STRING, not bytes. Buffer.from(url,'base64') does not throw — it yields ~18 bytes of noise — and the read endpoint consults the archive BEFORE the inline copy, so archiving one would have served garbage in place of the correct 404, in a file the orphan sweep treats as referenced. - The same row can reach archive() from two writers, so a row already pointing at a file is left alone rather than orphaning the first one. - getMedia now matches the caller's chatId dialects: an outbound row stores the literal or the neutral form depending on which writer won the persist race, the same duality the inline fallback already resolves. Separately, merging a URL-pointer payload onto the engine's own-send echo replaced bytes the gateway had already downloaded (wwjs enriches its echo with the real payload), so those two merge sites now keep the richer copy.
rmyndharis
force-pushed
the
feat/outbound-media-archive
branch
from
August 9, 2026 09:31
b9001ab to
0aa7708
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The chat-media archive only ever wrote inbound media —
archive()had a single call site, on the inbound persist path — so a sent attachment had no durable copy, no S3 portability and no TTL retention. #1172 made sent media retrievable from the inline row copy; this makes it archivable.Archiving now runs for outbound rows when
CHAT_MEDIA_ARCHIVE_OUTBOUND=trueis set alongsideCHAT_MEDIA_ARCHIVE_ENABLED. It is a sub-flag rather than a mode of its own, so the retention purge and orphan sweep — which only run while archiving is enabled — are guaranteed to be maintaining whatever it writes. Off by default: it doubles storage again for the outbound half.Refs #1165.
The guards this needed first
Pointing the existing
archive()at outbound rows would have been actively harmful, so three guards land with it, all insidearchive()so every current and future caller inherits them:Buffer.from('https://example.com/cat.png','base64')does not throw — it yields 18 bytes of noise — and the read endpoint consults the archive before the inline copy. Archiving one would have replaced the correct404with a200carrying garbage labelledimage/png, in a file the orphan sweep treats as referenced and therefore never reaps. This is the third copy of a discriminator the send path and the export controller already apply to this value.archive()twice; a row already pointing at a file is left alone rather than orphaning the first one.getMediamatches chatId dialects. An outbound row stores the caller's literal chatId or the engine-neutral form depending on which writer won the persist race — the same duality the inline fallback already resolves. Without this, archived outbound media would be unreadable exactly when the inline copy cannot stand in for it.Also fixed
Merging a URL-pointer payload onto the engine's own-send echo replaced bytes the gateway had already downloaded (whatsapp-web.js enriches its echo with the real payload), leaving a row that rendered as a bare marker after a reload. Both merge sites now keep the richer copy. This is opportunistic — whatsapp-web.js only, and only when the echo wins the race — not a general fix for URL sends.
Deliberately not included
Pre-fetching a URL send's bytes in
MessageServiceso they could be stored. It is not a localised change: it moves the fetch outside thefailSendfunnel, so an SSRF block would surface as a raw500— potentially leaking the resolved internal address the current path explicitly redacts — instead of the sanitised400; it bypasses the whatsapp-web.js stickertrustDeclaredType:falseanti-forgery branch; it drops the URL-basename filename default; it inflates every URL-send row from ~40 bytes to the full base64 payload, whichGET /messagesreturns verbatim and the export budget charges for; and it would make ~18 unmocked unit tests hit the network. That deserves its own decision, not a footnote in this one.Surfaces
configuration.ts,env.validation.ts(strict boolean — a typo silently leaves outbound rows unarchived),env-precedence.ts, both compose files,.env.example,env.validation.spec.ts,docs/06(the "outbound messages are never archived" sentence was rewritten, not patched),docs/07, and the changelog. The controller's OpenAPI descriptions and the five SDK docstrings describe the serving order rather than the configuration, and already name the URL-send case, so they stay accurate andopenapi.jsonis unchanged.Verification
Nine new tests: three URL-pointer spellings including
HTTPS://, idempotence, the dialect-matched lookup, both merge sites keeping downloaded bytes, and the flag/SENT gating on the archive call. Each guard was mutation-checked — removing the flag check, the SENT gate, the compose forward and thecheckBoolentry each make their own test fail.Full gate on the rebased tree: lint,
format:check, build,tsc --noEmit, 5206 unit tests (run twice),test:scripts(55), and 148 e2e tests.