Saltar a contenido

ADR-0007 · Strategy Pattern para conectores (packages/connector-sdk)

  • Status: Accepted (revisado 2026-05-13: +notifications-internal V1)
  • Date: 2026-04-30 · Revisado: 2026-05-13
  • Deciders: Cristian Fernández (Zerviz Group)
  • Related: ADR-0004 (eventing), ADR-0005 (auth M2M), CLAUDE.md rule #5 (estrategia mandatoria).

Context

Regla #5 de CLAUDE.md: "Strategy Pattern es mandatorio para todos los conectores sociales y de contact-center". El legacy mezcla META/, 360/ y Five9 dentro del monolito sin contrato común. Este ADR formaliza el SDK que todos los conectores deben implementar.

Decision

Contrato ConnectorStrategy (TypeScript)

// packages/connector-sdk/src/contract.ts
export interface ConnectorContext {
  tenantId: string;
  correlationId: string;
  connectorId: string;
  logger: Logger;
  tracer: Tracer;
  metrics: Metrics;
  secretsResolver: (secretName: string) => Promise<string>;
  breaker: CircuitBreaker;
}

export interface InboundEvent {
  channel: 'whatsapp' | 'messenger' | 'instagram' | 'email' | 'webchat' | 'voice' | 'sms' | 'notifications';
  externalMessageId: string;
  from: { id: string; name?: string; metadata?: Record<string, unknown> };
  to: { id: string; metadata?: Record<string, unknown> };
  receivedAt: string; // ISO8601
  type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'location' | 'interactive';
  content: { text?: string; mediaUrl?: string; interactive?: unknown };
  raw: unknown; // payload original del proveedor
}

export interface OutboundRequest {
  channel: InboundEvent['channel'];
  to: { id: string };
  type: InboundEvent['type'] | 'template';
  content: { text?: string; mediaUrl?: string; templateName?: string; templateParams?: unknown[] };
  idempotencyKey: string;
}

export interface OutboundResult {
  externalMessageId: string;
  status: 'sent' | 'queued' | 'failed';
  failureReason?: string;
}

export interface ConnectorStrategy {
  readonly id: string;          // ej. 'whatsapp-meta-cloud', 'whatsapp-360dialog', 'five9-digital'
  readonly version: string;     // SemVer del strategy
  readonly capabilities: {
    inbound: boolean;
    outbound: boolean;
    media: boolean;
    templates: boolean;
    interactive: boolean;
  };

  // Inbound
  verifyWebhook(req: WebhookRequest, ctx: ConnectorContext): Promise<boolean>;
  parseInbound(req: WebhookRequest, ctx: ConnectorContext): Promise<InboundEvent[]>;

  // Outbound
  send(req: OutboundRequest, ctx: ConnectorContext): Promise<OutboundResult>;

  // Health
  health(ctx: ConnectorContext): Promise<{ ok: boolean; details?: Record<string, unknown> }>;
}

Registry y dispatch

  • Registry vive en tenant-mgmt: tabla DDB ze-{env}-connector-registry con PK TENANT#<id>, SK CONNECTOR#<id>. Cada entrada referencia strategy_id, version, secret_arn, enabled.
  • connector-orchestrator consulta el registry para resolver qué strategy aplicar a una OutboundRequest según tenant_id + channel + ruteo del flow.
  • Dispatch: el orchestrator carga la strategy via dynamic import (packages/connector-sdk/strategies/<id>.ts) y la invoca con ConnectorContext materializado.
  • Idempotencia: OutboundRequest.idempotencyKey se persiste en DDB ze-outbound-idempotency (TTL 24 h) antes de invocar la strategy. Reintentos con misma key ⇒ retorno cacheado.

Strategies V1

Strategy ID Channel Source heredado Notas
whatsapp-meta-cloud whatsapp META/conector.mjs (legacy) Webhook firma x-hub-signature-256, outbound Meta Graph v23.
whatsapp-360dialog whatsapp 360/*.mjs + ZE-D360-Sender ZIP (reutilizable) Header D360-API-KEY.
five9-digital webchat handler.js rutas /five9/* Auth anon + DDB session cache.
webchat-internal webchat src/widget Para el widget propio embebible.
notifications-internal notifications aws/lambdas/src/notifications/index.mjs + DDB zengine_notification_flows (legacy productivo) Canal de notificaciones push/email. Sumado en revisión 2026-05-13 tras detectar deploy productivo (delta docs/discovery/08-legacy-delta.md §6.1).

V2+ (placeholders sin implementación V1): messenger-meta, instagram-meta, email-ses, sms-pinpoint, voice-amazon-connect, genesys-cloud.

Tests de contrato

  • packages/connector-sdk/src/contract.test.ts: suite que cualquier strategy debe pasar:
  • verifyWebhook rechaza payload con firma inválida.
  • parseInbound produce InboundEvent[] con shape válido (Zod schema).
  • send con idempotencyKey repetido devuelve mismo externalMessageId.
  • health retorna { ok: false } cuando se mockea fallo del proveedor.
  • CI corre la suite por cada strategy; falla ⇒ bloquea merge.

Hot-swap y versioning

  • Cambio de versión del strategy = bump SemVer + nuevo entry en registry. Tenant puede elegir versión (gradual rollout).
  • Strategy puede deprecarse anunciando deprecated_at en el registry; sunset 90 días después.

Consequences

Positive

  • Agregar canal nuevo = una strategy + entrada en registry. Sin tocar inbound-router, flow-engine, outbound-dispatcher.
  • Tests de contrato detectan regresiones cross-strategy.
  • Per-tenant strategy version permite rollouts graduales.

Negative

  • Devs deben respetar el contrato; tentación de saltarse ConnectorContext ⇒ regla en code-review + lint custom.

Alternatives considered

  • Plugin system con node:vm: rechazado (legacy ya lo usa para flows; superficie de inyección).
  • Microservicio por canal sin SDK común: rechazado, duplica retry/breaker/idempotencia.
  • Common adapter sin Strategy formal: rechazado, no resiste evolución a 7+ canales.

Tenant-isolation impact

  • Registry per-tenant ⇒ tenant A puede tener whatsapp-360dialog v1.2 y tenant B v2.0 simultáneos.
  • Secrets de cada strategy son per-tenant (KMS CMK del tenant).

Blast radius

  • Bug en strategy X ⇒ sólo el canal X. Circuit breaker abre, fallback opcional a strategy alternativa si está configurada (e.g. fallback whatsapp-meta-cloudwhatsapp-360dialog).

Cost note

  • DDB registry: < 1k items, free.
  • Test suite: incluida en CI.

ISO 27001 controls touched

  • A.14.2.5 (secure system engineering): contrato + tests obligatorios.
  • A.14.2.7 (outsourced development): cualquier conector externo provisto por terceros pasa la suite.

Sources

  • CLAUDE.md rule #5.
  • Playbook §6.4 (connector-strategist subagent).
  • docs/discovery/03-code-aws-crosswalk.md §4.1.bis (ZE-D360-Sender reutilizable).
  • ADR-0004 (circuit breaker comparte packages/connector-sdk/breaker.ts).