M2: Storage adapter: S3 client, in-memory fake, bucket CORS and upload mode (ENG-249) #12

Merged demi.dev
targeting main from stoopkidddd/eng-249-m2-storage-adapter-s3-client-in-memory-fake-bucket-cors-and
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-249-m2-storage-adapter-s3-client-in-memory-fake-bucket-cors-and

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

What's in this PR:

  • apps/server/src/storage/: the Storage contract (types.ts), S3Storage (AWS SDK v3, presigned PUT/GET, prefix-stripped listing, conditional create with ifNoneMatch: "*"), MemoryStorage (in-memory fake with presigned-request emulation), and one shared contract suite that runs against both.
  • upload-mode.ts: resolves UPLOAD_MODE=auto at boot by applying bucket CORS with a 2 s deadline; falls back to proxy with one warning (operation + safe reason only).
  • app.ts / index.ts: the app now takes the real storage and the resolved upload mode; the bucket origin goes into CSP connect-src only in direct mode. Storage is destroyed on shutdown.
  • log.ts: S3 key names are redacted.
  • test:e2e script + vitest.e2e.config.ts + MinIO global setup (reuses scripts/e2e/minio.ts).
  • README: storage and upload-mode notes.

Verify (lead):

  • mise run check:server → exit 0 (8 files, 88 tests)
  • pnpm --filter @pneumo/server test:e2e → exit 0 (7 tests against MinIO)
  • After e2e: no pneumo-e2e-* containers, no leftover listeners.

Deviations:

  • MinIO rejects PutBucketCors, so detection selects proxy against MinIO. Direct mode still needs a staging check on Railway's bucket (spec §18.1).
  • AWS SDK default retries are kept: with retries off, MinIO reset the connection after a 412 conflict.
  • The e2e:server mise task is added by the lead after ENG-277 merges (mise.toml is held by ENG-277).

🤖 Generated with Claude Code

Commits 0