@erlancarreira/evolution-chat 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bridge/index.cjs CHANGED
@@ -318,28 +318,31 @@ var ChatBridge = class {
318
318
  }
319
319
  }
320
320
  const existing = await store.getSessionByVisitorPhone(phone);
321
- if (existing !== null) {
321
+ if (existing !== null && existing.status === "active") {
322
322
  const firstTarget2 = existing.groupJid === null ? toWhatsappJid(normalizePhone(cfg.platformNumber)) : existing.groupJid;
323
- let waMessageId2;
324
323
  try {
325
- ({ waMessageId: waMessageId2 } = await this.deps.client.sendText(
324
+ const { waMessageId: waMessageId2 } = await this.deps.client.sendText(
326
325
  cfg.instance,
327
326
  firstTarget2,
328
327
  formatFirstMessage(existing.code, name, message)
329
- ));
328
+ );
329
+ await store.registerSentMessageId(existing.id, waMessageId2);
330
+ const initial2 = await store.appendMessage({
331
+ sessionId: existing.id,
332
+ direction: "visitor",
333
+ body: message,
334
+ waMessageId: waMessageId2,
335
+ status: "sent"
336
+ });
337
+ return { session: existing, messages: [initial2] };
330
338
  } catch (error) {
331
- await store.markStatus(existing.id, "failed");
332
- throw new ChatError("Falha ao enviar a primeira mensagem para o grupo", "send_failed", error);
339
+ if (isTargetGoneError(error)) {
340
+ await this.closeSessionCleanup(existing, "send_target_gone");
341
+ } else {
342
+ await store.markStatus(existing.id, "failed");
343
+ throw new ChatError("Falha ao enviar a primeira mensagem para o grupo", "send_failed", error);
344
+ }
333
345
  }
334
- await store.registerSentMessageId(existing.id, waMessageId2);
335
- const initial2 = await store.appendMessage({
336
- sessionId: existing.id,
337
- direction: "visitor",
338
- body: message,
339
- waMessageId: waMessageId2,
340
- status: "sent"
341
- });
342
- return { session: existing, messages: [initial2] };
343
346
  }
344
347
  const code = generateSessionCode();
345
348
  const visitorJid = toWhatsappJid(phone);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bridge/index.ts","../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/index.ts — superfície pública do subpath \"@erlancarreira/evolution-chat/bridge\".\n// Portas (contratos) + ConversationRouter (decisão de roteamento) + ChatBridge (orquestração)\n// e a Strategy de formatação. Adapters concretos são exportados pelos seus próprios subpaths\n// (./transports/supabase, ./next).\n\nexport * from \"./types\";\nexport * from \"./router\";\nexport * from \"./format\";\nexport * from \"./bridge\";\n\n// Erros de domínio compartilhados: o consumidor externo (ex.: stores do LMS) precisa\n// importar ChatError/ChatErrorCode daqui para que `instanceof ChatError` funcione —\n// cópias locais do tipo nunca casam com os erros lançados pelas rotas do SDK.\nexport * from \"../errors\";\n\n// Tipos de domínio que aparecem nas assinaturas das portas, reexportados para que o\n// consumidor do subpath ./bridge não precise importar do caminho interno src/types.\nexport type {\n ChatEvent,\n ChatMessage,\n ChatMessageDirection,\n ChatMessageStatus,\n ChatSession,\n ChatSessionStatus,\n} from \"../types\";\n","// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (modo grupo e modo direto):\n// - jid de grupo (@g.us): sessão por groupJid (comportamento original).\n// - jid pessoal (1:1, modo direto): sessão pelo telefone do visitante.\n// - isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// - sem texto → not_text\n// - caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(\n private readonly store: Pick<\n SessionStore,\n \"getSessionByGroupJid\" | \"getSessionByVisitorPhone\" | \"isEcho\"\n >,\n ) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n let session: Awaited<ReturnType<SessionStore[\"getSessionByGroupJid\"]>> = null;\n if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {\n session = await this.store.getSessionByGroupJid(msg.jid);\n } else {\n // Modo direto: o jid da conversa 1:1 é o JID pessoal do visitante (ex.: 5511...@s.whatsapp.net).\n const phone = msg.jid.split(\"@\")[0] ?? \"\";\n if (phone.length === 0) return { action: \"ignore\" };\n session = await this.store.getSessionByVisitorPhone(phone);\n }\n\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport { type EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession, GroupParticipantChange } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\n/**\n * Detecta erro da Evolution que indica que o destino sumiu: grupo apagado/saiu,\n * participante removido, número bloqueado, etc. Usado para encerrar a sessão com\n * aviso em vez de deixar o composer travado num envio que nunca chega.\n * Sinais: status 404/410 (grupo inexistente / não é participante) ou palavras-chave\n * no corpo/mensagem da resposta.\n */\nfunction isTargetGoneError(error: unknown): boolean {\n if (error === null || typeof error !== \"object\") return false;\n const status = (error as { status?: unknown }).status;\n if (status === 404 || status === 410) return true;\n const text = `${String((error as { message?: unknown }).message ?? \"\")} ${String(\n (error as { body?: unknown }).body ?? \"\",\n )}`.toLowerCase();\n return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n // Reusa a conversa ativa do mesmo visitante (um grupo/thread por cliente) em vez de\n // abrir um grupo novo a cada reabertura — evita a proliferação de grupos no WhatsApp.\n const existing = await store.getSessionByVisitorPhone(phone);\n if (existing !== null) {\n const firstTarget =\n existing.groupJid === null\n ? toWhatsappJid(normalizePhone(cfg.platformNumber))\n : existing.groupJid;\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(existing.code, name, message),\n ));\n } catch (error) {\n await store.markStatus(existing.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n await store.registerSentMessageId(existing.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: existing.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n return { session: existing, messages: [initial] };\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n const direct = cfg.createGroup === false;\n\n let groupJid: string | null = null;\n if (!direct) {\n groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n // Imagem de capa do grupo (logo do site / upload do painel). Cosmético: falha não aborta.\n const picture = (cfg.groupImage ?? \"\").trim();\n if (picture !== \"\") {\n try {\n await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);\n } catch {\n // ignora falha de imagem — o atendimento segue sem a capa.\n }\n }\n }\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n mode: direct ? \"direct\" : \"group\",\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n // Em modo direto a conversa é 1:1 com a plataforma; senão, vai para o grupo.\n let firstTarget: string;\n if (direct) {\n firstTarget = platformJid;\n } else {\n if (groupJid === null) throw new ChatError(\"Falha ao criar o grupo\", \"group_create_failed\");\n firstTarget = groupJid;\n }\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n\n // Destino: grupo (modo grupo) ou número da plataforma (modo direto 1:1, groupJid nulo).\n let target: string;\n if (session.groupJid === null) {\n target = toWhatsappJid(normalizePhone(cfg.platformNumber));\n } else {\n target = session.groupJid;\n }\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n target,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n // Se o destino sumiu (grupo saiu/apagado, número bloqueado), encerra a sessão com\n // aviso claro e sai do grupo órfão (best-effort) em vez de deixar o composer travado.\n if (isTargetGoneError(error)) {\n await this.closeSessionCleanup(session, \"send_target_gone\", {\n systemMessage:\n \"Não foi possível entregar a mensagem: o grupo não está mais disponível. Conversa encerrada — inicie uma nova para continuar.\",\n });\n }\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind === \"group_participants\") {\n return await this.handleGroupParticipants(parsed.event);\n }\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /**\n * Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é\n * removido do grupo da sessão, registra uma mensagem de sistema no chat (visível\n * para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão\n * — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.\n * NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution\n * não reenviar.\n */\n private async handleGroupParticipants(\n change: GroupParticipantChange,\n ): Promise<{ handled: boolean }> {\n try {\n const session = await this.deps.store.getSessionByGroupJid(change.groupJid);\n if (session === null) return { handled: true }; // grupo não é desta plataforma\n\n const phones = change.participants.map((jid) => jid.split(\"@\")[0] ?? jid);\n const visitorLeft = change.participants.some((jid) => {\n const phone = jid.split(\"@\")[0] ?? \"\";\n return phone !== \"\" && phone === session.visitorPhone;\n });\n\n let body: string;\n switch (change.action) {\n case \"leave\":\n body = `Participante(s) saíram do grupo: ${phones.join(\", \")}`;\n break;\n case \"remove\":\n body = `Participante(s) removido(s) do grupo: ${phones.join(\", \")}`;\n break;\n case \"add\":\n body = `Participante(s) adicionado(s) ao grupo: ${phones.join(\", \")}`;\n break;\n case \"promote\":\n body = `Participante(s) promovido(s) a admin: ${phones.join(\", \")}`;\n break;\n case \"demote\":\n body = `Participante(s) rebaixado(s) de admin: ${phones.join(\", \")}`;\n break;\n default:\n body = `Alteração de participantes no grupo: ${phones.join(\", \")}`;\n }\n\n const now = this.clock().toISOString();\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now);\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n\n if (visitorLeft) {\n await this.closeSessionCleanup(session, \"visitor_left\");\n }\n\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] group-participants não processado:\", error);\n return { handled: true };\n }\n }\n\n /**\n * Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do\n * lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na\n * Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais\n * (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.\n */\n private async closeSessionCleanup(\n session: ChatSession,\n reason: string,\n opts: { systemMessage?: string } = {},\n ): Promise<void> {\n const now = this.clock().toISOString();\n await this.deps.store.markStatus(session.id, \"closed\", reason);\n await this.deps.store.touchSession(session.id, now);\n\n if (opts.systemMessage !== undefined) {\n const notice = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body: opts.systemMessage,\n status: \"sent\",\n });\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message: notice });\n }\n\n await this.deps.transport.publish(session.realtimeToken, { type: \"session\", status: \"closed\" });\n\n if (\n session.groupJid !== null &&\n (reason === \"visitor_left\" || reason === \"send_target_gone\")\n ) {\n try {\n await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);\n } catch (error) {\n console.error(\"[evolution-chat] falha ao sair do grupo órfão:\", error);\n }\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":["firstTarget","waMessageId","initial"]}
1
+ {"version":3,"sources":["../src/bridge/index.ts","../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/index.ts — superfície pública do subpath \"@erlancarreira/evolution-chat/bridge\".\n// Portas (contratos) + ConversationRouter (decisão de roteamento) + ChatBridge (orquestração)\n// e a Strategy de formatação. Adapters concretos são exportados pelos seus próprios subpaths\n// (./transports/supabase, ./next).\n\nexport * from \"./types\";\nexport * from \"./router\";\nexport * from \"./format\";\nexport * from \"./bridge\";\n\n// Erros de domínio compartilhados: o consumidor externo (ex.: stores do LMS) precisa\n// importar ChatError/ChatErrorCode daqui para que `instanceof ChatError` funcione —\n// cópias locais do tipo nunca casam com os erros lançados pelas rotas do SDK.\nexport * from \"../errors\";\n\n// Tipos de domínio que aparecem nas assinaturas das portas, reexportados para que o\n// consumidor do subpath ./bridge não precise importar do caminho interno src/types.\nexport type {\n ChatEvent,\n ChatMessage,\n ChatMessageDirection,\n ChatMessageStatus,\n ChatSession,\n ChatSessionStatus,\n} from \"../types\";\n","// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (modo grupo e modo direto):\n// - jid de grupo (@g.us): sessão por groupJid (comportamento original).\n// - jid pessoal (1:1, modo direto): sessão pelo telefone do visitante.\n// - isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// - sem texto → not_text\n// - caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(\n private readonly store: Pick<\n SessionStore,\n \"getSessionByGroupJid\" | \"getSessionByVisitorPhone\" | \"isEcho\"\n >,\n ) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n let session: Awaited<ReturnType<SessionStore[\"getSessionByGroupJid\"]>> = null;\n if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {\n session = await this.store.getSessionByGroupJid(msg.jid);\n } else {\n // Modo direto: o jid da conversa 1:1 é o JID pessoal do visitante (ex.: 5511...@s.whatsapp.net).\n const phone = msg.jid.split(\"@\")[0] ?? \"\";\n if (phone.length === 0) return { action: \"ignore\" };\n session = await this.store.getSessionByVisitorPhone(phone);\n }\n\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport { type EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession, GroupParticipantChange } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\n/**\n * Detecta erro da Evolution que indica que o destino sumiu: grupo apagado/saiu,\n * participante removido, número bloqueado, etc. Usado para encerrar a sessão com\n * aviso em vez de deixar o composer travado num envio que nunca chega.\n * Sinais: status 404/410 (grupo inexistente / não é participante) ou palavras-chave\n * no corpo/mensagem da resposta.\n */\nfunction isTargetGoneError(error: unknown): boolean {\n if (error === null || typeof error !== \"object\") return false;\n const status = (error as { status?: unknown }).status;\n if (status === 404 || status === 410) return true;\n const text = `${String((error as { message?: unknown }).message ?? \"\")} ${String(\n (error as { body?: unknown }).body ?? \"\",\n )}`.toLowerCase();\n return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n // Reusa a conversa ativa do mesmo visitante (um grupo/thread por cliente) em vez de\n // abrir um grupo novo a cada reabertura — evita a proliferação de grupos no WhatsApp.\n // Só reaproveita se a sessão estiver ativa; se o grupo sumiu no WhatsApp (visitante\n // saiu/apagou), encerramos a sessão antiga e deixamos o fluxo abaixo recriar um grupo\n // novo para o mesmo visitante (auto-recuperação — sem grupo zumbi preso no histórico).\n const existing = await store.getSessionByVisitorPhone(phone);\n if (existing !== null && existing.status === \"active\") {\n const firstTarget =\n existing.groupJid === null\n ? toWhatsappJid(normalizePhone(cfg.platformNumber))\n : existing.groupJid;\n try {\n const { waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(existing.code, name, message),\n );\n await store.registerSentMessageId(existing.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: existing.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n return { session: existing, messages: [initial] };\n } catch (error) {\n if (isTargetGoneError(error)) {\n // Grupo apagado/saído: encerra a sessão antiga e recria um grupo novo abaixo.\n await this.closeSessionCleanup(existing, \"send_target_gone\");\n } else {\n await store.markStatus(existing.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n }\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n const direct = cfg.createGroup === false;\n\n let groupJid: string | null = null;\n if (!direct) {\n groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n // Imagem de capa do grupo (logo do site / upload do painel). Cosmético: falha não aborta.\n const picture = (cfg.groupImage ?? \"\").trim();\n if (picture !== \"\") {\n try {\n await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);\n } catch {\n // ignora falha de imagem — o atendimento segue sem a capa.\n }\n }\n }\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n mode: direct ? \"direct\" : \"group\",\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n // Em modo direto a conversa é 1:1 com a plataforma; senão, vai para o grupo.\n let firstTarget: string;\n if (direct) {\n firstTarget = platformJid;\n } else {\n if (groupJid === null) throw new ChatError(\"Falha ao criar o grupo\", \"group_create_failed\");\n firstTarget = groupJid;\n }\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n\n // Destino: grupo (modo grupo) ou número da plataforma (modo direto 1:1, groupJid nulo).\n let target: string;\n if (session.groupJid === null) {\n target = toWhatsappJid(normalizePhone(cfg.platformNumber));\n } else {\n target = session.groupJid;\n }\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n target,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n // Se o destino sumiu (grupo saiu/apagado, número bloqueado), encerra a sessão com\n // aviso claro e sai do grupo órfão (best-effort) em vez de deixar o composer travado.\n if (isTargetGoneError(error)) {\n await this.closeSessionCleanup(session, \"send_target_gone\", {\n systemMessage:\n \"Não foi possível entregar a mensagem: o grupo não está mais disponível. Conversa encerrada — inicie uma nova para continuar.\",\n });\n }\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind === \"group_participants\") {\n return await this.handleGroupParticipants(parsed.event);\n }\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /**\n * Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é\n * removido do grupo da sessão, registra uma mensagem de sistema no chat (visível\n * para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão\n * — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.\n * NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution\n * não reenviar.\n */\n private async handleGroupParticipants(\n change: GroupParticipantChange,\n ): Promise<{ handled: boolean }> {\n try {\n const session = await this.deps.store.getSessionByGroupJid(change.groupJid);\n if (session === null) return { handled: true }; // grupo não é desta plataforma\n\n const phones = change.participants.map((jid) => jid.split(\"@\")[0] ?? jid);\n const visitorLeft = change.participants.some((jid) => {\n const phone = jid.split(\"@\")[0] ?? \"\";\n return phone !== \"\" && phone === session.visitorPhone;\n });\n\n let body: string;\n switch (change.action) {\n case \"leave\":\n body = `Participante(s) saíram do grupo: ${phones.join(\", \")}`;\n break;\n case \"remove\":\n body = `Participante(s) removido(s) do grupo: ${phones.join(\", \")}`;\n break;\n case \"add\":\n body = `Participante(s) adicionado(s) ao grupo: ${phones.join(\", \")}`;\n break;\n case \"promote\":\n body = `Participante(s) promovido(s) a admin: ${phones.join(\", \")}`;\n break;\n case \"demote\":\n body = `Participante(s) rebaixado(s) de admin: ${phones.join(\", \")}`;\n break;\n default:\n body = `Alteração de participantes no grupo: ${phones.join(\", \")}`;\n }\n\n const now = this.clock().toISOString();\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now);\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n\n if (visitorLeft) {\n await this.closeSessionCleanup(session, \"visitor_left\");\n }\n\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] group-participants não processado:\", error);\n return { handled: true };\n }\n }\n\n /**\n * Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do\n * lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na\n * Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais\n * (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.\n */\n private async closeSessionCleanup(\n session: ChatSession,\n reason: string,\n opts: { systemMessage?: string } = {},\n ): Promise<void> {\n const now = this.clock().toISOString();\n await this.deps.store.markStatus(session.id, \"closed\", reason);\n await this.deps.store.touchSession(session.id, now);\n\n if (opts.systemMessage !== undefined) {\n const notice = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body: opts.systemMessage,\n status: \"sent\",\n });\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message: notice });\n }\n\n await this.deps.transport.publish(session.realtimeToken, { type: \"session\", status: \"closed\" });\n\n if (\n session.groupJid !== null &&\n (reason === \"visitor_left\" || reason === \"send_target_gone\")\n ) {\n try {\n await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);\n } catch (error) {\n console.error(\"[evolution-chat] falha ao sair do grupo órfão:\", error);\n }\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":["firstTarget","waMessageId","initial"]}
package/bridge/index.js CHANGED
@@ -288,28 +288,31 @@ var ChatBridge = class {
288
288
  }
289
289
  }
290
290
  const existing = await store.getSessionByVisitorPhone(phone);
291
- if (existing !== null) {
291
+ if (existing !== null && existing.status === "active") {
292
292
  const firstTarget2 = existing.groupJid === null ? toWhatsappJid(normalizePhone(cfg.platformNumber)) : existing.groupJid;
293
- let waMessageId2;
294
293
  try {
295
- ({ waMessageId: waMessageId2 } = await this.deps.client.sendText(
294
+ const { waMessageId: waMessageId2 } = await this.deps.client.sendText(
296
295
  cfg.instance,
297
296
  firstTarget2,
298
297
  formatFirstMessage(existing.code, name, message)
299
- ));
298
+ );
299
+ await store.registerSentMessageId(existing.id, waMessageId2);
300
+ const initial2 = await store.appendMessage({
301
+ sessionId: existing.id,
302
+ direction: "visitor",
303
+ body: message,
304
+ waMessageId: waMessageId2,
305
+ status: "sent"
306
+ });
307
+ return { session: existing, messages: [initial2] };
300
308
  } catch (error) {
301
- await store.markStatus(existing.id, "failed");
302
- throw new ChatError("Falha ao enviar a primeira mensagem para o grupo", "send_failed", error);
309
+ if (isTargetGoneError(error)) {
310
+ await this.closeSessionCleanup(existing, "send_target_gone");
311
+ } else {
312
+ await store.markStatus(existing.id, "failed");
313
+ throw new ChatError("Falha ao enviar a primeira mensagem para o grupo", "send_failed", error);
314
+ }
303
315
  }
304
- await store.registerSentMessageId(existing.id, waMessageId2);
305
- const initial2 = await store.appendMessage({
306
- sessionId: existing.id,
307
- direction: "visitor",
308
- body: message,
309
- waMessageId: waMessageId2,
310
- status: "sent"
311
- });
312
- return { session: existing, messages: [initial2] };
313
316
  }
314
317
  const code = generateSessionCode();
315
318
  const visitorJid = toWhatsappJid(phone);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (modo grupo e modo direto):\n// - jid de grupo (@g.us): sessão por groupJid (comportamento original).\n// - jid pessoal (1:1, modo direto): sessão pelo telefone do visitante.\n// - isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// - sem texto → not_text\n// - caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(\n private readonly store: Pick<\n SessionStore,\n \"getSessionByGroupJid\" | \"getSessionByVisitorPhone\" | \"isEcho\"\n >,\n ) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n let session: Awaited<ReturnType<SessionStore[\"getSessionByGroupJid\"]>> = null;\n if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {\n session = await this.store.getSessionByGroupJid(msg.jid);\n } else {\n // Modo direto: o jid da conversa 1:1 é o JID pessoal do visitante (ex.: 5511...@s.whatsapp.net).\n const phone = msg.jid.split(\"@\")[0] ?? \"\";\n if (phone.length === 0) return { action: \"ignore\" };\n session = await this.store.getSessionByVisitorPhone(phone);\n }\n\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport { type EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession, GroupParticipantChange } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\n/**\n * Detecta erro da Evolution que indica que o destino sumiu: grupo apagado/saiu,\n * participante removido, número bloqueado, etc. Usado para encerrar a sessão com\n * aviso em vez de deixar o composer travado num envio que nunca chega.\n * Sinais: status 404/410 (grupo inexistente / não é participante) ou palavras-chave\n * no corpo/mensagem da resposta.\n */\nfunction isTargetGoneError(error: unknown): boolean {\n if (error === null || typeof error !== \"object\") return false;\n const status = (error as { status?: unknown }).status;\n if (status === 404 || status === 410) return true;\n const text = `${String((error as { message?: unknown }).message ?? \"\")} ${String(\n (error as { body?: unknown }).body ?? \"\",\n )}`.toLowerCase();\n return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n // Reusa a conversa ativa do mesmo visitante (um grupo/thread por cliente) em vez de\n // abrir um grupo novo a cada reabertura — evita a proliferação de grupos no WhatsApp.\n const existing = await store.getSessionByVisitorPhone(phone);\n if (existing !== null) {\n const firstTarget =\n existing.groupJid === null\n ? toWhatsappJid(normalizePhone(cfg.platformNumber))\n : existing.groupJid;\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(existing.code, name, message),\n ));\n } catch (error) {\n await store.markStatus(existing.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n await store.registerSentMessageId(existing.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: existing.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n return { session: existing, messages: [initial] };\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n const direct = cfg.createGroup === false;\n\n let groupJid: string | null = null;\n if (!direct) {\n groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n // Imagem de capa do grupo (logo do site / upload do painel). Cosmético: falha não aborta.\n const picture = (cfg.groupImage ?? \"\").trim();\n if (picture !== \"\") {\n try {\n await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);\n } catch {\n // ignora falha de imagem — o atendimento segue sem a capa.\n }\n }\n }\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n mode: direct ? \"direct\" : \"group\",\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n // Em modo direto a conversa é 1:1 com a plataforma; senão, vai para o grupo.\n let firstTarget: string;\n if (direct) {\n firstTarget = platformJid;\n } else {\n if (groupJid === null) throw new ChatError(\"Falha ao criar o grupo\", \"group_create_failed\");\n firstTarget = groupJid;\n }\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n\n // Destino: grupo (modo grupo) ou número da plataforma (modo direto 1:1, groupJid nulo).\n let target: string;\n if (session.groupJid === null) {\n target = toWhatsappJid(normalizePhone(cfg.platformNumber));\n } else {\n target = session.groupJid;\n }\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n target,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n // Se o destino sumiu (grupo saiu/apagado, número bloqueado), encerra a sessão com\n // aviso claro e sai do grupo órfão (best-effort) em vez de deixar o composer travado.\n if (isTargetGoneError(error)) {\n await this.closeSessionCleanup(session, \"send_target_gone\", {\n systemMessage:\n \"Não foi possível entregar a mensagem: o grupo não está mais disponível. Conversa encerrada — inicie uma nova para continuar.\",\n });\n }\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind === \"group_participants\") {\n return await this.handleGroupParticipants(parsed.event);\n }\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /**\n * Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é\n * removido do grupo da sessão, registra uma mensagem de sistema no chat (visível\n * para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão\n * — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.\n * NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution\n * não reenviar.\n */\n private async handleGroupParticipants(\n change: GroupParticipantChange,\n ): Promise<{ handled: boolean }> {\n try {\n const session = await this.deps.store.getSessionByGroupJid(change.groupJid);\n if (session === null) return { handled: true }; // grupo não é desta plataforma\n\n const phones = change.participants.map((jid) => jid.split(\"@\")[0] ?? jid);\n const visitorLeft = change.participants.some((jid) => {\n const phone = jid.split(\"@\")[0] ?? \"\";\n return phone !== \"\" && phone === session.visitorPhone;\n });\n\n let body: string;\n switch (change.action) {\n case \"leave\":\n body = `Participante(s) saíram do grupo: ${phones.join(\", \")}`;\n break;\n case \"remove\":\n body = `Participante(s) removido(s) do grupo: ${phones.join(\", \")}`;\n break;\n case \"add\":\n body = `Participante(s) adicionado(s) ao grupo: ${phones.join(\", \")}`;\n break;\n case \"promote\":\n body = `Participante(s) promovido(s) a admin: ${phones.join(\", \")}`;\n break;\n case \"demote\":\n body = `Participante(s) rebaixado(s) de admin: ${phones.join(\", \")}`;\n break;\n default:\n body = `Alteração de participantes no grupo: ${phones.join(\", \")}`;\n }\n\n const now = this.clock().toISOString();\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now);\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n\n if (visitorLeft) {\n await this.closeSessionCleanup(session, \"visitor_left\");\n }\n\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] group-participants não processado:\", error);\n return { handled: true };\n }\n }\n\n /**\n * Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do\n * lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na\n * Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais\n * (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.\n */\n private async closeSessionCleanup(\n session: ChatSession,\n reason: string,\n opts: { systemMessage?: string } = {},\n ): Promise<void> {\n const now = this.clock().toISOString();\n await this.deps.store.markStatus(session.id, \"closed\", reason);\n await this.deps.store.touchSession(session.id, now);\n\n if (opts.systemMessage !== undefined) {\n const notice = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body: opts.systemMessage,\n status: \"sent\",\n });\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message: notice });\n }\n\n await this.deps.transport.publish(session.realtimeToken, { type: \"session\", status: \"closed\" });\n\n if (\n session.groupJid !== null &&\n (reason === \"visitor_left\" || reason === \"send_target_gone\")\n ) {\n try {\n await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);\n } catch (error) {\n console.error(\"[evolution-chat] falha ao sair do grupo órfão:\", error);\n }\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":["firstTarget","waMessageId","initial"]}
1
+ {"version":3,"sources":["../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (modo grupo e modo direto):\n// - jid de grupo (@g.us): sessão por groupJid (comportamento original).\n// - jid pessoal (1:1, modo direto): sessão pelo telefone do visitante.\n// - isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// - sem texto → not_text\n// - caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(\n private readonly store: Pick<\n SessionStore,\n \"getSessionByGroupJid\" | \"getSessionByVisitorPhone\" | \"isEcho\"\n >,\n ) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n let session: Awaited<ReturnType<SessionStore[\"getSessionByGroupJid\"]>> = null;\n if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {\n session = await this.store.getSessionByGroupJid(msg.jid);\n } else {\n // Modo direto: o jid da conversa 1:1 é o JID pessoal do visitante (ex.: 5511...@s.whatsapp.net).\n const phone = msg.jid.split(\"@\")[0] ?? \"\";\n if (phone.length === 0) return { action: \"ignore\" };\n session = await this.store.getSessionByVisitorPhone(phone);\n }\n\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport { type EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession, GroupParticipantChange } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\n/**\n * Detecta erro da Evolution que indica que o destino sumiu: grupo apagado/saiu,\n * participante removido, número bloqueado, etc. Usado para encerrar a sessão com\n * aviso em vez de deixar o composer travado num envio que nunca chega.\n * Sinais: status 404/410 (grupo inexistente / não é participante) ou palavras-chave\n * no corpo/mensagem da resposta.\n */\nfunction isTargetGoneError(error: unknown): boolean {\n if (error === null || typeof error !== \"object\") return false;\n const status = (error as { status?: unknown }).status;\n if (status === 404 || status === 410) return true;\n const text = `${String((error as { message?: unknown }).message ?? \"\")} ${String(\n (error as { body?: unknown }).body ?? \"\",\n )}`.toLowerCase();\n return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n // Reusa a conversa ativa do mesmo visitante (um grupo/thread por cliente) em vez de\n // abrir um grupo novo a cada reabertura — evita a proliferação de grupos no WhatsApp.\n // Só reaproveita se a sessão estiver ativa; se o grupo sumiu no WhatsApp (visitante\n // saiu/apagou), encerramos a sessão antiga e deixamos o fluxo abaixo recriar um grupo\n // novo para o mesmo visitante (auto-recuperação — sem grupo zumbi preso no histórico).\n const existing = await store.getSessionByVisitorPhone(phone);\n if (existing !== null && existing.status === \"active\") {\n const firstTarget =\n existing.groupJid === null\n ? toWhatsappJid(normalizePhone(cfg.platformNumber))\n : existing.groupJid;\n try {\n const { waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(existing.code, name, message),\n );\n await store.registerSentMessageId(existing.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: existing.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n return { session: existing, messages: [initial] };\n } catch (error) {\n if (isTargetGoneError(error)) {\n // Grupo apagado/saído: encerra a sessão antiga e recria um grupo novo abaixo.\n await this.closeSessionCleanup(existing, \"send_target_gone\");\n } else {\n await store.markStatus(existing.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n }\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n const direct = cfg.createGroup === false;\n\n let groupJid: string | null = null;\n if (!direct) {\n groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n // Imagem de capa do grupo (logo do site / upload do painel). Cosmético: falha não aborta.\n const picture = (cfg.groupImage ?? \"\").trim();\n if (picture !== \"\") {\n try {\n await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);\n } catch {\n // ignora falha de imagem — o atendimento segue sem a capa.\n }\n }\n }\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n mode: direct ? \"direct\" : \"group\",\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n // Em modo direto a conversa é 1:1 com a plataforma; senão, vai para o grupo.\n let firstTarget: string;\n if (direct) {\n firstTarget = platformJid;\n } else {\n if (groupJid === null) throw new ChatError(\"Falha ao criar o grupo\", \"group_create_failed\");\n firstTarget = groupJid;\n }\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n\n // Destino: grupo (modo grupo) ou número da plataforma (modo direto 1:1, groupJid nulo).\n let target: string;\n if (session.groupJid === null) {\n target = toWhatsappJid(normalizePhone(cfg.platformNumber));\n } else {\n target = session.groupJid;\n }\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n target,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n // Se o destino sumiu (grupo saiu/apagado, número bloqueado), encerra a sessão com\n // aviso claro e sai do grupo órfão (best-effort) em vez de deixar o composer travado.\n if (isTargetGoneError(error)) {\n await this.closeSessionCleanup(session, \"send_target_gone\", {\n systemMessage:\n \"Não foi possível entregar a mensagem: o grupo não está mais disponível. Conversa encerrada — inicie uma nova para continuar.\",\n });\n }\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind === \"group_participants\") {\n return await this.handleGroupParticipants(parsed.event);\n }\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /**\n * Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é\n * removido do grupo da sessão, registra uma mensagem de sistema no chat (visível\n * para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão\n * — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.\n * NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution\n * não reenviar.\n */\n private async handleGroupParticipants(\n change: GroupParticipantChange,\n ): Promise<{ handled: boolean }> {\n try {\n const session = await this.deps.store.getSessionByGroupJid(change.groupJid);\n if (session === null) return { handled: true }; // grupo não é desta plataforma\n\n const phones = change.participants.map((jid) => jid.split(\"@\")[0] ?? jid);\n const visitorLeft = change.participants.some((jid) => {\n const phone = jid.split(\"@\")[0] ?? \"\";\n return phone !== \"\" && phone === session.visitorPhone;\n });\n\n let body: string;\n switch (change.action) {\n case \"leave\":\n body = `Participante(s) saíram do grupo: ${phones.join(\", \")}`;\n break;\n case \"remove\":\n body = `Participante(s) removido(s) do grupo: ${phones.join(\", \")}`;\n break;\n case \"add\":\n body = `Participante(s) adicionado(s) ao grupo: ${phones.join(\", \")}`;\n break;\n case \"promote\":\n body = `Participante(s) promovido(s) a admin: ${phones.join(\", \")}`;\n break;\n case \"demote\":\n body = `Participante(s) rebaixado(s) de admin: ${phones.join(\", \")}`;\n break;\n default:\n body = `Alteração de participantes no grupo: ${phones.join(\", \")}`;\n }\n\n const now = this.clock().toISOString();\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now);\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n\n if (visitorLeft) {\n await this.closeSessionCleanup(session, \"visitor_left\");\n }\n\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] group-participants não processado:\", error);\n return { handled: true };\n }\n }\n\n /**\n * Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do\n * lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na\n * Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais\n * (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.\n */\n private async closeSessionCleanup(\n session: ChatSession,\n reason: string,\n opts: { systemMessage?: string } = {},\n ): Promise<void> {\n const now = this.clock().toISOString();\n await this.deps.store.markStatus(session.id, \"closed\", reason);\n await this.deps.store.touchSession(session.id, now);\n\n if (opts.systemMessage !== undefined) {\n const notice = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body: opts.systemMessage,\n status: \"sent\",\n });\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message: notice });\n }\n\n await this.deps.transport.publish(session.realtimeToken, { type: \"session\", status: \"closed\" });\n\n if (\n session.groupJid !== null &&\n (reason === \"visitor_left\" || reason === \"send_target_gone\")\n ) {\n try {\n await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);\n } catch (error) {\n console.error(\"[evolution-chat] falha ao sair do grupo órfão:\", error);\n }\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":["firstTarget","waMessageId","initial"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erlancarreira/evolution-chat",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Chat widget + Evolution API bridge (WhatsApp) — reutilizável, hexagonal, SOLID",
5
5
  "license": "MIT",
6
6
  "type": "module",