M3: Encrypted file send and download in the browser (ENG-261) #27

Merged demi.dev
targeting main from stoopkidddd/eng-261-m3-encrypted-file-send-and-download-in-the-browser
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-261-m3-encrypted-file-send-and-download-in-the-browser

Linear: https://linear.app/engineereddev/issue/ENG-261

What's in this PR (20 files, all under apps/web/; three omp commits):

  • Attach: multiple-file picker, drop anywhere on Home with an overlay, paste of files and images; one push per file; the composer text goes with the first file. Attach, drop and paste are unavailable until files can actually be sent (filesReady).
  • Size check against limits.maxFileBytes using ciphertextSize, before any reservation; a rejected selection keeps the draft and names the limit.
  • Upload: generateFileKey → POST /api/files → createFileEncryptor into a Blob → XHR PUT with progress (direct: presigned URL + returned headers; proxy: /api/files/:id/content with the bearer) → POST /api/pushes with the file payload. Cancel aborts before the push is created.
  • File rows and download: icon by MIME, name, size, Download with byte progress through createFileDecryptor; any tag failure shows "This file was corrupted or tampered with" and saves nothing.
  • Harness: startWebRuntime(prefix, { uploadMode, maxFileBytes }) for per-test direct/proxy servers; readiness now waits for the server's "Server listening" event plus one health request within the existing 30 s deadline (no retries or timeout increases).
  • Tests: vitest (size limit, progress reducer, cancel, readiness gating, same-commit handler race); Playwright in both modes (SHA-256 round trips at 0 B / 1 B / 65,536 B / 5 MiB, over-limit rejected with no POST /api/files, paste, cancel mid-upload, tamper, blocked-until-ready); axe and screenshots.

Verify (lead, rebased on 7a01948 with ENG-262 merged; conflicts in README.md and e2e/runtime.ts resolved keeping both sides):

  • mise run check:web → exit 0 (126 tests, build OK)
  • mise run e2e:web → exit 0, twice in a row (42 passed, 42 passed)
  • Implementer: three consecutive e2e:web runs green before the rebase.

Screenshots (not committed): file-{unavailable,uploading,item,tamper}-{light,dark,mobile}.png.

Deviations: none. Check-fix rounds: (1) the ENG-258 DevicesView fixture after the rebase; (2) the flaky readiness race found by the lead (Attach enabled before files could be sent; harness readiness under load).

🤖 Generated with Claude Code

Commits 0