Skip to main content
Traducción. Autoritativo: fs-msg-0006-channel-adapters.md.

Contexto

Cuatro adaptadores, un contrato. NotificationChannelPort (enmienda A1) dice que un adaptador hace exactamente una cosa: send(destinatario, contenidoRenderizado, opts) -> ProviderResult. No decide si enviar, no elige canal, no verifica consentimiento. Esas decisiones se tomaron aguas arriba, y un adaptador que las cuestiona es un adaptador que va a discrepar con la cascada. Van como una sola entrega y no cuatro porque implementan el mismo contrato con los mismos tests; separarlos repetiría un spec cuatro veces e invitaría cuatro interpretaciones. El adaptador de webhook saliente es el raro: su destinatario es un sistema y no una persona, pero pasa por la misma cascada, los mismos carriles y el mismo seguimiento de estados. Esa uniformidad vale más que la pequeña incomodidad.

Alcance (normativo)

  • ResendEmailAdapter, InAppAdapter, WebhookAdapter, FcmPushAdapter.
  • messaging.in_app_notifications con estado de lectura.
  • messaging.webhook_endpoints con firma HMAC y protección SSRF.
  • Normalización del resultado del proveedor a nuestro vocabulario de estados.
  • Clasificación de fallas por adaptador: reintentable o terminal.

Fuera de alcance (normativo)

  • SMS — FS-MSG-0009, tras su ADR de proveedor.
  • WhatsApp y Live Activities — F2.
  • Actualizaciones de wallet passes, que son un adaptador de NotificationChannelPort entregado con FS-LOY-0014.
  • Decidir, renderizar o encolar.

Comportamiento (normativo)

  1. Un adaptador entrega y reporta, nada más. Nunca consulta consentimiento, supresión ni configuración. Si lo necesitara, la cascada estaba mal.
  2. Todo adaptador devuelve un resultado normalizado: aceptado con id del proveedor, o fallido con clasificación retryable o terminal. Un error que el adaptador no puede clasificar es retryable — reintentar una falla terminal desperdicia un job, pero no reintentar una transitoria pierde un mensaje.
  3. Ningún SDK de proveedor se importa fuera de su archivo de adaptador. Impuesto por lint (R21).
  4. Las notificaciones in-app se guardan con estado de lectura y son el único canal donde el destinatario consulta en vez de recibir. Obedecen la cascada igual que cualquier otro.
  5. Los webhooks salientes van firmados con HMAC y timestamp, protegidos contra SSRF (sin rangos privados, sin redirecciones hacia ellos), y su inclusión de PII es opt-in por endpoint, apagada por defecto.
  6. El push se entrega por FCM para ambas plataformas. Un token inválido devuelve terminal y suprime ese token, igual que un rebote duro.
  7. Los adaptadores son sin estado y desplegables independientemente. Agregar uno es un archivo nuevo más una fila de catálogo, nunca un cambio en el despachador.
  8. Todo adaptador registra la latencia del proveedor, para que un proveedor lento sea visible antes de volverse una cola.

Datos (normativo)

API (normativo)

Eventos (normativo)

Ninguno emitido por los adaptadores. Producen resultados de proveedor, que FS-MSG-0003 convierte en historial y eventos.

Criterios de aceptación (normativo)

  1. Ningún SDK de proveedor se importa fuera de un archivo de adaptador — impuesto por lint con fixture.
  2. Todo adaptador devuelve resultado normalizado, y un error inclasificable es retryable.
  3. Un webhook hacia un rango de IP privado se rechaza antes de que salga cualquier request.
  4. Una firma de webhook verifica con el algoritmo documentado y un timestamp reproducido se rechaza.
  5. Un token de push inválido devuelve terminal y suprime ese token.
  6. Las notificaciones in-app respetan la cascada.
  7. Un endpoint que falla consecutivamente sobre el umbral se desactiva y se notifica al tenant.
  8. Negativo: ningún adaptador lee consentimiento, supresión ni configuración de canal.

Ejecución

Un solo slice desde la perspectiva del worker. Los adaptadores viven junto a backend/workers; el puerto en packages/core. Segunda mitad de TS-002.

Preguntas abiertas

Changelog

Registro de entrega

Aún no implementado.