Capa de interoperabilidad · Prueba técnica de arquitectura

ADR-029: los datos sensibles se enmascaran al pasar a la DLQ

Aceptado el 15/11/2026 por Andrés (opción A). Lo implementa el carril `pagos-y-adaptadores` en payment-service; order-service aplica el mismo marcador al outbox (`entrada-valida`, R8).

Contexto

docs/to-be/06-eventos.md fija que el mensaje de reintento y de DLQ (dead letter queue: cola donde quedan los mensajes que agotaron sus reintentos) es el original, sin cambios en su payload ni en sus cabeceras CloudEvents. Esa regla sirve: el mensaje apartado muestra el contrato y los datos que viajaron, y se puede reprocesar tal cual.

La revisión independiente (docs/revision-final.md, hallazgo R19) encontró que payment-service copia completo un PaymentRequested a su DLQ, con payment.token (el token de la pasarela que envía el canal). La DLQ guarda 30 días. La regla 5 de CLAUDE.md prohíbe datos de pago en eventos. Quitar el token chocaba con 06 y con el contrato, que declaraba en la DLQ el mismo PaymentRequested, cuyo esquema exige token con mode: CAPTURE.

Hechos que pesan:

Opciones

Opción A favor En contra
A. Enmascarar los datos sensibles al pasar a la DLQ: valor fijo --data_redacted-- y cabecera andes_payload_redacted con los campos enmascarados; el reintento conserva el original Cumple la regla 5 en la cola de 30 días; el resto del mensaje sigue siendo el original y se lee contra el contrato; la cabecera dice qué falta Excepción a la regla de 06; el mensaje enmascarado no sirve para repetir la operación
B. No cambiar nada y anotar el sacrificio Sin trabajo; la DLQ se puede reprocesar tal cual 30 días de tokens en un tópico; contradice la regla 5

Decisión

Opción A, con una regla general para todos los servicios:

  1. El mensaje de reintento y de DLQ es el original, sin cambios, para poder entender el contrato y los datos que viajaron. La única excepción son los datos sensibles: siempre se enmascaran al pasar a la DLQ y nunca aparecen en logs.
  2. Enmascarar es reemplazar el valor por el marcador fijo --data_redacted--, uno solo para toda la capa, sin nombre de proveedor ni de campo (en payment-service, la constante SensitiveFields.MASK), y listar los campos en la cabecera andes_payload_redacted, separados por coma, con la ruta del campo en el mensaje (por ejemplo payment.token para el token del PaymentCommand). Si el payload no es JSON, no se pueden ubicar los campos: se enmascara entero y la cabecera vale *.
  3. Cada servicio tiene una sola lista de campos sensibles por tipo de mensaje, como fuente única. En payment-service es consumer/SensitiveFields.java.
  4. El tópico de reintento conserva el original, porque el reintento lo necesita para repetir la operación.
  5. El contrato declara el mensaje enmascarado en el canal DLQ (PaymentRequestedMasked, con token igual a MaskedValue).

Qué se sacrifica