Story-2865 · T3d-a

WhatsApp media survives the provider URL

An inbound image, document, video, or audio message is downloaded at the webhook seam while its provider URL is fresh, checked with SHA-256, and landed under an idempotent key in PRAMAAN-owned S3. The checksum and object receipt are persisted on the existing inbound-message row. Confirmation, target attachment, and Blake handoff are Story-2867 and are deliberately absent here.

The acquisition seam

Meta webhook
  → verify signature + claim dedupe row
  → resolve LIVE bindings (membership mirror + revocation epoch)
       no live binding → refuse; no download, no S3 write
  → lock selected binding and re-check status + epoch
  → invoke non-VPC whatsapp-sender Lambda
       GET media metadata → immediately stream fresh URL under hard byte cap
       → compute SHA-256 → compare provider checksum when supplied
       → PutObject(channel-media/{sha256("meta:" + provider_message_id)})
  → persist checksum + bucket + key + size + content type + acquired_at
  → only then continue caption disambiguation

Any permanent acquisition error stamps the inbound row failed.
Any transient provider/S3 error returns 500 so Meta retries the same dedupe key.

Acceptance behavior

CaseResult
Fresh mediametadata and CDN reads are back-to-back; bytes are streamed only up to the configured cap
URL expires mid-flightthe next attempt fetches new metadata and a new URL; the stale URL is never reused
Checksum mismatch / oversizefail closed; no S3 receipt is recorded
Unbound senderzero acquisition calls and zero S3 writes
Revoked binding or epochbinding row lock plus live epoch check refuses before the sender invocation
Ambiguous captionmedia is acquired first, then the deterministic matter prompt may span turns without relying on the expiring URL

Durability and tenancy

Migration d6f4a2c8b190 extends business.channel_inbound_messages with one complete media receipt. A check constraint forbids partial receipts, the staff-only NULL-firm row stays behind the table's existing forced RLS policies, and a partial acquisition index supports staff receipt lookup. The downgrade removes the index, constraint, and every added column.

Red-first proof

Capability dependency

The application path requires the channel-media bucket and s3:PutObject on the WhatsApp sender role. The canary and workflow-contract tests are the named detector. The PR must remain blocked until the linked pramaan-infra twin supplies that bucket and grant.