Files
multica/server/internal/handler/attachment_capability_test.go
Danila Katalshov 02e8e6c356 MUL-6023 feat(attachments): forced-attachment download URL across all storage modes (#6756)
* feat(attachments): forced-attachment download URL across all storage modes

Follow-up to #6713 (Refs #6712). The download button consumed `download_url`,
which is load-intent: the preview / inline-media paths need it to serve media
inline, so a cross-origin capability previews an image/PDF in-tab and a
presigned `download_url` 401s in token-mode. Add a separate, credential-free
`attachment_download_url` that forces Content-Disposition: attachment in every
storage mode, and point the web + desktop download buttons at it. `download_url`
is left untouched for the preview / inline-media paths.

Server (single-attachment endpoint only, never in list responses):
- proxy: a dl=1 variant of the #6092 capability, intent folded into the signed
  HMAC message so a load link cannot be flipped to a forced download (or the
  reverse); redemption forces AttachmentContentDisposition. Load-intent
  signatures stay byte-identical.
- presign: PresignGetWithContentDisposition(AttachmentContentDisposition).
- cloudfront: SignedURLWithContentDisposition (disposition folded into the
  signed resource, so a client cannot strip it).

Client: use-download-attachment.ts prefers the new field on web and desktop,
falling back to the #6092 capability (#6713) then the cookie-gated endpoint for
older servers. Optional zod/type field, parsed with the existing .loose()
fallback.

Follow-up to #6092 / #6713. Refs #6712.

* test(attachments): expect the second presign call for attachment_download_url

GetAttachmentByID now presigns the object twice in presign mode — once inline
for download_url, once with a forced attachment disposition for the new
attachment_download_url. Update TestGetAttachmentByID_AutoPublicEndpointReturnsPresignedDownloadURL
to expect both calls and to assert the forced-attachment presigned URL.

* fix(attachments): address review — omitempty, CloudFront test, mode-keyed switch

Addresses @Bohan-J's review on #6756:

- Add attachment_download_url `omitempty` so it stops shipping as an empty
  string in every list response, matching its own doc comment ("never in list
  responses") and the list-payload size tuned in #5999.
- Add TestGetAttachmentByID_CloudFrontModeSignsForcedAttachmentDownloadURL,
  covering the previously-unexercised CloudFront arm: attachment_download_url is
  CloudFront-signed and carries response-content-disposition=attachment folded
  into the signed Resource, and the load-intent download_url sibling does not
  force an attachment.
- Key the download-URL switch on the resolved mode instead of h.CFSigner != nil,
  so an explicit proxy/presign override takes effect even when a signer is
  configured; guard the CloudFront arm on a non-nil signer to keep an explicit
  cloudfront mode without a signer from panicking.
- Reframe the download-intent domain-separation comment as deliberate future
  headroom rather than a current threat mitigation (flipping today's two intents
  is not itself exploitable).
- Blank attachment_download_url in attachmentToDraftUpload alongside download_url,
  so a short-lived signed URL is not persisted into a draft.

Refs #6756 / #6712.
2026-08-12 15:02:03 +08:00

18 KiB