Linear: https://linear.app/engineereddev/issue/ENG-249
What's in this PR:
apps/server/src/storage/: theStoragecontract (types.ts),S3Storage(AWS SDK v3, presigned PUT/GET, prefix-stripped listing, conditional create withifNoneMatch: "*"),MemoryStorage(in-memory fake with presigned-request emulation), and one shared contract suite that runs against both.upload-mode.ts: resolvesUPLOAD_MODE=autoat boot by applying bucket CORS with a 2 s deadline; falls back toproxywith 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 CSPconnect-srconly indirectmode. Storage is destroyed on shutdown.log.ts: S3 key names are redacted.test:e2escript +vitest.e2e.config.ts+ MinIO global setup (reusesscripts/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 selectsproxyagainst 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:servermise task is added by the lead after ENG-277 merges (mise.toml is held by ENG-277).
🤖 Generated with Claude Code