teqo/src/utilities/mediaStorage.ts
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

63 lines
2.1 KiB
TypeScript

/**
* Resolves the media storage configuration from the S3_* environment
* variables (Garage S3-compatible endpoint).
*
* Fail-closed contract (OPS52):
* - No S3_* var set -> { enabled: false } (dev/test use Payload's local storage)
* - Partially set -> throws with the exact list of missing vars (never boots
* against the wrong endpoint/bucket, never AWS defaults)
* - All set -> { enabled: true, ... } for the s3Storage plugin
*
* S3_REGION is optional: the Garage server (garage.toml) fixes `s3_region =
* "garage"`, which is the only accepted value for signature validation.
*
* Public URLs are NOT generated: the bucket stays private and Payload proxies
* the files through its /media/file route (the plugin's staticHandler streams
* the object from Garage), so `media.url` keeps the relative
* /media/file/<filename> contract unchanged.
*/
export const S3_STORAGE_ENV_KEYS = [
'S3_BUCKET',
'S3_ENDPOINT',
'S3_ACCESS_KEY_ID',
'S3_SECRET_ACCESS_KEY',
] as const
const S3_REGION_DEFAULT = 'garage'
export type S3StorageEnv =
| { enabled: false }
| {
enabled: true
bucket: string
endpoint: string
region: string
accessKeyId: string
secretAccessKey: string
}
export function resolveS3StorageEnv(env: Record<string, string | undefined>): S3StorageEnv {
const value = (key: string) => env[key]?.trim() || ''
const present = S3_STORAGE_ENV_KEYS.filter((key) => value(key) !== '')
if (present.length === 0) {
return { enabled: false }
}
if (present.length < S3_STORAGE_ENV_KEYS.length) {
const missing = S3_STORAGE_ENV_KEYS.filter((key) => !present.includes(key))
throw new Error(
`Media storage S3 configurado parcialmente: faltam ${missing.join(
', ',
)}. Configure todas as envs S3_* (bucket, endpoint, credenciais) ou nenhuma (storage local).`,
)
}
return {
enabled: true,
bucket: value('S3_BUCKET'),
endpoint: value('S3_ENDPOINT'),
region: value('S3_REGION') || S3_REGION_DEFAULT,
accessKeyId: value('S3_ACCESS_KEY_ID'),
secretAccessKey: value('S3_SECRET_ACCESS_KEY'),
}
}