Update plans

This commit is contained in:
2026-05-14 18:48:00 +02:00
parent 5a00a2478e
commit 7aef20fe71
2 changed files with 378 additions and 24 deletions
+192 -24
View File
@@ -3,14 +3,14 @@
## Contexto
Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`.
Esto mejora latencia y descarga la API, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”).
Esto mejora latencia y descarga el host/servicio, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”).
## Qué pasa hoy si el usuario spamea
- Se generan muchas URLs pre-firmadas (costo bajo pero no cero).
- El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB.
- El impacto principal es:
- coste/uso de S3 (storage + requests)
- posible saturación del endpoint de generación de URLs
- posible saturación del componente de generación de URLs
## Objetivo
Reducir abuso y coste, manteniendo:
@@ -18,22 +18,54 @@ Reducir abuso y coste, manteniendo:
- backend sin streams
- workflows asíncronos + MassTransit Outbox
## Qué significa “sólido” aquí
Como el upload es directo a S3 (o compatible), el backend no puede garantizar “single-use” del upload en el lado del storage mientras una URL sea válida. Por tanto, el objetivo realista es:
- Asegurar que **cualquier abuso solo pueda impactar un prefijo temporal** barato de limpiar.
- Poner límites duros (cuotas) en DB para que el coste tenga un techo.
- No aceptar referencias a objetos no válidos (verificación antes de consumir).
- Evitar pérdida de datos en reemplazos (no borrar la versión previa hasta éxito).
## Principio de configuración (no hardcode)
Todas las rutas/prefijos/TTLs/límites deben ser **configurables**.
Regla:
- En `Application`/`Infrastructure` no debe haber strings hardcodeadas de keys S3 (p.ej. `temp/`, `uploads/`, `PlayLists/`, `Tracks/`).
- Las rutas deben centralizarse en opciones/config (p.ej. `StorageRoutes`, `PlayListRoutes`, `TrackRoutes`, o similar), de forma que cambiar la convención de keys no requiera tocar lógica.
Los ejemplos de este documento son **convenciones sugeridas** y deben leerse como plantillas parametrizadas.
## Invariantes de seguridad (reglas que no se rompen)
- El cliente solo puede escribir en rutas temporales bajo `{TempRootPrefix}/...` (o el prefijo temporal equivalente). La ruta final **nunca** recibe credenciales pre-firmadas.
- Cada intent emite un `Key` **nuevo** (GUID). No hay keys “estables” para reusar.
- Una entidad solo apunta a un `pictureName/audioName` si su intent asociado se valida y se consume.
- Reemplazos: el borrado de la versión anterior ocurre **al final** del workflow, tras actualizar DB.
## Prioridad (coste alto)
Si el coste es el principal riesgo, el orden recomendado es:
1) **Cuotas duras en DB** (intents activos + bytes activos) + job de expiración.
2) Prefijo temporal + cleanup por prefijo (lifecycle si existe, y job como fallback).
3) Validación de objeto (HEAD) antes de consumir.
4) (Opcional) Migrar a pre-signed POST si el backend de storage lo soporta bien.
## Decisiones validadas (no tocaría)
- El orden de prioridad (cuotas → prefijo temporal → HEAD → POST) es el más sensato cuando el **coste** es el riesgo principal.
- La advertencia sobre S3-compatible / SeaweedFS es importante: el diseño debe ser seguro sin depender de features avanzadas.
- El principio de no hardcodear rutas y centralizar prefijos/convenciones en config es coherente con el estilo del repo.
- La semántica de errores debe permanecer separada del mapeo a códigos/respuestas (responsabilidad del host/presentación).
- En reemplazos, no borrar la versión anterior hasta éxito del workflow es clave para resiliencia con Courier.
## Mitigaciones recomendadas (pueden coexistir)
### 1) Rate limiting por usuario/IP (primera línea de defensa)
Aplicar limitación en el host/API (no en `Application`). Ejemplos:
- `N` requests/min por `UserId`
- opcional: `M` requests/min por IP
Ventajas:
- Corta abuso sin tocar DB/S3.
- No cambia dominio.
Nota: requiere acceso al proyecto host (controllers/minimal API) para aplicarlo.
### 2) “Upload intents” en DB (control y conciliación)
### 1) “Upload intents” en DB (control y conciliación)
Persistir una intención de upload cuando se genera un pre-signed URL.
> Importante: **no usar el nombre original del fichero** como `pictureName/audioName`. La key debe usar un **identificador generado** (p.ej. GUID) para evitar colisiones, inputs “raros” y facilitar cleanup.
Sugerencia práctica:
- `objectName = {guid}.{ext}` (o directamente `{guid}` si la extensión no aporta valor)
- Guardar `originalFileName` solo como metadata opcional (si se necesita para UI), pero **nunca** como key.
Modelo sugerido (mínimo):
- `UploadIntent`
- `Id`
@@ -41,19 +73,67 @@ Modelo sugerido (mínimo):
- `Bucket`
- `Key`
- `ContentType`
- `ExpectedSizeBytes` (opcional; si el caller conoce el tamaño)
- `ExpiresAt`
- `Status`: `Issued | Consumed | Expired`
- `CreatedAt`
Opcional (útil para auditoría/UX):
- `OriginalFileName`
- `Purpose` (`TrackPicture | TrackAudio | PlayListPicture`)
Reglas:
- Al pedir URL (Request*Upload*):
- insertar intent `Issued`
- imponer límite por usuario: p.ej. `MaxActiveIntentsPerUser = 5`
- si supera el límite: devolver `429 Too Many Requests` (o `Conflict`) según convención del API
- imponer límites por usuario (ver “Cuotas”)
- si supera el límite: fallar con un error semántico (p.ej. `UploadIntentLimitExceeded`)
- Al crear/actualizar entidad (Create*/Update* V2):
- validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado
- validar que el objeto existe en S3 (HEAD) y que tamaño / `Content-Type` cumplen la policy del backend
- marcar intent como `Consumed`
#### Cuotas (anti-cost) 🔴 Alta
Además del conteo de intents, imponer **cuotas por bytes** para que el coste tenga techo.
Recomendación mínima:
- `MaxActiveUploadIntentsPerUser` (número)
- `MaxActiveUploadBytesPerUser` (bytes) — suma de objetos temporales “en vuelo” por usuario
Cómo aproximarlo sin streams:
- Guardar `ExpectedSizeBytes` en el intent (si está disponible).
- Si no se dispone del tamaño, usar un valor conservador por `Purpose` (p.ej. imagen <= X, audio <= Y) y validarlo en HEAD antes de consumir.
> Nota: si el caller puede mentir sobre tamaño, no confíes ciegamente en `ExpectedSizeBytes`; úsalo como guardrail y valida al consumir.
#### Semántica de errores (intent inválido) 🟡 Media
Definir errores semánticos y dejar el mapeo al “caller” (host/presentación):
- `UploadIntentLimitExceeded`
- `UploadIntentNotFoundOrNotAccessible` (si se quiere no filtrar existencia)
- `UploadIntentAlreadyConsumed`
- `UploadIntentExpired`
- `UploadObjectNotFound`
- `UploadObjectPolicyViolation` (tamaño, `Content-Type`, extensión, etc.)
#### Nota sobre race conditions en el límite 🟡 Media
El check “`count(Issued) < Max`” es vulnerable a concurrencia (dos requests paralelas pueden pasar el check y crear 2 intents extra).
Mitigaciones (de menor a mayor robustez):
- Ejecutar **check + insert** en la **misma transacción** con aislamiento fuerte (p.ej. `SERIALIZABLE`).
- Usar un mecanismo de lock por usuario (p.ej. advisory lock en PostgreSQL) durante la emisión.
- Modelar el límite con una tabla/contador por usuario y aplicar **update condicional** (estilo “compare-and-swap”).
#### Job de expiración de intents 🔴 Alta
Aunque `ExpiresAt` esté en DB, hace falta un mecanismo que marque y limpie intents caducados:
- Un job periódico (worker/cron) que:
- marque intents `Issued` con `ExpiresAt < now` como `Expired`
- opcionalmente encole el borrado del objeto S3 (si existe)
Si se aplica lifecycle en S3 (ver sección 2), el job puede ser “solo DB” (marcar `Expired`) y dejar el borrado real a S3.
Adicionalmente, este job es un buen sitio para:
- borrar intents `Expired` antiguos (retención de auditoría configurable)
- emitir métricas/contadores (intents expirados por usuario)
Ventajas:
- Previene spam con reglas duras y auditables.
- Permite cleanup de huérfanos con certeza.
@@ -61,7 +141,7 @@ Ventajas:
Coste:
- entidad + migración + lógica de validación.
### 3) Limitar el scope del pre-signed URL (reducción de daño)
### 2) Limitar el scope del pre-signed URL (reducción de daño)
Aplicar restricciones conservadoras:
- Reducir TTL:
- `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s)
@@ -69,27 +149,115 @@ Aplicar restricciones conservadoras:
- `ContentType` permitido
- extensiones permitidas (`FileType`)
- Usar keys temporales por usuario:
- `PlayLists/TempUploads/{userId}/{guid}.{ext}`
- `Tracks/TempUploads/{userId}/{guid}.{ext}`
- Opción recomendada (facilita lifecycle por prefijo):
- `{TempRootPrefix}/{userId}/playlists/{guid}.{ext}`
- `{TempRootPrefix}/{userId}/tracks/{guid}.{ext}`
- Alternativa (si se quiere mantener todo bajo `uploads/{userId}`):
- `{UploadsRootPrefix}/{userId}/PlayLists/TempUploads/{guid}.{ext}`
- `{UploadsRootPrefix}/{userId}/Tracks/TempUploads/{guid}.{ext}`
Luego, en el workflow/worker:
- mover/copiar de `TempUploads/...` a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`)
- mover/copiar desde la ruta temporal (p.ej. `temp/...` o `.../TempUploads/...`) a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`)
- borrar el objeto temporal
Ventajas:
- Cleanup masivo sencillo (por prefijo `TempUploads/{userId}`)
- Cleanup masivo sencillo (por prefijo `{TempRootPrefix}/`)
- Reduce el impacto de huérfanos.
#### S3 Lifecycle Rule en `TempUploads` 🔴 Alta
Configurar una **Lifecycle Rule** en el bucket para auto-expirar objetos bajo el prefijo de temporales, por ejemplo:
- Prefijo: `{TempRootPrefix}/`
- Expiración: 1–7 días (según tolerancia a reintentos)
Nota: las reglas de lifecycle por **prefijo** no soportan comodines en medio (p.ej. `uploads/*/...`).
Si se usa la alternativa `uploads/{userId}/.../TempUploads/...`, para filtrar correctamente se recomienda:
- cambiar la convención de key para que el prefijo sea común (p.ej. `{TempRootPrefix}/...`), o
- usar **tags** en los objetos (p.ej. `temporary=true`) y filtrar la lifecycle rule por tag (si se puede firmar el tagging en el upload).
Esto evita que el coste de huérfanos crezca indefinidamente incluso si el job de cleanup falla.
#### Cleanup backend (fallback) 🔴 Alta
No dependas solo de lifecycle del storage (en S3 compatibles puede variar). Añadir un job de cleanup que:
- liste por prefijo temporal `{TempRootPrefix}/`
- borre objetos con edad > `TempUploadsRetentionDays`
Este job debe ser:
- incremental (paginación)
- tolerante a errores (reintentos)
- con límites de throughput (para no auto-generar costes por borrar en masa)
#### Restricción de `Content-Type` en S3 🟡 Media
Si el objetivo es enforcement “duro” a nivel de S3 (no solo validación posterior), **pre-signed POST** suele ser más robusto que PUT.
**pre-signed PUT** (lo actual en este repo):
- Permite fijar `Content-Type` si se pre-firma incluyendo ese header.
- Es más limitado para imponer condiciones ricas (tamaño, tags, etc.).
- Recomendación: combinar con validación posterior (HEAD) antes de consumir el intent.
**pre-signed POST** (recomendado si buscas más control):
- Permite policy conditions como:
- `content-length-range` (tamaño máximo)
- `eq`/`starts-with` para `Content-Type`
- key/prefijo (el cliente no elige keys fuera del scope)
- headers requeridos (p.ej. SSE)
- opcional: tagging (p.ej. `temporary=true`) para lifecycle por tag
Trade-off: el cliente sube con `multipart/form-data` (POST), pero el backend sigue sin manejar streams.
#### Nota S3-compatible / SeaweedFS 🟡 Media
No todos los backends S3 compatibles implementan al 100%:
- policy conditions de POST
- tagging en upload
- respuestas/errores idénticos a AWS
Recomendación:
- Diseñar el sistema para ser seguro **sin depender** de condiciones avanzadas (keys únicas + prefijo temporal + cuotas + verificación HEAD).
- Tratar POST/condiciones como una mejora opcional, tras validar compatibilidad del backend.
#### Reemplazos: no borrar originales antiguos hasta éxito ✅
Al actualizar una imagen/audio existente:
- Subir el nuevo binario a la ruta temporal (p.ej. `{TempRootPrefix}/.../{newGuid}`).
- Procesar (resize/transcode) y subir a rutas finales con nombres nuevos.
- Actualizar la entidad en DB para apuntar a los nuevos `*PictureName/*AudioFolderName`.
- **Solo al final** borrar los objetos antiguos (original + sizes / audio procesado), cuando el workflow haya completado.
Esto evita perder la versión anterior si el worker falla a mitad (punto crítico para resiliencia).
## Validación del objeto antes de consumir (HEAD) 🔴 Alta
Antes de consumir un intent y actualizar DB:
- `HEAD Bucket/Key`
- validar `Content-Type` esperado (según `Purpose`)
- validar tamaño máximo (`MaxUploadBytes` o por propósito)
- (opcional) validar extensión inferida de `Key` si se usa
Si falla la validación:
- no consumir el intent
- marcarlo como violación o dejarlo expirar
- el cleanup se encarga del objeto
## Orden de implementación sugerido
1) Rate limiting en API (rápido + mayor impacto)
2) Upload intents en DB (límite de activos + validación al crear/actualizar)
3) Reducir TTL y mover a rutas temporales (`TempUploads/...`) + cleanup
1) Upload intents en DB (límite de activos + validación al crear/actualizar)
2) Reducir TTL y mover a rutas temporales + S3 lifecycle + cleanup
3) Si se requiere enforcement fuerte: migrar a pre-signed POST + policy conditions
## Parámetros recomendados (config)
- `UploadUrlExpiresInSeconds` (default: 120)
- `MaxActiveUploadIntentsPerUser` (default: 5)
- `MaxActiveUploadBytesPerUser` (default sugerido: depende de coste; p.ej. 200MB)
- `AllowedPictureExtensions` / `AllowedAudioExtensions`
- `AllowedContentTypes` (lista blanca)
- `TempUploadsRetentionDays` (default: 3)
- `PresignedUploadMethod` (default sugerido: `POST` si se quiere enforcement fuerte; `PUT` si se prioriza simplicidad)
- `MaxUploadBytes` (límite máximo de tamaño por upload)
Jobs (config):
- `UploadIntentsExpirationJobIntervalSeconds`
- `TempUploadsCleanupJobIntervalSeconds`
- `ExpiredIntentsRetentionDays`
Rutas/prefijos (ejemplos):
- `TempRootPrefix` (default sugerido: `temp`)
- `UploadsRootPrefix` (default sugerido: `uploads`)
## Notas
- Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos.
+186
View File
@@ -0,0 +1,186 @@
# Tech stack y arquitectura (MusifyBackend)
Este documento describe **qué tecnologías usa este repo** y **cómo encajan** (capas, workers, mensajería, storage, observabilidad). Está pensado para alguien que llega de cero o para una IA que necesita contexto.
## Foto rápida
- Runtime: **.NET 10** (C#)
- Estilo: **Clean Architecture** (capas `Domain` / `Application` / `Infrastructure`)
- Casos de uso: **CQRS** con **DispatchR** (similar a MediatR)
- Persistencia: **Entity Framework Core** + **PostgreSQL** (provider `Npgsql`)
- Workers / mensajería: **MassTransit** + **RabbitMQ**
- Orquestación de workflows: **MassTransit Courier / RoutingSlip** (Activities)
- Storage de objetos: **SeaweedFS** expuesto como **S3-compatible** (vía AWS SDK `IAmazonS3`)
- Observabilidad: **OpenTelemetry** (traces + metrics, export OTLP)
- Procesado multimedia:
- Imágenes: **SixLabors.ImageSharp** (resize, WebP)
- Audio: **ffmpeg** (transcode a DASH)
## Estructura del repo (capas)
### `Domain/` (núcleo de negocio)
- Entidades puras (sin dependencias de infraestructura).
- Ejemplos: `Track`, `PlayList`, `Upload`, etc.
- Estados de negocio relevantes:
- `ProcessingStatus` (procesado en curso/finalizado, etc.).
- `LifeCycleStatus` (p.ej. `Active`, `Removing`) para deletes asíncronos.
### `Application/` (casos de uso + contratos)
- Contiene comandos/queries/handlers por feature.
- CQRS:
- `Commands/` y `Queries/` por feature (`PlayLists`, `Tracks`, `Users`, ...).
- `Handlers/` ejecutan la lógica de aplicación (coordinar DB + eventos), pero **no implementan** detalles de infraestructura.
- Contratos con infraestructura: `Application/Abstractions/Infrastructure/*`
- `IDatabase` (abstracto de EF DbContext)
- `IEventBus` (publicación de eventos)
- `IStorageService`, `IPictureService`, `IAudioTranscoderService`, etc.
- Eventos de integración: `Application/Events/*`
- Se publican desde `Application` y se consumen en `Infrastructure`.
### `Infrastructure/` (implementaciones + integraciones)
- Implementa las abstracciones de `Application`.
- Contiene:
- EF Core DbContext y migraciones
- MassTransit (consumers, activities, routing slips, outbox)
- Integración S3 (SeaweedFS) mediante AWS SDK
- Observabilidad OpenTelemetry
- Servicios de procesado (ImageSharp/ffmpeg)
## CQRS con DispatchR
- Los “use cases” se modelan como:
- **Commands**: mutaciones (crear/actualizar/borrar).
- **Queries**: lecturas.
- Los handlers viven en `Application/*/Handlers/` y dependen de:
- `IDatabase` para persistencia (sin acoplarse al DbContext concreto).
- `IEventBus` para emitir eventos cuando hay trabajo asíncrono.
## Persistencia (EF Core + PostgreSQL)
- DbContext: `Infrastructure/Persistence/Database.cs`
- Usa `UseNpgsql(connectionString)`.
- Incluye tablas de MassTransit Outbox/Inbox (ver sección de mensajería).
- DI: `Infrastructure/Persistence/DatabaseDependencyInjection.cs`
- Config: `Infrastructure/Configuration/DatabaseConfiguration.cs` (sección `Database`)
- `ConnectionString`
## Mensajería y workers (MassTransit + RabbitMQ)
### Qué se usa
- Broker: RabbitMQ (`MassTransit.RabbitMQ`).
- MassTransit está configurado en dos modos:
1) **Cliente** (publica eventos): `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` → `AddMassTransitClient()`
2) **Workers** (consumen y ejecutan activities): `AddMassTransitConsumers()`
### Outbox / Inbox (robustez)
- Se usa **EntityFramework Outbox** (`AddEntityFrameworkOutbox<Database>()`) con:
- `UsePostgres()`
- `UseBusOutbox()`
- `QueryDelay = 1s`
- `DuplicateDetectionWindow = 30m`
- En el DbContext se registran entidades de MassTransit:
- `AddInboxStateEntity()`
- `AddOutboxMessageEntity()`
- `AddOutboxStateEntity()`
Idea clave:
- La outbox asegura que, tras confirmar la transacción de DB, los mensajes/eventos pendientes se publican de forma fiable.
### Courier / RoutingSlip (workflows asíncronos)
Para operaciones multi-paso (p.ej. imagen/audio y deletes), se usa **MassTransit Courier**:
- Un “RoutingSlip” es una lista de pasos.
- Cada paso es una **Activity** (con args + log opcional).
- Existen dos tipos que aparecen en este repo:
- `ExecuteActivity` (endpoint `<name>_execute`), típicamente acciones atómicas.
- `Activity` (con compensación/log), típico para pasos de workflow.
Dónde mirar:
- Builders: `Infrastructure/MassTransit/RoutingSlip/Builders/*`
- Activities: `Infrastructure/MassTransit/Activities/*`
- Consumers (punto de entrada del workflow): `Infrastructure/MassTransit/Consumers/*`
- Registro de endpoints: `Infrastructure/MassTransit/DependencyInjection/*`
Ejemplos de workflows presentes:
- Procesado de imagen (resize + upload + update DB)
- Procesado de audio (transcode DASH + upload + update DB)
- Deletes asíncronos (marcar “removing”, borrar objetos, borrar filas)
### Config de MassTransit
- `Infrastructure/Configuration/MassTransitConfiguration.cs` (sección `MassTransit`)
- `Address`
- `Username`
- `Password`
## Storage de objetos (SeaweedFS como S3-compatible)
### Qué se usa
- SDK: `Amazon.S3` (`IAmazonS3`).
- Implementación: `Infrastructure/Services/StorageService.cs` (usa `IAmazonS3`).
- DI: `Infrastructure/Services/ServicesDependencyInjection.cs` → `AddStorageService()`
### Config
- `Infrastructure/Configuration/InfrastructureStorageConfiguration.cs` (sección `InfrastructureStorage`)
- `Address` (ServiceURL)
- `AccessKey` / `SecretAccessKey`
- `ForcePathStyle` (útil en S3-compatibles)
- `UseHttp`
Notas prácticas (S3-compatible):
- Algunas features avanzadas de AWS S3 pueden no comportarse igual (depende de SeaweedFS). Diseñar workflows para que sean correctos con el subconjunto básico: PUT/GET/HEAD/LIST/DELETE y paths bien definidos.
## Procesado multimedia
### Imágenes
- Librería: `SixLabors.ImageSharp`
- Servicio: `Infrastructure/Services/PictureService.cs`
- Contrato: `Application/Abstractions/Infrastructure/IPictureService.cs`
- Patrón: workers descargan/transforman/suben; la app coordina por eventos.
### Audio
- Herramienta: `ffmpeg` (binario externo)
- Config: `Infrastructure/Configuration/AudioTranscoderConfiguration.cs` (sección `AudioTranscoder`)
- Timeout, codec/bitrate/channels/sample-rate, segmentación DASH, etc.
- Servicio: `Infrastructure/Services/AudioTranscoderService.cs`
- Contrato: `Application/Abstractions/Infrastructure/IAudioTranscoderService.cs`
### Directorio temporal (workers)
- Config: `Infrastructure/Configuration/WorkerConfiguration.cs` (sección `Workers`)
- `Routes.TemporaryFilesDirectory`
## Observabilidad (OpenTelemetry)
- DI: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs`
- Config: `Infrastructure/Configuration/OpenTelemetryConfiguration.cs` (sección `OpenTelemetry`)
- `OtlpEndpoint`
Qué instrumenta:
- Tracing:
- `MassTransit`
- ASP.NET Core
- HttpClient
- EF Core
- Exporter OTLP
- Métricas:
- `MassTransit`
- ASP.NET Core
- HttpClient
- Runtime + Process
- Exporter OTLP
## Identidad (Keycloak)
- El repo asume integración con **Keycloak** (ver `AGENTS.md` y nombres como `GetUserByKeycloakQuery`).
- La capa `Application` trata el “Keycloak Id” como identificador de usuario en algunos casos de uso.
- La integración concreta (tokens, validación, etc.) puede vivir fuera de estas 3 capas o en otro proyecto/host.
## Glosario rápido
- **Command / Query**: operación de escritura / lectura.
- **Handler**: clase que ejecuta el caso de uso (CQRS).
- **EventBus**: abstracción para publicar eventos (MassTransit por debajo).
- **Consumer**: “listener” de mensajes en una cola.
- **RoutingSlip**: workflow compuesto por Activities.
- **Activity**: paso del workflow (puede tener log/compensación).
- **Outbox**: patrón para publicar eventos de forma fiable tras commit de DB.
## “Dónde empiezo a leer” (ruta recomendada)
1) Arquitectura general: `AGENTS.md`
2) Casos de uso: `Application/*/Commands`, `Application/*/Queries` y sus `Handlers/`
3) Persistencia: `Infrastructure/Persistence/Database.cs` + migraciones
4) Mensajería/workflows: `Infrastructure/MassTransit/*`
5) Integraciones: `Infrastructure/Services/*` (S3, imagen, audio)
6) Observabilidad: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs`