📋 Changelog • Releases • Issues
Microservicio HTTP para gestionar archivos en Cloudflare R2 mediante una API REST segura con API Key.
r2-service es un servicio especializado que centraliza el CRUD de objetos en un bucket R2 para evitar que otras piezas del sistema hablen directo con el storage. Su responsabilidad única es exponer endpoints de subida, descarga, borrado y listado de archivos, validando entradas y estandarizando errores de dominio. Este servicio no accede a base de datos, no conoce otros servicios y no procesa imágenes.
| Capa | Tecnología | Versión | Propósito |
|---|---|---|---|
| Runtime | Node.js | 20+ | Ejecutar el servicio HTTP en entorno server-side. |
| Lenguaje | TypeScript | 5.x | Tipado estricto y mantenibilidad del código. |
| Framework HTTP | Hono | 4.12.8 | Definición de rutas, middleware y manejo de requests/responses. |
| SDK S3-compatible | AWS S3 SDK v3 | 3.1014.0 | Operaciones PutObject, GetObject, DeleteObject, ListObjectsV2, HeadObject. |
| Storage | Cloudflare R2 | Servicio gestionado | Persistencia de objetos y archivos. |
| Validación | Zod | 4.3.6 | Validación de variables de entorno y entradas HTTP. |
| Testing | Vitest | 4.1.0 | Pruebas unitarias y cobertura. |
| Package manager | pnpm | 8+ (repo en 10.32.1) | Instalación de dependencias y scripts. |
| Contenedores | Docker | 24+ recomendado | Empaquetado y despliegue en contenedor. |
| Dev runner TS | tsx | 4.21.0 | Ejecución en desarrollo con recarga en caliente. |
- Node.js 20+.
- pnpm 8+.
- Cuenta de Cloudflare con R2 habilitado.
- Bucket de R2 creado.
- API Token de R2 con permisos Object Read & Write.
- Docker (opcional, para despliegue).
git clone https://github.com/tu-organizacion/r2-service.git
cd r2-service
pnpm install
cp .env.example .envConfigura el archivo .env usando como base .env.example.
| Variable | Descripción | Requerida | Default | Ejemplo |
|---|---|---|---|---|
R2_ACCOUNT_ID |
ID de cuenta de Cloudflare. | Sí | No | 1234567890abcdef1234567890abcdef |
R2_ACCESS_KEY_ID |
Access Key ID del token R2. | Sí | No | a1b2c3d4e5f6g7h8i9j0 |
R2_SECRET_ACCESS_KEY |
Secret Access Key del token R2. | Sí | No | xYz...clave-secreta...123 |
R2_BUCKET_NAME |
Nombre del bucket R2 objetivo. | Sí | No | amazon-products-images |
PORT |
Puerto HTTP del servicio. | No | 3000 |
3000 |
NODE_ENV |
Entorno de ejecución (development, production, test). |
No | development |
development |
LOG_LEVEL |
Nivel mínimo de logs estructurados (debug, info, warn, error). |
No | info |
debug |
API_KEY |
Clave usada por el header x-api-key (mínimo 32 chars). |
Sí | No | 4f7f6f6d1c... |
REMOTE_FETCH_ALLOWED_HOSTS |
Hosts remotos permitidos para descarga, separados por coma. | Sí | No | images-na.ssl-images-amazon.com,m.media-amazon.com |
REMOTE_FETCH_ALLOWED_MIME_TYPES |
MIME types aceptados en la respuesta remota, separados por coma. | Sí | No | image/jpeg,image/png,image/webp |
REMOTE_FETCH_MAX_BYTES |
Tamaño máximo en bytes del archivo remoto descargado. | Sí | No | 5242880 (5 MB) |
REMOTE_FETCH_TIMEOUT_MS |
Tiempo máximo en ms para la petición HTTP remota. | Sí | No | 10000 (10 s) |
REMOTE_FETCH_MAX_REDIRECTS |
Número máximo de redirecciones HTTP permitidas. | Sí | No | 3 |
Diagnóstico de descarga remota:
- En
NODE_ENV=developmentyNODE_ENV=test, el servicio emite logs de diagnóstico del flujoRemoteFileFetcherServicepara resolución DNS, preparación de request, invocación delookuppinneado y errores de request remota. - En
NODE_ENV=production, esos logs de diagnóstico se silencian y solo se conservan los errores operativos normales del servicio.
Genera una API Key segura con Node.js:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
⚠️ Nunca subas.enval repositorio. Contiene credenciales y secretos.
- Entra a
https://dash.cloudflare.com. - Selecciona tu cuenta y abre R2 Object Storage.
- Crea un bucket (nombre recomendado:
amazon-products-images). - Ve a Manage R2 API Tokens.
- Crea un token con permisos Object Read & Write.
- Copia
Account ID,Access Key IDySecret Access Keyal archivo.env.
Modo desarrollo:
pnpm devModo producción:
pnpm build && pnpm startVerificar que funciona:
curl http://localhost:3000/healthRespuesta esperada:
{
"status": "ok",
"service": "r2-service",
"timestamp": "2026-03-23T14:00:00.000Z"
}Output esperado en consola al arrancar correctamente:
{"service":"r2-service","env":"development","level":"info","message":"server.started","timestamp":"2026-03-23T14:00:00.000Z","port":3000}| Script | Comando | Descripción |
|---|---|---|
dev |
tsx watch src/index.ts |
Arranca en desarrollo con recarga automática. |
build |
tsc && tsc-alias |
Compila TypeScript a dist/ y resuelve aliases. |
start |
node dist/index.js |
Ejecuta la build de producción. |
release |
semantic-release |
Calcula versión automáticamente, genera CHANGELOG.md, crea tag vX.Y.Z y release en GitHub. |
release:dry |
semantic-release --dry-run |
Simula el release sin escribir cambios ni crear tags. |
test |
vitest |
Ejecuta la suite de pruebas. |
test:coverage |
vitest --coverage |
Ejecuta tests con reporte de cobertura. |
lint |
eslint src --ext .ts |
Analiza estilo y reglas de calidad en src. |
typecheck |
tsc --noEmit |
Valida tipos sin generar artefactos. |
prepare |
husky |
Activa/instala hooks de Git al instalar dependencias. |
commitlint |
commitlint --edit |
Valida el mensaje de commit contra Conventional Commits. |
Este repositorio valida commits automáticamente mediante hooks de Git:
pre-commit: ejecutapnpm run lint.commit-msg: ejecutacommitlintsobre el mensaje del commit.
Si los hooks no están activos localmente, ejecuta:
pnpm run prepareReglas principales del mensaje de commit:
- Formato:
<type>: <subject>. - Tipos permitidos:
feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert. - Header máximo: 100 caracteres.
El repositorio usa semantic-release para automatizar versionado y changelog.
Flujo configurado:
- Trigger:
pusha ramamasteren GitHub Actions (.github/workflows/release.yml). - Entrada: historial de commits con formato Conventional Commits.
- Salida automática:
- Calcula siguiente versión semántica.
- Genera/actualiza
CHANGELOG.md. - Crea commit
chore(release): x.y.z. - Crea tag
vX.Y.Z. - Publica release en GitHub.
El versionado es automático basado en los tipos de commit Conventional Commits. La versión comienza en 1.0.0 y se incrementa según:
| Tipo de commit | Incrementa | Ejemplo | Resultado |
|---|---|---|---|
fix: ... |
PATCH | fix(r2): corrige error al borrar archivo |
1.0.0 → 1.0.1 |
feat: ... |
MINOR | feat(api): agrega endpoint de descarga |
1.0.0 → 1.1.0 |
BREAKING CHANGE |
MAJOR | feat(api)!: rediseña formato de respuesta |
1.0.0 → 2.0.0 |
Ejemplos de commits para cada tipo de versión:
Patch (1.0.0 → 1.0.1):
fix(auth): corrige validación de API Key
Minor (1.0.0 → 1.1.0):
feat(api): agrega soporte para presigned URLs
Major (1.0.0 → 2.0.0):
feat(api)!: cambia estructura de respuesta del upload
BREAKING CHANGE: el endpoint POST /files/:key ahora retorna {success, data} en lugar de {uploaded, metadata}
Para validar localmente sin publicar:
pnpm run release:dryPara ejecutar release manualmente con versión específica (avanzado):
pnpm run release -- --release-as 2.0.0
⚠️ Si ejecutasrelease:dryfuera de GitHub Actions, exportaGH_TOKENoGITHUB_TOKENcon un token válido para evitar el errorENOGHTOKEN.
- URL:
http://localhost:3000/docs. - Disponible solo fuera de producción (
NODE_ENV != production). - En la UI, autentica requests agregando el header
x-api-keycon el valor deAPI_KEY.
ℹ️ Cuando
NODE_ENV=production, la ruta/docsno se expone.
| Método | Ruta | Descripción | Auth requerida |
|---|---|---|---|
GET |
/health |
Estado de disponibilidad del servicio. | No |
GET |
/api/v1/files |
Lista archivos en R2 (acepta prefix). |
Sí (x-api-key) |
POST |
/api/v1/files/signed-url |
Genera una URL firmada temporal para descargar un archivo privado existente. | Sí (x-api-key) |
POST |
/api/v1/files/:key |
Sube un archivo binario a R2. | Sí (x-api-key) |
GET |
/api/v1/files/:key |
Descarga un archivo desde R2. | Sí (x-api-key) |
DELETE |
/api/v1/files/:key |
Elimina un archivo de R2. | Sí (x-api-key) |
ℹ️
POST /api/v1/files/signed-urlesta pensado para buckets privados y entrega acceso temporal por archivo sin volver publico el bucket.
curl http://localhost:3000/health{
"status": "ok",
"service": "r2-service",
"timestamp": "2026-03-23T14:00:00.000Z"
}curl http://localhost:3000/healthz{
"code": 404,
"message": "Not Found"
}curl -H "x-api-key: TU_API_KEY" "http://localhost:3000/api/v1/files?prefix=productos/"{
"success": true,
"data": {
"files": [
{
"key": "productos/demo/imagen.jpg",
"size": 183452,
"lastModified": "2026-03-23T14:02:21.000Z"
}
],
"count": 1
},
"timestamp": "2026-03-23T14:20:44.000Z"
}curl "http://localhost:3000/api/v1/files?prefix=productos/"{
"code": "UNAUTHORIZED",
"message": "API Key inválida o ausente.",
"timestamp": "2026-03-23T14:25:00.000Z"
}curl -X POST \
-H "x-api-key: TU_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary "@./ejemplos/imagen.jpg" \
"http://localhost:3000/api/v1/files/productos/demo/imagen.jpg?contentType=image/jpeg"{
"success": true,
"data": {
"key": "productos/demo/imagen.jpg",
"size": 183452,
"contentType": "image/jpeg",
"uploadedAt": "2026-03-23T14:12:58.000Z"
},
"timestamp": "2026-03-23T14:12:58.000Z"
}curl -X POST --data-binary "@./ejemplos/imagen.jpg" "http://localhost:3000/api/v1/files/productos/demo/imagen.jpg"{
"code": "UNAUTHORIZED",
"message": "API Key inválida o ausente.",
"timestamp": "2026-03-23T14:25:00.000Z"
}curl -X POST \
-H "x-api-key: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key":"privados/demo/factura.pdf","expiresIn":900}' \
"http://localhost:3000/api/v1/files/signed-url"{
"success": true,
"data": {
"signedUrl": "https://<account>.r2.cloudflarestorage.com/<bucket>/privados/demo/factura.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900",
"expiresIn": 900,
"expiresAt": "2026-03-26T18:00:00.000Z"
},
"timestamp": "2026-03-26T17:45:00.000Z"
}curl -X POST \
-H "x-api-key: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key":"privados/demo/factura.pdf","expiresIn":10}' \
"http://localhost:3000/api/v1/files/signed-url"{
"code": "VALIDATION_ERROR",
"message": "Error de validación",
"details": {
"fieldErrors": {
"expiresIn": [
"expiresIn debe ser mayor o igual a 60 segundos."
]
},
"formErrors": []
},
"timestamp": "2026-03-26T17:45:10.000Z"
}curl -H "x-api-key: TU_API_KEY" "http://localhost:3000/api/v1/files/productos/demo/imagen.jpg" --output imagen.jpg{
"success": true,
"data": {
"downloaded": true,
"note": "La respuesta real es binaria; se guarda en el archivo indicado por --output."
}
}curl -H "x-api-key: TU_API_KEY" "http://localhost:3000/api/v1/files/productos/no-existe.jpg"{
"code": "R2_NOT_FOUND",
"message": "El archivo 'productos/no-existe.jpg' no existe en R2.",
"timestamp": "2026-03-23T14:26:00.000Z"
}curl -X DELETE -H "x-api-key: TU_API_KEY" "http://localhost:3000/api/v1/files/productos/demo/imagen.jpg"{
"success": true,
"data": {
"key": "productos/demo/imagen.jpg",
"deletedAt": "2026-03-23T14:18:11.000Z"
},
"timestamp": "2026-03-23T14:18:11.000Z"
}curl -X DELETE -H "x-api-key: TU_API_KEY" "http://localhost:3000/api/v1/files/productos/no-existe.jpg"{
"code": "R2_NOT_FOUND",
"message": "El archivo 'productos/no-existe.jpg' no existe en R2.",
"timestamp": "2026-03-23T14:26:00.000Z"
}r2-service/
├── .env.example # Plantilla oficial de variables de entorno.
├── .gitignore # Exclusiones de Git para secretos y artefactos.
├── README.md # Documentación principal del servicio.
├── package.json # Dependencias y scripts del proyecto.
├── pnpm-lock.yaml # Lockfile de pnpm para instalaciones reproducibles.
├── tsconfig.json # Configuración TypeScript (strict y aliases).
├── vitest.config.ts # Configuración de Vitest y cobertura.
├── coverage/ # Reportes HTML/JSON/XML de cobertura de tests.
│ ├── base.css # Estilos del reporte HTML de cobertura.
│ ├── block-navigation.js # Navegación del reporte HTML de cobertura.
│ ├── clover.xml # Reporte de cobertura formato Clover.
│ ├── coverage-final.json # Resumen estructurado de cobertura.
│ ├── index.html # Entrada principal del reporte de cobertura.
│ ├── prettify.css # Estilos de resaltado para reporte HTML.
│ ├── prettify.js # Script de resaltado para reporte HTML.
│ ├── sorter.js # Ordenamiento de tablas en el reporte HTML.
│ ├── errors/ # Detalle de cobertura por archivos de errores.
│ │ ├── AppError.ts.html # Cobertura de src/errors/AppError.ts.
│ │ ├── index.html # Índice de cobertura de carpeta errors.
│ │ ├── index.ts.html # Cobertura de src/errors/index.ts.
│ │ ├── R2DeleteError.ts.html # Cobertura de src/errors/R2DeleteError.ts.
│ │ ├── R2NotFoundError.ts.html # Cobertura de src/errors/R2NotFoundError.ts.
│ │ ├── R2UploadError.ts.html # Cobertura de src/errors/R2UploadError.ts.
│ │ ├── UnauthorizedError.ts.html # Cobertura de src/errors/UnauthorizedError.ts.
│ │ └── ValidationError.ts.html # Cobertura de src/errors/ValidationError.ts.
│ └── services/ # Detalle de cobertura por servicios.
│ ├── index.html # Índice de cobertura de carpeta services.
│ └── R2Service.ts.html # Cobertura de src/services/R2Service.ts.
├── src/
│ ├── index.ts # Entrada del proceso Node, carga env e inicia servidor.
│ ├── config/
│ │ ├── env.ts # Validación Zod de variables de entorno.
│ │ ├── logger.ts # Logger estructurado central y serialización de errores.
│ │ ├── openapi.ts # Especificación OpenAPI base y componentes.
│ │ └── r2Client.ts # Cliente S3 configurado para Cloudflare R2.
│ ├── errors/
│ │ ├── AppError.ts # Clase base de errores de dominio.
│ │ ├── index.ts # Barrel de exportación de errores.
│ │ ├── R2DeleteError.ts # Error de fallo al eliminar en R2.
│ │ ├── R2NotFoundError.ts # Error cuando un objeto no existe en R2.
│ │ ├── R2SignedUrlError.ts # Error al generar URL firmada temporal de descarga.
│ │ ├── R2UploadError.ts # Error de fallo de subida a R2.
│ │ ├── UnauthorizedError.ts # Error por API Key ausente o inválida.
│ │ └── ValidationError.ts # Error de validación de entradas.
│ ├── middleware/
│ │ ├── auth.middleware.ts # Validación de API Key con timingSafeEqual.
│ │ ├── error.middleware.ts # Mapeo de errores a respuestas HTTP JSON.
│ │ ├── index.ts # Barrel de middlewares.
│ │ ├── logger.middleware.ts # Logging estructurado de requests y métricas de respuesta.
│ │ └── request-context.middleware.ts # Generación y propagación de requestId por solicitud.
│ ├── routes/
│ │ ├── docs.routes.ts # OpenAPI JSON y UI Scalar.
│ │ ├── files.routes.ts # Endpoints CRUD de archivos en R2.
│ │ ├── health.routes.ts # Endpoint de health check del servicio.
│ │ └── index.ts # Composición global de rutas y middlewares.
│ ├── schemas/
│ │ ├── delete.schema.ts # Esquema Zod para DELETE /files/:key.
│ │ ├── download.schema.ts # Esquema Zod para GET /files/:key.
│ │ ├── index.ts # Barrel de esquemas Zod.
│ │ ├── list.schema.ts # Esquema Zod para query de listado.
│ │ ├── signed-url.schema.ts # Esquema Zod para POST /files/signed-url.
│ │ └── upload.schema.ts # Esquema Zod para upload y validación de key.
│ └── services/
│ └── R2Service.ts # Servicio de dominio para operaciones en R2.
└── test/
├── config/ # Espacio reservado para tests de configuración.
├── errors/ # Espacio reservado para tests de errores.
├── integration/ # Tests de integración HTTP sobre el pipeline real de Hono.
│ ├── import-from-url.routes.test.ts # Contrato HTTP del endpoint import-from-url con app.request.
│ └── signed-url.routes.test.ts # Contrato HTTP del endpoint de URL firmada temporal.
└── unit/
├── error.middleware.test.ts # Tests del middleware de errores y serialización de fallos.
├── logger.middleware.test.ts # Tests de logging request/response con requestId.
├── R2Service.signed-url.test.ts # Tests unitarios de generación de URL firmada temporal.
├── R2Service.test.ts # Tests unitarios de R2Service con SDK mockeado.
└── RemoteFileFetcherService.test.ts # Tests unitarios del descargador remoto con DNS/fetch mockeados.
- TypeScript con
strict: truey sinany. async/awaitsiempre; no usar.then()ni.catch()encadenados.- Errores de dominio siempre con
AppErrory subclases. - Validación con Zod antes de invocar la capa de servicio.
- Una responsabilidad por función y por clase.
- JSDoc obligatorio en funciones y métodos públicos.
Ejecutar toda la suite:
pnpm testVer cobertura:
pnpm test:coverageSuites actuales:
test/unit/R2Service.test.ts- Componente probado:
R2Service, encargado del CRUD sobre Cloudflare R2 mediante AWS S3 SDK v3. - Comportamiento crítico que protege: que el servicio sanee claves y prefijos, traduzca fallos esperados del SDK a errores de dominio y construya respuestas consistentes sin depender de R2 real.
- Dependencias aisladas con mocks:
@config/r2Client.jsmediantesendMockpara simular respuestas del SDK y@config/env.jspara controlarR2_BUCKET_NAME. - Escenarios cubiertos: upload exitoso, sanitización de
key, fallo de subida mapeado aR2UploadError, lectura exitosa,R2NotFoundErrorporNoSuchKeyo404, propagación de errores inesperados, borrado exitoso, borrado de archivo inexistente, fallo de delete mapeado, listado con resultados, listado vacío, envío deprefix, descarte de objetos sinKey, sanitización deprefix, existencia positiva, inexistencia y sanitización enfileExists. - Garantías de seguridad de la suite: evita regresiones en sanitización contra path traversal para
keyyprefix, y asegura que los errores expuestos al resto del servicio sigan siendo errores de dominio controlados en lugar de filtrar fallos crudos del SDK. - Comando individual:
- Componente probado:
pnpm vitest run test/unit/R2Service.test.tstest/unit/R2Service.signed-url.test.ts- Componente probado: método
getDownloadSignedUrlenR2Service, responsable de generar URLs firmadas temporales para objetos privados. - Comportamiento crítico que protege: que solo se firmen objetos existentes, que la key se sanitice antes de firmar y que los fallos del presigner se traduzcan a error de dominio controlado.
- Dependencias aisladas con mocks:
@config/r2Client.jsparaHeadObject,@config/env.jspara bucket y@aws-sdk/s3-request-presignerpara evitar firmas reales. - Escenarios cubiertos: firma exitosa, sanitización de key, archivo inexistente y fallo del presigner mapeado a
R2SignedUrlError. - Garantías de seguridad de la suite: evita regresiones que permitan firmar rutas no sanitizadas o exponer errores crudos del SDK/presigner en la capa superior.
- Comando individual:
- Componente probado: método
pnpm vitest run test/unit/R2Service.signed-url.test.tstest/unit/logger.middleware.test.ts- Componente probado:
loggerMiddlewarejunto conrequestContextMiddlewarepara registrar entrada/salida de requests con correlación porrequestId. - Comportamiento crítico que protege: que el cierre de cada request registre
status,durationMsyrequestId, y que los errores HTTP queden clasificados con nivelerror. - Dependencias aisladas con mocks:
@config/env.jspara controlarLOG_LEVELyNODE_ENV, y spies sobreconsole.debug,console.infoyconsole.errorpara capturar eventos de log estructurado. - Escenarios cubiertos: respuesta 200 con requestId propagado desde header y respuesta 500 con emisión de
request.enden nivelerror. - Garantías de seguridad de la suite: valida la trazabilidad por request para auditoría de fallos y evita regresiones donde se pierda la correlación entre petición, respuesta y error operativo.
- Comando individual:
- Componente probado:
pnpm vitest run test/unit/logger.middleware.test.tstest/unit/error.middleware.test.ts- Componente probado:
errorMiddlewareintegrado conrequestContextMiddlewarepara mapear errores de dominio/validación y emitir logs enriquecidos. - Comportamiento crítico que protege: que errores de dominio con
causese serialicen en logs y que errores de validación mantengan respuesta400consistente con logging en nivelwarn. - Dependencias aisladas con mocks:
@config/env.jspara fijarLOG_LEVEL/NODE_ENVy spies sobreconsole.warn/console.errorpara verificar contenido estructurado del log. - Escenarios cubiertos:
R2UploadErrorcon causa anidada en respuesta 500 yValidationErrorcon detalles de entrada en respuesta 400. - Garantías de seguridad de la suite: asegura que el servicio conserve detalle técnico interno para diagnóstico sin romper el contrato HTTP de errores controlados al cliente.
- Comando individual:
- Componente probado:
pnpm vitest run test/unit/error.middleware.test.tstest/unit/RemoteFileFetcherService.test.ts- Componente probado:
RemoteFileFetcherService, responsable de descargar recursos remotos con allowlist, validación DNS anti-SSRF, control manual de redirects y validaciones de MIME y tamaño. - Comportamiento crítico que protege: que ninguna relajación en allowlist, bloqueo de IPs privadas, tipo MIME permitido, límite de bytes o validación de redirects pase inadvertida.
- Dependencias aisladas con mocks:
node:dns/promisespara controlar resoluciones DNS,fetchglobal para simular respuestas remotas y@config/env.jspara fijar hosts permitidos, MIME aceptados, tamaño máximo, timeout y máximo de redirects. - Escenarios cubiertos: host fuera de allowlist, hostname permitido que resuelve a IP privada, respuesta con MIME inválido, body que supera el tamaño máximo, redirect hacia host no permitido y descarga exitosa con
buffer,contentType,finalUrlysize. - Garantías de seguridad de la suite: endurece la defensa SSRF validando hostname y resolución DNS antes de descargar, impide aceptar contenido remoto fuera de política y garantiza que el límite de tamaño siga aplicándose incluso durante la lectura del body.
- Comando individual:
- Componente probado:
pnpm vitest run test/unit/RemoteFileFetcherService.test.tstest/integration/import-from-url.routes.test.ts- Componente probado: el contrato HTTP de
POST /api/v1/files/import-from-urlmontado sobre elappreal de Hono ensrc/routes/index.ts, incluyendofiles.routes,authMiddlewareyerrorMiddleware. - Comportamiento crítico que protege: que el endpoint responda de forma consistente con
201,400y401dentro del pipeline real de rutas y middleware, sin saltarse autenticación, parseo JSON ni manejo centralizado de errores. - Dependencias aisladas con mocks:
@config/env.jspara fijar API key y configuración requerida por el arranque del pipeline, eImportFileFromUrlUseCasepara evitar descargas remotas y subidas reales a R2. - Escenarios cubiertos: importación exitosa con body válido, rechazo de body con JSON inválido y rechazo por ausencia del header
x-api-key. - Garantías de seguridad de la suite: asegura que el endpoint siga exigiendo autenticación antes de tocar el caso de uso, que los errores de entrada inválida se traduzcan a
400controlado y que ninguna prueba necesite red real ni almacenamiento externo para validar el contrato HTTP. - Comando individual:
- Componente probado: el contrato HTTP de
pnpm vitest run test/integration/import-from-url.routes.test.tstest/integration/signed-url.routes.test.ts- Componente probado: contrato HTTP de
POST /api/v1/files/signed-urlsobreappreal de Hono conauthMiddlewareyerrorMiddleware. - Comportamiento crítico que protege: respuesta homogénea en éxito (
200) y errores (400,401,404) dentro del pipeline real. - Dependencias aisladas con mocks:
@config/env.jspara configuración de arranque yr2Service.getDownloadSignedUrlpara evitar dependencia de R2 real. - Escenarios cubiertos: generación exitosa, ausencia de
x-api-key,expiresInfuera de rango y archivo inexistente. - Garantías de seguridad de la suite: valida que la autenticación ocurra antes de tocar la lógica de firma y que inputs inválidos no lleguen a la capa de servicio.
- Comando individual:
- Componente probado: contrato HTTP de
pnpm vitest run test/integration/signed-url.routes.test.tstest/config/: carpeta preparada para futuras pruebas de validación y configuración.test/errors/: carpeta preparada para futuras pruebas de mapeo de errores de dominio.
Nota de aislamiento:
- Ninguna suite usa red real ni R2 real; todas las dependencias externas se aíslan con
vi.mockovi.stubGlobal, incluso en integración HTTP cuando se prueba el pipeline real conapp.request.
Construir imagen:
docker build -t r2-service .Ejecutar contenedor con .env:
docker run --env-file .env -p 3000:3000 r2-serviceEjecutar con docker-compose:
docker-compose up -dℹ️ Usa un usuario non-root en la imagen (por ejemplo,
USER node) para reducir superficie de riesgo en runtime.
- El servicio protege endpoints de API con
x-api-key. - La clave se genera de forma segura con
crypto.randomBytesy se envía en el headerx-api-key. - La comparación se hace con
timingSafeEqualpara mitigar timing attacks. - El servicio nunca debe loguear: headers de autorización, credenciales ni contenido de archivos.
- Las keys de objetos se sanitizan para prevenir path traversal.
- Las rutas de documentación no están disponibles en producción.
| Error | Causa probable | Solución |
|---|---|---|
ZodError al arrancar |
.env incompleto o variable faltante/invalidada. |
Verifica todas las variables requeridas y sus formatos en la sección de configuración. |
401 en todos los endpoints protegidos |
API_KEY incorrecto o header ausente. |
Envía x-api-key con el valor exacto de API_KEY de tu .env. |
404 en /docs |
NODE_ENV=production; docs deshabilitado. |
Cambia a NODE_ENV=development para entorno local. |
| Error de conexión a R2 | Credenciales incorrectas o bucket inexistente. | Revisa R2_ACCOUNT_ID, claves R2 y existencia real de R2_BUCKET_NAME. |
tsx no encuentra .env |
Falta import "dotenv/config" en src/index.ts. |
Asegura que la importación exista al inicio del entrypoint. |
Error ERR_INVALID_IP_ADDRESS o fallos opacos en importación remota |
El runtime puede invocar el lookup interno esperando múltiples direcciones o la IP pinneada resultó inválida. |
Revisa los logs remote_fetch.dns_resolved, remote_fetch.request_prepared, remote_fetch.lookup_invoked y remote_fetch.request_error en entorno no productivo para confirmar la IP/familia pinneada y el modo de resolución usado por Node. |
- Rama principal:
master. - Flujo: crear rama de trabajo, desarrollar cambios, abrir PR hacia
main. - Conventional Commits requeridos:
feat,fix,docs,test,refactor,chore. - Antes del PR ejecuta:
pnpm lint
pnpm typecheckMIT.