Three S3 configurations had grown separately — S3_* for documents,
GENERATED_IMAGES_S3_* for images, and AUDIO_BACKUPS_S3_* after them — with
different key names and their own client construction. That is why moving
storage meant hunting through several files.
src/utils/objectStorage.js now resolves settings for any purpose: its own
variables first, then the shared S3_* ones, with a per-purpose bucket name
(S3_BUCKET_AUDIO_BACKUPS). One endpoint plus three bucket names is enough
for the whole app, and a purpose that needs its own account still overrides
everything. Audio backups and documents use it; generated images keeps its
own tested storage module, whose variable names the resolver already
understands.
Nothing existing has to change: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY and
the AWS_* fallbacks still resolve, and path-style addressing keeps each
purpose's previous default — off for documents, so a Backblaze endpoint
behaves as before, on where a custom endpoint implies MinIO. A _FILE
credential now always beats an inline one, so a mounted secret cannot be
shadowed by an inherited environment variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
- adminMilestones was reached only because adminConfig is mounted on
/api/admin ahead of it and guards the whole path. adminMiddleware checks
req.user.role and nothing else, so it failed closed (403) rather than
open — but on mount order, not intent. It now states the requirement,
with a test covering all four admin routers.
- TODO.md records the whole audit: what was verified working (live
transcription round trip, voice mode wiring), what was fixed, the two
advisories that are unreachable and why, and the CI/CD and Kubernetes
work worth doing before scaling out.
Security review found nothing else exploitable: parameterised SQL
throughout (the one interpolated table name is allowlisted), CORS refuses
to start open in production, JWT_SECRET refuses to start unset in
production, rate limits on /api and each auth route, a real CSP, and no
secrets in the repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU