Saltar a contenido

inbound-router

Lambda que recibe mensajes de canales (WhatsApp Meta/360, webchat) y los normaliza para invocar al flow-engine. Dispatcher por eventSource.

Endpoints

API GW HTTP https://eqeloz3trh.execute-api.us-east-2.amazonaws.com:

Path Quién llama Async/Sync
POST /v1/webchat/messages Widget / clientes REST sync (decisión ADR-0013)
GET /v1/webchat/conversations/:id/messages Widget (long-poll) sync
POST /v1/webhooks/whatsapp-meta Meta Cloud async (cola SQS)
POST /v1/webhooks/whatsapp-360dialog 360dialog async
POST /v1/webhooks/five9-digital Five9 async

Dispatcher por eventSource

El mismo handler atiende invocaciones desde API Gateway (HTTP), EventBridge y SQS. El dispatcher en handler.ts decide:

if (event.requestContext?.http) {
  // API GW HTTP → webchat sync o webhook
} else if (event.Records?.[0]?.eventSource === 'aws:sqs') {
  // SQS → procesamiento batch async de webhooks
} else if (event.source === 'aws.events') {
  // EventBridge scheduled
}

Strategies de inbound

Strategy Trigger Verificación
webchat-http POST /v1/webchat/messages tenant + widget + origen CORS + idempotency key
whatsapp-meta-cloud POST webhook Meta HMAC SHA-256 con app_secret (header x-hub-signature-256)
whatsapp-360dialog POST webhook 360 API key en header + HMAC opcional
five9-digital POST webhook Five9 basic auth + IP allowlist

Las strategies viven en services/inbound-router/src/strategies/. Cada una expone verify(event) → tenantId y normalize(event) → CanonicalMessage.

HMAC verification (ejemplo Meta)

import crypto from 'node:crypto';

export function verifyMetaSignature(rawBody: string, header: string, appSecret: string): boolean {
  const [algo, sig] = header.split('=');
  if (algo !== 'sha256') return false;
  const expected = crypto.createHmac('sha256', appSecret).update(rawBody, 'utf8').digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Secret en Secrets Manager /zen/dev/whatsapp-meta/<tenantId>/app-secret.

Dedup

Tabla DDB zen-dev-dedup con TTL 24h:

  • PK: TENANT#<id>#KEY#<idempotency-key>.
  • Atributos: messageId, responseBody (para devolver el mismo response en hits cached), expiresAt.

CanonicalMessage

Estructura interna normalizada a partir de cualquier canal:

type CanonicalMessage = {
  tenantId: string;
  widgetId?: string;        // sólo webchat
  channel: 'webchat' | 'whatsapp' | 'whatsapp-360' | 'five9';
  conversationId: string;
  messageId: string;
  text?: string;
  attachments: Attachment[];
  user: { id: string; firstName?: string; email?: string; phone?: string };
  metadata: Record<string, unknown>;
  receivedAt: string;
  correlationId: string;
};

Errores comunes

Error Causa Mitigación
InvalidSignatureError HMAC no coincide Verificar secret + raw body sin reparseo
TenantNotFoundError tenant inexistente o suspendido Devolver 404 al canal externo si conviene
DedupConflict Misma idempotency key con body diferente 409
RateLimitExceeded >30 msg/min/sesión 429 con Retry-After