@erlancarreira/evolution-chat 0.1.0 → 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/api/index.cjs +53 -4
- package/api/index.cjs.map +1 -1
- package/api/index.d.cts +31 -0
- package/api/index.d.ts +31 -0
- package/api/index.js +53 -4
- package/api/index.js.map +1 -1
- package/bridge/index.cjs +192 -6
- package/bridge/index.cjs.map +1 -1
- package/bridge/index.d.cts +40 -2
- package/bridge/index.d.ts +40 -2
- package/bridge/index.js +192 -6
- package/bridge/index.js.map +1 -1
- package/next/index.d.cts +39 -1
- package/next/index.d.ts +39 -1
- package/package.json +1 -1
- package/transports/supabase/index.cjs +24 -1
- package/transports/supabase/index.cjs.map +1 -1
- package/transports/supabase/index.d.cts +4 -2
- package/transports/supabase/index.d.ts +4 -2
- package/transports/supabase/index.js +23 -1
- package/transports/supabase/index.js.map +1 -1
- package/widget/index.cjs +39 -11
- package/widget/index.cjs.map +1 -1
- package/widget/index.d.cts +2 -2
- package/widget/index.d.ts +2 -2
- package/widget/index.js +39 -11
- package/widget/index.js.map +1 -1
- package/widget/styles.css +23 -0
- package/widget/styles.css.map +1 -1
- package/widget-embed/evolution-chat.iife.js +58 -12
package/bridge/index.cjs
CHANGED
|
@@ -42,8 +42,14 @@ var ConversationRouter = class {
|
|
|
42
42
|
*/
|
|
43
43
|
async decide(msg, now) {
|
|
44
44
|
void now;
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
let session = null;
|
|
46
|
+
if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {
|
|
47
|
+
session = await this.store.getSessionByGroupJid(msg.jid);
|
|
48
|
+
} else {
|
|
49
|
+
const phone = msg.jid.split("@")[0] ?? "";
|
|
50
|
+
if (phone.length === 0) return { action: "ignore" };
|
|
51
|
+
session = await this.store.getSessionByVisitorPhone(phone);
|
|
52
|
+
}
|
|
47
53
|
if (session === null) return { action: "unknown_session" };
|
|
48
54
|
if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: "echo" };
|
|
49
55
|
if (typeof msg.text !== "string" || msg.text.length === 0) return { action: "not_text" };
|
|
@@ -189,6 +195,9 @@ function parseWebhookEvent(payload) {
|
|
|
189
195
|
if (typeof state === "string" && state.length > 0) return { kind: "connection", state };
|
|
190
196
|
return { kind: "ignored", reason: "connection.update sem state" };
|
|
191
197
|
}
|
|
198
|
+
if (event === "group-participants.update") {
|
|
199
|
+
return parseGroupParticipants(data);
|
|
200
|
+
}
|
|
192
201
|
if (event !== "messages.upsert") {
|
|
193
202
|
return { kind: "ignored", reason: `evento n\xE3o tratado: ${event}` };
|
|
194
203
|
}
|
|
@@ -201,6 +210,32 @@ function parseWebhookEvent(payload) {
|
|
|
201
210
|
}
|
|
202
211
|
return lastIgnored;
|
|
203
212
|
}
|
|
213
|
+
function parseGroupParticipants(data) {
|
|
214
|
+
if (!isRecord(data)) return { kind: "ignored", reason: "group-participants sem data" };
|
|
215
|
+
const groupJid = data["id"];
|
|
216
|
+
if (typeof groupJid !== "string" || groupJid.length === 0) {
|
|
217
|
+
return { kind: "ignored", reason: "group-participants sem id (groupJid)" };
|
|
218
|
+
}
|
|
219
|
+
const rawParticipants = data["participants"];
|
|
220
|
+
if (!Array.isArray(rawParticipants)) {
|
|
221
|
+
return { kind: "ignored", reason: "group-participants sem participants[]" };
|
|
222
|
+
}
|
|
223
|
+
const participants = [];
|
|
224
|
+
for (const p of rawParticipants) {
|
|
225
|
+
if (typeof p === "string" && p.length > 0) participants.push(p);
|
|
226
|
+
}
|
|
227
|
+
if (participants.length === 0) {
|
|
228
|
+
return { kind: "ignored", reason: "group-participants sem participantes v\xE1lidos" };
|
|
229
|
+
}
|
|
230
|
+
const actionRaw = data["action"];
|
|
231
|
+
const action = typeof actionRaw === "string" ? actionRaw : "unknown";
|
|
232
|
+
const authorRaw = data["author"];
|
|
233
|
+
const author = typeof authorRaw === "string" ? authorRaw : null;
|
|
234
|
+
return {
|
|
235
|
+
kind: "group_participants",
|
|
236
|
+
event: { groupJid, participants, action, author, raw: data }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
204
239
|
|
|
205
240
|
// src/bridge/bridge.ts
|
|
206
241
|
var NAME_MIN = 2;
|
|
@@ -230,6 +265,15 @@ function requireText(value, min, max, field) {
|
|
|
230
265
|
}
|
|
231
266
|
return trimmed;
|
|
232
267
|
}
|
|
268
|
+
function isTargetGoneError(error) {
|
|
269
|
+
if (error === null || typeof error !== "object") return false;
|
|
270
|
+
const status = error.status;
|
|
271
|
+
if (status === 404 || status === 410) return true;
|
|
272
|
+
const text = `${String(error.message ?? "")} ${String(
|
|
273
|
+
error.body ?? ""
|
|
274
|
+
)}`.toLowerCase();
|
|
275
|
+
return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);
|
|
276
|
+
}
|
|
233
277
|
var ChatBridge = class {
|
|
234
278
|
constructor(deps) {
|
|
235
279
|
this.deps = deps;
|
|
@@ -273,12 +317,50 @@ var ChatBridge = class {
|
|
|
273
317
|
);
|
|
274
318
|
}
|
|
275
319
|
}
|
|
320
|
+
const existing = await store.getSessionByVisitorPhone(phone);
|
|
321
|
+
if (existing !== null && existing.status === "active") {
|
|
322
|
+
const firstTarget2 = existing.groupJid === null ? toWhatsappJid(normalizePhone(cfg.platformNumber)) : existing.groupJid;
|
|
323
|
+
try {
|
|
324
|
+
const { waMessageId: waMessageId2 } = await this.deps.client.sendText(
|
|
325
|
+
cfg.instance,
|
|
326
|
+
firstTarget2,
|
|
327
|
+
formatFirstMessage(existing.code, name, message)
|
|
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] };
|
|
338
|
+
} catch (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
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
276
347
|
const code = generateSessionCode();
|
|
277
348
|
const visitorJid = toWhatsappJid(phone);
|
|
278
349
|
const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));
|
|
279
350
|
const participants = [.../* @__PURE__ */ new Set([visitorJid, platformJid])];
|
|
280
351
|
const subject = `${cfg.projectName} \u2014 ${name} (#${code})`;
|
|
281
|
-
const
|
|
352
|
+
const direct = cfg.createGroup === false;
|
|
353
|
+
let groupJid = null;
|
|
354
|
+
if (!direct) {
|
|
355
|
+
groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);
|
|
356
|
+
const picture = (cfg.groupImage ?? "").trim();
|
|
357
|
+
if (picture !== "") {
|
|
358
|
+
try {
|
|
359
|
+
await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
282
364
|
const session = await store.createSession({
|
|
283
365
|
code,
|
|
284
366
|
realtimeToken: generateRealtimeToken(),
|
|
@@ -286,14 +368,22 @@ var ChatBridge = class {
|
|
|
286
368
|
visitorPhone: phone,
|
|
287
369
|
visitorContact: input.contact ?? null,
|
|
288
370
|
groupJid,
|
|
371
|
+
mode: direct ? "direct" : "group",
|
|
289
372
|
ipHash: input.ipHash ?? null,
|
|
290
373
|
userAgent: input.userAgent ?? null
|
|
291
374
|
});
|
|
375
|
+
let firstTarget;
|
|
376
|
+
if (direct) {
|
|
377
|
+
firstTarget = platformJid;
|
|
378
|
+
} else {
|
|
379
|
+
if (groupJid === null) throw new ChatError("Falha ao criar o grupo", "group_create_failed");
|
|
380
|
+
firstTarget = groupJid;
|
|
381
|
+
}
|
|
292
382
|
let waMessageId;
|
|
293
383
|
try {
|
|
294
384
|
({ waMessageId } = await this.deps.client.sendText(
|
|
295
385
|
cfg.instance,
|
|
296
|
-
|
|
386
|
+
firstTarget,
|
|
297
387
|
formatFirstMessage(code, name, message)
|
|
298
388
|
));
|
|
299
389
|
} catch (error) {
|
|
@@ -318,7 +408,12 @@ var ChatBridge = class {
|
|
|
318
408
|
if (session === null) throw new ChatError("Sess\xE3o n\xE3o encontrada", "session_not_found");
|
|
319
409
|
if (session.status !== "active") throw new ChatError("Sess\xE3o encerrada", "session_closed");
|
|
320
410
|
const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, "Mensagem");
|
|
321
|
-
|
|
411
|
+
let target;
|
|
412
|
+
if (session.groupJid === null) {
|
|
413
|
+
target = toWhatsappJid(normalizePhone(cfg.platformNumber));
|
|
414
|
+
} else {
|
|
415
|
+
target = session.groupJid;
|
|
416
|
+
}
|
|
322
417
|
const pending = await store.appendMessage({
|
|
323
418
|
sessionId: session.id,
|
|
324
419
|
direction: "visitor",
|
|
@@ -329,11 +424,16 @@ var ChatBridge = class {
|
|
|
329
424
|
try {
|
|
330
425
|
({ waMessageId } = await this.deps.client.sendText(
|
|
331
426
|
cfg.instance,
|
|
332
|
-
|
|
427
|
+
target,
|
|
333
428
|
formatFollowup(session.visitorName, body)
|
|
334
429
|
));
|
|
335
430
|
} catch (error) {
|
|
336
431
|
await store.updateMessageStatus(pending.id, "failed");
|
|
432
|
+
if (isTargetGoneError(error)) {
|
|
433
|
+
await this.closeSessionCleanup(session, "send_target_gone", {
|
|
434
|
+
systemMessage: "N\xE3o foi poss\xEDvel entregar a mensagem: o grupo n\xE3o est\xE1 mais dispon\xEDvel. Conversa encerrada \u2014 inicie uma nova para continuar."
|
|
435
|
+
});
|
|
436
|
+
}
|
|
337
437
|
throw new ChatError("Falha ao enviar mensagem para o grupo", "send_failed", error);
|
|
338
438
|
}
|
|
339
439
|
await store.registerSentMessageId(session.id, waMessageId);
|
|
@@ -348,6 +448,9 @@ var ChatBridge = class {
|
|
|
348
448
|
async handleWebhook(payload) {
|
|
349
449
|
try {
|
|
350
450
|
const parsed = parseWebhookEvent(payload);
|
|
451
|
+
if (parsed.kind === "group_participants") {
|
|
452
|
+
return await this.handleGroupParticipants(parsed.event);
|
|
453
|
+
}
|
|
351
454
|
if (parsed.kind !== "message") return { handled: false };
|
|
352
455
|
const now = this.clock();
|
|
353
456
|
const decision = await this.router.decide(parsed.event, now);
|
|
@@ -371,6 +474,89 @@ var ChatBridge = class {
|
|
|
371
474
|
return { handled: false };
|
|
372
475
|
}
|
|
373
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é
|
|
479
|
+
* removido do grupo da sessão, registra uma mensagem de sistema no chat (visível
|
|
480
|
+
* para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão
|
|
481
|
+
* — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.
|
|
482
|
+
* NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution
|
|
483
|
+
* não reenviar.
|
|
484
|
+
*/
|
|
485
|
+
async handleGroupParticipants(change) {
|
|
486
|
+
try {
|
|
487
|
+
const session = await this.deps.store.getSessionByGroupJid(change.groupJid);
|
|
488
|
+
if (session === null) return { handled: true };
|
|
489
|
+
const phones = change.participants.map((jid) => jid.split("@")[0] ?? jid);
|
|
490
|
+
const visitorLeft = change.participants.some((jid) => {
|
|
491
|
+
const phone = jid.split("@")[0] ?? "";
|
|
492
|
+
return phone !== "" && phone === session.visitorPhone;
|
|
493
|
+
});
|
|
494
|
+
let body;
|
|
495
|
+
switch (change.action) {
|
|
496
|
+
case "leave":
|
|
497
|
+
body = `Participante(s) sa\xEDram do grupo: ${phones.join(", ")}`;
|
|
498
|
+
break;
|
|
499
|
+
case "remove":
|
|
500
|
+
body = `Participante(s) removido(s) do grupo: ${phones.join(", ")}`;
|
|
501
|
+
break;
|
|
502
|
+
case "add":
|
|
503
|
+
body = `Participante(s) adicionado(s) ao grupo: ${phones.join(", ")}`;
|
|
504
|
+
break;
|
|
505
|
+
case "promote":
|
|
506
|
+
body = `Participante(s) promovido(s) a admin: ${phones.join(", ")}`;
|
|
507
|
+
break;
|
|
508
|
+
case "demote":
|
|
509
|
+
body = `Participante(s) rebaixado(s) de admin: ${phones.join(", ")}`;
|
|
510
|
+
break;
|
|
511
|
+
default:
|
|
512
|
+
body = `Altera\xE7\xE3o de participantes no grupo: ${phones.join(", ")}`;
|
|
513
|
+
}
|
|
514
|
+
const now = this.clock().toISOString();
|
|
515
|
+
const message = await this.deps.store.appendMessage({
|
|
516
|
+
sessionId: session.id,
|
|
517
|
+
direction: "system",
|
|
518
|
+
body,
|
|
519
|
+
status: "sent"
|
|
520
|
+
});
|
|
521
|
+
await this.deps.store.touchSession(session.id, now);
|
|
522
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "message", message });
|
|
523
|
+
if (visitorLeft) {
|
|
524
|
+
await this.closeSessionCleanup(session, "visitor_left");
|
|
525
|
+
}
|
|
526
|
+
return { handled: true };
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.error("[evolution-chat] group-participants n\xE3o processado:", error);
|
|
529
|
+
return { handled: true };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do
|
|
534
|
+
* lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na
|
|
535
|
+
* Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais
|
|
536
|
+
* (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.
|
|
537
|
+
*/
|
|
538
|
+
async closeSessionCleanup(session, reason, opts = {}) {
|
|
539
|
+
const now = this.clock().toISOString();
|
|
540
|
+
await this.deps.store.markStatus(session.id, "closed", reason);
|
|
541
|
+
await this.deps.store.touchSession(session.id, now);
|
|
542
|
+
if (opts.systemMessage !== void 0) {
|
|
543
|
+
const notice = await this.deps.store.appendMessage({
|
|
544
|
+
sessionId: session.id,
|
|
545
|
+
direction: "system",
|
|
546
|
+
body: opts.systemMessage,
|
|
547
|
+
status: "sent"
|
|
548
|
+
});
|
|
549
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "message", message: notice });
|
|
550
|
+
}
|
|
551
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "session", status: "closed" });
|
|
552
|
+
if (session.groupJid !== null && (reason === "visitor_left" || reason === "send_target_gone")) {
|
|
553
|
+
try {
|
|
554
|
+
await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error("[evolution-chat] falha ao sair do grupo \xF3rf\xE3o:", error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
374
560
|
/** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */
|
|
375
561
|
async history(token, afterIso) {
|
|
376
562
|
const session = await this.deps.store.getSessionByToken(token);
|
package/bridge/index.cjs.map
CHANGED
|
@@ -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 (ordem fixada pelo plano):\n// 1. jid não termina em \"@g.us\" → ignore (sem tocar no store)\n// 2. sem sessão para o groupJid → unknown_session\n// 3. isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// 4. sem texto → not_text\n// 5. 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(private readonly store: Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">) {}\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 if (!msg.jid.endsWith(GROUP_JID_SUFFIX)) return { action: \"ignore\" };\n\n const session = await this.store.getSessionByGroupJid(msg.jid);\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 { InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\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 !== \"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","// 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 } 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\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 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\n const groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\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 ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n groupJid,\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 if (session.groupJid === null) throw new ChatError(\"Sessão sem grupo associado\", \"send_failed\");\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 session.groupJid,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\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 !== \"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 /** 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":[]}
|
|
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.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ChatMessageDirection = "visitor" | "owner";
|
|
1
|
+
type ChatMessageDirection = "visitor" | "owner" | "system";
|
|
2
2
|
type ChatMessageStatus = "pending" | "sent" | "failed";
|
|
3
3
|
type ChatSessionStatus = "active" | "closed" | "failed";
|
|
4
4
|
/** Mensagem persistida (domínio). */
|
|
@@ -20,6 +20,8 @@ interface ChatSession {
|
|
|
20
20
|
visitorPhone: string;
|
|
21
21
|
visitorContact: string | null;
|
|
22
22
|
groupJid: string | null;
|
|
23
|
+
/** Modo da conversa: "group" (grupo por visitante, padrão) ou "direct" (1:1 com a plataforma). */
|
|
24
|
+
mode?: "group" | "direct";
|
|
23
25
|
status: ChatSessionStatus;
|
|
24
26
|
createdAt: string;
|
|
25
27
|
lastMessageAt: string | null;
|
|
@@ -39,6 +41,10 @@ interface ChatConfig {
|
|
|
39
41
|
closeHours: number;
|
|
40
42
|
leaveOnClose: boolean;
|
|
41
43
|
webhookToken: string;
|
|
44
|
+
/** true (padrão) = cria um grupo por visitante; false = conversa 1:1 direta com a plataforma. */
|
|
45
|
+
createGroup?: boolean;
|
|
46
|
+
/** URL pública da imagem de capa do grupo (opcional; ignora se vazia). */
|
|
47
|
+
groupImage?: string;
|
|
42
48
|
}
|
|
43
49
|
/** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
|
|
44
50
|
type ChatEvent = {
|
|
@@ -71,6 +77,8 @@ interface SessionStore {
|
|
|
71
77
|
visitorPhone: string;
|
|
72
78
|
visitorContact?: string | null;
|
|
73
79
|
groupJid: string | null;
|
|
80
|
+
/** Modo da conversa: "group" (padrão) ou "direct" (1:1 com a plataforma). Opcional. */
|
|
81
|
+
mode?: "group" | "direct";
|
|
74
82
|
/** Hash não-reversível do IP (LGPD): usado por countRecentSessionsByIpHash. */
|
|
75
83
|
ipHash?: string | null;
|
|
76
84
|
/** User-Agent bruto da requisição (auditoria/forense; nunca exibido). */
|
|
@@ -78,6 +86,8 @@ interface SessionStore {
|
|
|
78
86
|
}): Promise<ChatSession>;
|
|
79
87
|
getSessionByToken(token: string): Promise<ChatSession | null>;
|
|
80
88
|
getSessionByGroupJid(jid: string): Promise<ChatSession | null>;
|
|
89
|
+
/** Lookup da sessão ativa de um visitante pelo telefone (modo direto 1:1). */
|
|
90
|
+
getSessionByVisitorPhone(phone: string): Promise<ChatSession | null>;
|
|
81
91
|
appendMessage(input: {
|
|
82
92
|
sessionId: string;
|
|
83
93
|
direction: ChatMessageDirection;
|
|
@@ -144,7 +154,7 @@ type RouterDecision = {
|
|
|
144
154
|
|
|
145
155
|
declare class ConversationRouter {
|
|
146
156
|
private readonly store;
|
|
147
|
-
constructor(store: Pick<SessionStore, "getSessionByGroupJid" | "isEcho">);
|
|
157
|
+
constructor(store: Pick<SessionStore, "getSessionByGroupJid" | "getSessionByVisitorPhone" | "isEcho">);
|
|
148
158
|
/**
|
|
149
159
|
* `now` faz parte do contrato para que a bridge propague uma única leitura de relógio
|
|
150
160
|
* por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda
|
|
@@ -167,13 +177,25 @@ interface CreateGroupResult {
|
|
|
167
177
|
interface EvolutionClient {
|
|
168
178
|
sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
|
|
169
179
|
createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
|
|
180
|
+
setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;
|
|
170
181
|
leaveGroup(instance: string, groupJid: string): Promise<void>;
|
|
182
|
+
/** Encerra a sessão WhatsApp da instância (desvincula o dispositivo). */
|
|
183
|
+
logout(instance: string): Promise<void>;
|
|
171
184
|
getConnectionState(instance: string): Promise<"open" | "connecting" | "close">;
|
|
172
185
|
connectQR(instance: string): Promise<{
|
|
173
186
|
qrBase64: string | null;
|
|
174
187
|
pairingCode: string | null;
|
|
175
188
|
}>;
|
|
176
189
|
setWebhook(instance: string, url: string, events: string[]): Promise<void>;
|
|
190
|
+
/** Cria a instância na Evolution (one-time). Exige a apiKey com permissão de criação. */
|
|
191
|
+
createInstance(instance: string, integration?: "WHATSAPP-BAILEYS" | "WHATSAPP-BUSINESS"): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Garante que a instância existe (idempotente): se `getConnectionState` responde
|
|
194
|
+
* (qualquer estado), não faz nada; só chama `createInstance` quando a leitura falha
|
|
195
|
+
* (instância inexistente). Assim o LMS gerencia o ciclo de vida sem criar manualmente
|
|
196
|
+
* na Evolution — o único passo humano é escanear o QR de pareamento.
|
|
197
|
+
*/
|
|
198
|
+
ensureInstance(instance: string): Promise<void>;
|
|
177
199
|
}
|
|
178
200
|
|
|
179
201
|
interface StartChatInput {
|
|
@@ -218,6 +240,22 @@ declare class ChatBridge {
|
|
|
218
240
|
handleWebhook(payload: unknown): Promise<{
|
|
219
241
|
handled: boolean;
|
|
220
242
|
}>;
|
|
243
|
+
/**
|
|
244
|
+
* Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é
|
|
245
|
+
* removido do grupo da sessão, registra uma mensagem de sistema no chat (visível
|
|
246
|
+
* para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão
|
|
247
|
+
* — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.
|
|
248
|
+
* NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution
|
|
249
|
+
* não reenviar.
|
|
250
|
+
*/
|
|
251
|
+
private handleGroupParticipants;
|
|
252
|
+
/**
|
|
253
|
+
* Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do
|
|
254
|
+
* lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na
|
|
255
|
+
* Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais
|
|
256
|
+
* (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.
|
|
257
|
+
*/
|
|
258
|
+
private closeSessionCleanup;
|
|
221
259
|
/** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */
|
|
222
260
|
history(token: string, afterIso?: string | null): Promise<{
|
|
223
261
|
session: ChatSession | null;
|