diff --git a/docs/presigned-upload-hardening.md b/docs/presigned-upload-hardening.md index a31b247..f9159b8 100644 --- a/docs/presigned-upload-hardening.md +++ b/docs/presigned-upload-hardening.md @@ -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. diff --git a/docs/tech-stack.md b/docs/tech-stack.md new file mode 100644 index 0000000..9243f62 --- /dev/null +++ b/docs/tech-stack.md @@ -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()`) 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 `_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`