teqo-backup/docs/changelog/2026-08-18-ops52.md
Francisco Solla dabc00f46c ops(OPS52): migra a media de produção do Vercel Blob para o Garage S3
- payload.config.ts: vercelBlobStorage -> s3Storage (@payloadcms/storage-s3)
  com endpoint Garage (forcePathStyle, region garage); bucket privado — o
  staticHandler do plugin streama os objetos em /api/media/file/<filename>,
  mantendo o contrato de URL relativa das 40 rows pré-existentes
- src/utilities/mediaStorage.ts: resolveS3StorageEnv fail-closed (S3_* parcial
  -> throw no boot com a lista exata; nenhuma -> storage local dev/test)
- seed-posts: remove pre-delete do @vercel/blob (S3 sobrescreve nativamente)
- worktree-env.mjs: s3EnvCopiedLines all-or-nothing (unit-testada)
- envs S3_BUCKET/S3_ENDPOINT/S3_REGION/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY
  no .env.example/.env.test; cloud-setup.sh sem o sed morto do blob
- deps: -storage-vercel-blob -@vercel/blob +storage-s3; importMap regenerado
- docs: impl plan (revisão A->B: Garage v2 sem acesso anônimo na API S3),
  AGENTS.md, changelog 2026-08-18-ops52
2026-08-17 22:36:34 -03:00

2.1 KiB

Recently resolved (2026-08-18): OPS52 — a media saiu do Vercel Blob para o Garage S3 do homeserver (bucket teqo-media + key teqo-key, já existentes no Garage v2.1.0). O plugin @payloadcms/storage-vercel-blob (e o @vercel/blob do seed) foi trocado por @payloadcms/storage-s3 na config, no padrão R2 da doc do Payload (endpoint + forcePathStyle: true, region garage). Decisão de arquitetura (revisão A→B durante a execução, gate com o humano): o Garage v2.1.0 não suporta acesso anônimo na API S3 (sem --public-read; source: "when we support anonymous access in the future"; testado: anonymous/anon → NoSuchAccessKey), então o bucket fica privado e a app serve a media por proxy — sem disablePayloadAccessControl, o staticHandler do plugin streama o objeto do Garage em /api/media/file/<filename> (range + etag/304 nativos) — o mesmo contrato de URL relativa que as 40 rows pré-existentes já têm (a recuperação dos arquivos é a Issue #10/OPS52-media). Fail-closed: resolveS3StorageEnv (src/utilities/mediaStorage.ts, unit-testado) — envs S3_* parciais → throw no boot com a lista exata de faltantes; nenhuma → storage local (dev/test). Envs novas: S3_BUCKET, S3_ENDPOINT, S3_REGION (default garage), S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY (.env.example, .env.test, worktree.mjs com cópia all-or-nothing). Seed de posts: pre-delete do Blob removido (o S3 sobrescreve o key determinístico nativamente — idempotência intacta). Prova de contrato real: PUT/GET/DELETE via SDK contra o Garage (tailnet http://100.119.220.31:3900), depois media criada pelo Payload local com S3 ativo → media.url relativa + GET /api/media/file/... → 200 image/png; bucket devolvido a 0 objects e .env.local do worktree limpo (sem credenciais de prod). Sem migration (config-only; importMap regenerado sem VercelBlobClientUploadHandler). Runbook de cutover registrado no impl plan: setar as 5 S3_* no ~/stack/teqo-1313.env (endpoint tailnet) antes do próximo deploy (OPS53). Repo: docs/plans/ops52-blob-vercel-para-garage-impl.md; infra-solla STATE.md atualizado à parte.