Story-2867 · EPIC-2834 T3d-b

A WhatsApp document cannot file itself

Acquired channel media now waits in one durable confirmation row per document. The target IDs come only from the sender's live, RLS-bounded bindings; a caption matter code is merely a selector over that authorized set. Sticky conversation scope and provider-supplied firm or matter hints never become filing authority.

The safe path

verified WhatsApp sender + acquired media receipt
  → one batched context read validates user + every live binding
       active membership + revocation epoch + canonical matter access
  → derive one accessible firm + matter
       ambiguous → ask for an explicit matter code; file nothing
  → require matters:write
  → create pending confirmation (10-minute expires_at)
  → prompt names filename + firm + matter
  → YES / M-NNN YES / per-document C-XXXXXXXX YES
       transaction lock serializes this counsel's pending queue
       row lock proves status=pending and expires_at>now()
       → recheck binding, membership, permission, and matter access
       → DocumentService.presign_upload
       → server-side copy channel-media → documents
       → DocumentService.finalize_upload
       → stamp confirmed with resolved_document_id
  → GuardDuty scan → extraction → registration → Blake

enabled rate(1 minute) worker
  → app-only expiry capability (no table-wide staff content read)
  → bounded due-row UPDATE ... FOR UPDATE SKIP LOCKED, using DB clock
  → pending → expired (no document side effect)
  → late YES names expiry and opens one fresh 10-minute ask

Acceptance behavior

ScenarioResult
CC-07prompt names firm and matter; only that document enters the existing Blake pipeline after confirmation
Binding authoritythe confirmation boundary accepts no provider firm, matter, or recipient authority; only the verified binding target can become pending, and no document row exists before counsel confirms. M4 email transport is outside the v1 ceremony.
CC-28bare YES/NO with several pending documents is rejected and every pending filename gets a C-XXXXXXXX selector; same-matter documents remain independently confirmable
CC-29the existing one-minute worker stamps elapsed rows while the channel is silent; late YES files nothing, acknowledges the old expiry once, and offers a fresh ask from current binding scope
CC-30a target outside the current firm scope requires the exact typed matter code; bare YES leaves it pending

State and concurrency

Migration c8e2a4f6d9b3 appends the frozen Blake handoff facts to the existing forced-RLS table: filename, detected content type, byte size, matter number, typed-confirmation flag, and the time an expired ask was handled. A transaction-scoped PostgreSQL advisory lock serializes one counsel's confirmation queue; a row lock then makes expiry and Blake handoff single winners, so two concurrent YES deliveries cannot mint two document rows and the clock cannot expire a row after S3 side effects begin. Context derivation uses one narrow SECURITY DEFINER batch reader: one GUC query plus one data query for one or many bindings (HR-19), with verified-user, membership, epoch, and canonical matter-access checks inside the reader. Failed prompt delivery aborts the row; failed S3 handoff rolls the database savepoint back and leaves the confirmation retryable. Because confirmation rows now contain filenames and storage receipts, the migration removes their blanket staff metadata policy: support reads require an active, firm-scoped support grant (HR-17), while the scheduled worker can call only the bounded pending-to-expired database function. If the binding dies between media acquisition and the second authorization read, the webhook transaction fails for redelivery; the media turn is never silently consumed.

Red-first proof

Capability contract

Clock expiry adds no cloud capability: it reuses the already enabled Terraform-owned rate(1 minute) relay shell, and its deploy smoke executes the exact bounded expiry function in a separate transaction.

The Blake copy path does require the live WhatsApp webhook role to read channel-media/*, write the documents raw-object prefix, and use the documents KMS key. Those Terraform grants and a copy-path canary are deployment-owned work; the functions tests plant both successful and denied S3 clients so the state machine stays honest around that boundary.