ADR-0007 · Strategy Pattern para conectores (packages/connector-sdk)¶
- Status: Accepted (revisado 2026-05-13: +
notifications-internalV1) - Date: 2026-04-30 · Revisado: 2026-05-13
- Deciders: Cristian Fernández (Zerviz Group)
- Related: ADR-0004 (eventing), ADR-0005 (auth M2M),
CLAUDE.mdrule #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 DDBze-{env}-connector-registrycon PKTENANT#<id>, SKCONNECTOR#<id>. Cada entrada referenciastrategy_id,version,secret_arn,enabled. connector-orchestratorconsulta el registry para resolver qué strategy aplicar a unaOutboundRequestsegúntenant_id+channel+ ruteo del flow.- Dispatch: el orchestrator carga la strategy via dynamic import (
packages/connector-sdk/strategies/<id>.ts) y la invoca conConnectorContextmaterializado. - Idempotencia:
OutboundRequest.idempotencyKeyse persiste en DDBze-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 |
META/conector.mjs (legacy) |
Webhook firma x-hub-signature-256, outbound Meta Graph v23. |
|
whatsapp-360dialog |
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:verifyWebhookrechaza payload con firma inválida.parseInboundproduceInboundEvent[]con shape válido (Zod schema).sendconidempotencyKeyrepetido devuelve mismoexternalMessageId.healthretorna{ 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_aten 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.2y tenant Bv2.0simultá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-cloud↔whatsapp-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.mdrule #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).