@erlancarreira/evolution-chat 0.1.0

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/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @erlancarreira/evolution-chat
2
+
3
+ Widget de chat (React) + bridge para a **Evolution API v2** (WhatsApp): o visitante abre uma
4
+ conversa no site, o SDK cria um grupo no WhatsApp com o número da plataforma, relaya as
5
+ mensagens nos dois sentidos e entrega tudo em tempo real no painel — com polling de 5 s como
6
+ fallback quando o canal cai. Arquitetura hexagonal: o núcleo (`bridge`) só conhece **portas**
7
+ (`SessionStore`, `RealtimeTransport`, `RealtimeHandle`, `ChatLimiter`) — persistência e
8
+ transporte são injetados; o adapter oficial usa Supabase, mas qualquer banco serve.
9
+
10
+ | Subpath | O que entrega |
11
+ | --- | --- |
12
+ | `./api` | `createEvolutionClient`, parser de webhook, ids, telefone |
13
+ | `./bridge` | `ChatBridge`, portas (`SessionStore`, `RealtimeTransport`, …), router |
14
+ | `./next` | `createChatRoutes`, `createWebhookRoute` (App Router, sem importar `next`) |
15
+ | `./transports/supabase` | `createSupabaseTransport` (server) · `createSupabaseRealtimeHandle` (widget) |
16
+ | `./widget` | `<ChatWidget>` React + `injectWidgetStyles` + i18n (pt/en/es) |
17
+ | `widget-embed/evolution-chat.iife.js` | `<evolution-chat>` standalone (`<script>`, React embutido) |
18
+
19
+ ## Instalação
20
+
21
+ ```bash
22
+ npm install @erlancarreira/evolution-chat # npm (react + react-dom são peers)
23
+ npm install github:erlancarreira/evolution-chat # do repositório — rode `npm run build` (a saída não é versionada)
24
+ npm install file:../evolution-chat # cópia local em desenvolvimento (idem)
25
+ ```
26
+
27
+ `@supabase/supabase-js` é peer **opcional** (só para os adapters de realtime). No `<script>`
28
+ nada disso se aplica: React, react-dom e supabase-js vão embutidos.
29
+
30
+ ## Quickstart — Next.js (App Router)
31
+
32
+ **1. Monte o bridge uma vez (server-only).** `getConfig` é resolvido a cada request, então a
33
+ config pode viver numa tabela de settings do tenant.
34
+
35
+ ```ts
36
+ // lib/chat/server.ts
37
+ import { createClient } from "@supabase/supabase-js";
38
+ import { createEvolutionClient } from "@erlancarreira/evolution-chat/api";
39
+ import { ChatBridge } from "@erlancarreira/evolution-chat/bridge";
40
+ import { createSupabaseTransport } from "@erlancarreira/evolution-chat/transports/supabase";
41
+ import { createChatRoutes, createWebhookRoute } from "@erlancarreira/evolution-chat/next";
42
+ import { createSessionStore } from "./session-store"; // seu adapter (ver seção)
43
+ import { getChatConfig } from "./settings"; // () => Promise<ChatConfig>
44
+
45
+ const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
46
+ const bridge = new ChatBridge({
47
+ client: createEvolutionClient({ baseUrl: process.env.EVOLUTION_URL!, apiKey: process.env.EVOLUTION_API_KEY! }),
48
+ store: createSessionStore(admin),
49
+ transport: createSupabaseTransport(admin),
50
+ });
51
+
52
+ export const chat = createChatRoutes({ bridge, getConfig: getChatConfig, getIpHash: hashIp });
53
+ // hashIp: (req) => sha256(ip + salt) — sem isto o limite de sessões por IP não roda
54
+ export const webhook = createWebhookRoute({ bridge, getConfig: getChatConfig });
55
+ ```
56
+
57
+ **2. Encaminhe as rotas.**
58
+
59
+ ```ts
60
+ // app/api/chat/route.ts
61
+ export const GET = (req: Request) => chat.GET(req);
62
+ export const POST = (req: Request) => chat.POST(req);
63
+
64
+ // app/api/chat/webhook/route.ts → Evolution aponta para /api/chat/webhook?token=<webhookToken>
65
+ export const POST = (req: Request) => webhook.POST(req);
66
+ ```
67
+
68
+ **3. Renderize o widget** (client component). O CSS é auto-injetado no mount (prefere `<link>`?
69
+ importe `@erlancarreira/evolution-chat/widget/styles.css`); `realtime` é a porta que assina o canal da sessão.
70
+
71
+ ```tsx
72
+ "use client";
73
+ import { ChatWidget } from "@erlancarreira/evolution-chat/widget";
74
+ import { createSupabaseRealtimeHandle } from "@erlancarreira/evolution-chat/transports/supabase";
75
+
76
+ const realtime = createSupabaseRealtimeHandle(process.env.NEXT_PUBLIC_SUPABASE_URL!,
77
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);
78
+
79
+ export function SupportChat() {
80
+ return <ChatWidget endpoint="/api/chat" locale="pt" projectName="Aulivra"
81
+ welcome="Fale com a gente no WhatsApp" accentColor="#25D366" realtime={realtime} />;
82
+ }
83
+ ```
84
+
85
+ ## Quickstart — `<script>` (sem bundler)
86
+
87
+ Sirva `widget-embed/evolution-chat.iife.js` (copie para `public/` ou aponte para o CDN;
88
+ ≈1 MB não minificado, ~207 kB gzip) e use a tag: atributos viram props, e
89
+ `data-supabase-url`/`data-supabase-key` ligam o realtime — sem eles o widget cai no polling de
90
+ 5 s em vez de quebrar.
91
+
92
+ ```html
93
+ <script src="/evolution-chat.iife.js"></script>
94
+ <evolution-chat
95
+ endpoint="/api/chat"
96
+ locale="pt"
97
+ welcome="Precisa de ajuda?"
98
+ project="Aulivra"
99
+ accent="#25D366"
100
+ data-supabase-url="https://xyz.supabase.co"
101
+ data-supabase-key="chave-anon-publica"
102
+ ></evolution-chat>
103
+ ```
104
+
105
+ O widget monta dentro de um **shadow root** (o CSS `.ecw-*` é injetado lá dentro, isolando os dois
106
+ lados). A global do bundle é `EvolutionChatEmbed`; incluir o script duas vezes não lança.
107
+
108
+ ## `ChatConfig`
109
+
110
+ É o objeto que `getConfig` devolve, a cada request. O padrão é ler de settings do tenant, com os segredos vindos de env:
111
+
112
+ ```ts
113
+ import type { ChatConfig } from "@erlancarreira/evolution-chat/next";
114
+
115
+ export async function getChatConfig(): Promise<ChatConfig> {
116
+ const s = await db.settings.findFirst();
117
+ return {
118
+ enabled: s?.chatEnabled ?? false, // false → POST /api/chat devolve 404
119
+ projectName: s?.chatProjectName ?? "Aulivra", // assunto do grupo: "<projectName> — <nome> (#CODE)"
120
+ platformNumber: s?.chatPlatformNumber ?? "5511999998888", // número da plataforma (só dígitos, com DDI)
121
+ evolutionUrl: process.env.EVOLUTION_URL!, // ex.: https://evolution.exemplo.com.br
122
+ instance: process.env.EVOLUTION_INSTANCE!, // instância conectada a esse número
123
+ apiKey: process.env.EVOLUTION_API_KEY!, // apikey da instância (NUNCA vai ao cliente)
124
+ welcome: s?.chatWelcome ?? "Como podemos ajudar?",
125
+ closeHours: s?.chatCloseHours ?? 24, // inatividade p/ fechar (0 = nunca)
126
+ leaveOnClose: s?.chatLeaveOnClose ?? true, // sai do grupo ao encerrar
127
+ webhookToken: process.env.CHAT_WEBHOOK_TOKEN!, // segredo do webhook
128
+ };
129
+ }
130
+ ```
131
+
132
+ `closeHours`/`leaveOnClose` são consumidos pelo **hospedeiro**: quem fecha (cron, painel do
133
+ atendente) chama `store.markStatus(id, "closed")` e, se `leaveOnClose`,
134
+ `client.leaveGroup(instance, groupJid)`. Para avisar o visitante na hora, publique
135
+ `{ type: "session", status: "closed" }` via `transport.publish` — hoje o SDK publica só
136
+ `{ type: "message" }`; sem esse publish o status chega ao widget apenas no GET (reabrir o
137
+ painel, ou o polling quando o canal cai).
138
+
139
+ ## Implementando um `SessionStore` próprio
140
+
141
+ `import type { SessionStore, ChatSession, ChatMessage } from "@erlancarreira/evolution-chat/bridge"`
142
+ — é a única superfície de persistência que o bridge usa. Não há cache nem estado em memória.
143
+
144
+ ```ts
145
+ export interface SessionStore {
146
+ createSession(input: { code: string; realtimeToken: string; visitorName: string;
147
+ visitorPhone: string; visitorContact?: string | null; groupJid: string | null;
148
+ ipHash?: string | null; userAgent?: string | null }): Promise<ChatSession>;
149
+ getSessionByToken(token: string): Promise<ChatSession | null>; // token = realtimeToken
150
+ getSessionByGroupJid(jid: string): Promise<ChatSession | null>; // roteia o webhook pelo grupo
151
+ appendMessage(input: { sessionId: string; direction: ChatMessageDirection; body: string;
152
+ waMessageId?: string | null; status?: ChatMessageStatus }): Promise<ChatMessage>;
153
+ updateMessageStatus(id: string, status: ChatMessageStatus): Promise<void>;
154
+ listMessages(sessionId: string, afterIso?: string | null): Promise<ChatMessage[]>;
155
+ findMessageByWaId(sessionId: string, waMessageId: string): Promise<ChatMessage | null>;
156
+ registerSentMessageId(sessionId: string, waMessageId: string): Promise<void>;
157
+ isEcho(sessionId: string, waMessageId: string): Promise<boolean>;
158
+ touchSession(sessionId: string, atIso: string): Promise<void>;
159
+ setGroupJid(sessionId: string, groupJid: string): Promise<void>;
160
+ markStatus(sessionId: string, status: ChatSessionStatus, reason?: string): Promise<void>;
161
+ countRecentSessionsByIpHash(ipHash: string, windowMs: number): Promise<number>;
162
+ }
163
+ ```
164
+
165
+ O que o bridge **espera** do seu adapter:
166
+
167
+ - `appendMessage` devolve a mensagem com `id` e `createdAt` (ISO) preenchidos — `createdAt` é o
168
+ cursor do polling/replay.
169
+ - `findMessageByWaId` + `registerSentMessageId`/`isEcho` sustentam a idempotência: a Evolution
170
+ reenvia enquanto não devolvemos `handled: true`, e o eco das mensagens que **nós** enviamos
171
+ precisa ser reconhecido (tabela própria de ids, não uma coluna em `messages`).
172
+ - `getSessionByGroupJid` é 1 grupo ↔ 1 sessão; o router **não** filtra por status, então
173
+ devolva `null` em `closed`/`failed` para não anexar mensagens a conversa encerrada.
174
+ `touchSession` atualiza `lastMessageAt` (o que alimenta um job de `closeHours`).
175
+ - `countRecentSessionsByIpHash` é a guarda de 5 sessões/IP/10 min. O `ipHash` e o `userAgent`
176
+ da requisição chegam ao adapter via `createSession({ ..., ipHash, userAgent })`, então basta
177
+ persistir as colunas `ip_hash`/`user_agent` da migration para a contagem funcionar contra a
178
+ mesma tabela `chat_sessions` (o adapter Supabase do LMS faz isso). O `limiter` de
179
+ `createChatRoutes` (30 POSTs/min/IP) funciona independente, via a porta `ChatLimiter`.
180
+ - Falha de infraestrutura: `throw` — as rotas mapeiam para 500/502 (nunca 422).
181
+
182
+ ## Segurança
183
+
184
+ - **Webhook sempre responde 200.** Um 5xx faz a Evolution reentregar para sempre; token
185
+ inválido, corpo não-JSON e erro interno devolvem `{ ignored: true }` com log server-side, sem
186
+ vazar o motivo. O `webhookToken` (URL `?token=…` ou header `x-webhook-token`) é a única
187
+ credencial do endpoint: valor longo e aleatório, trocado se vazar.
188
+ - **Não logue segredos.** `apiKey` e `webhookToken` ficam no servidor (`getConfig`); as rotas
189
+ nunca devolvem a sessão completa — só `code`/`status`/`visitorName` (mais o `realtimeToken` no
190
+ POST que abre a conversa). Telefone do visitante e `groupJid` não saem do servidor.
191
+ - **Rate limit em duas camadas:** 5 sessões/IP/10 min no bridge e 30 POSTs/min/IP no `limiter`
192
+ das rotas. O honeypot `website` faz o bot receber sucesso falso, sem tocar em banco nem
193
+ Evolution.
194
+ - **RLS no Supabase:** as tabelas ficam só para a service role (adapter server-side); habilite
195
+ RLS sem policies para `anon`. O canal de realtime é **broadcast** (não passa por RLS) — por
196
+ isso a credencial de escuta é o `realtimeToken` de 192 bits gerado por sessão. No `<script>`,
197
+ `data-supabase-key` é a chave **anon/public** — nunca a service role.
198
+
199
+ ## Desenvolvimento
200
+
201
+ `npm run build` (tsup → `{api,bridge,next,transports/supabase,widget,widget-embed}` na raiz do pacote) ·
202
+ `npm test` (vitest, node + jsdom) · `npm run typecheck` · `npm run lint`. Licença: MIT.
package/api/index.cjs ADDED
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/api/index.ts
21
+ var api_exports = {};
22
+ __export(api_exports, {
23
+ EvolutionApiError: () => EvolutionApiError,
24
+ createEvolutionClient: () => createEvolutionClient,
25
+ generateRealtimeToken: () => generateRealtimeToken,
26
+ generateSessionCode: () => generateSessionCode,
27
+ isValidPhone: () => isValidPhone,
28
+ normalizePhone: () => normalizePhone,
29
+ parseWebhookEvent: () => parseWebhookEvent,
30
+ toWhatsappJid: () => toWhatsappJid
31
+ });
32
+ module.exports = __toCommonJS(api_exports);
33
+
34
+ // src/errors.ts
35
+ var ChatError = class extends Error {
36
+ code;
37
+ cause;
38
+ constructor(message, code, cause) {
39
+ super(message);
40
+ this.name = "ChatError";
41
+ this.code = code;
42
+ this.cause = cause;
43
+ }
44
+ };
45
+
46
+ // src/api/phone.ts
47
+ var NON_DIGITS = /\D+/g;
48
+ function normalizePhone(raw, defaultDdi = "55") {
49
+ const digits = raw.replace(NON_DIGITS, "");
50
+ if (!digits) throw new ChatError("Telefone vazio", "invalid_input");
51
+ const explicitDdi = raw.includes("+");
52
+ const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;
53
+ if (withDdi.length < 10 || withDdi.length > 15) {
54
+ throw new ChatError("Telefone inv\xE1lido", "invalid_input");
55
+ }
56
+ return withDdi;
57
+ }
58
+ function isValidPhone(digits) {
59
+ return digits.length >= 10 && digits.length <= 15;
60
+ }
61
+ function toWhatsappJid(phoneDigits) {
62
+ return `${phoneDigits}@s.whatsapp.net`;
63
+ }
64
+
65
+ // src/api/ids.ts
66
+ var SESSION_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
67
+ var SESSION_CODE_LENGTH = 4;
68
+ var SESSION_ACCEPT_LIMIT = 256 - 256 % SESSION_ALPHABET.length;
69
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
70
+ var TOKEN_BYTES = 24;
71
+ function randomChar(alphabet, acceptLimit) {
72
+ const buffer = new Uint8Array(1);
73
+ for (; ; ) {
74
+ globalThis.crypto.getRandomValues(buffer);
75
+ const byte = buffer[0];
76
+ if (byte !== void 0 && byte < acceptLimit) {
77
+ return alphabet.charAt(byte % alphabet.length);
78
+ }
79
+ }
80
+ }
81
+ function generateSessionCode() {
82
+ return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join("");
83
+ }
84
+ function generateRealtimeToken() {
85
+ const bytes = new Uint8Array(TOKEN_BYTES);
86
+ globalThis.crypto.getRandomValues(bytes);
87
+ let token = "";
88
+ let acc = 0;
89
+ let bits = 0;
90
+ for (const byte of bytes) {
91
+ acc = acc << 8 | byte;
92
+ bits += 8;
93
+ while (bits >= 6) {
94
+ bits -= 6;
95
+ token += BASE64URL_ALPHABET.charAt(acc >>> bits & 63);
96
+ }
97
+ }
98
+ return token;
99
+ }
100
+
101
+ // src/api/client.ts
102
+ var EvolutionApiError = class extends Error {
103
+ constructor(status, body, operation) {
104
+ super(`Evolution ${operation} falhou (${status})`);
105
+ this.status = status;
106
+ this.body = body;
107
+ this.name = "EvolutionApiError";
108
+ }
109
+ status;
110
+ body;
111
+ };
112
+ function readPath(json, ...path) {
113
+ let cur = json;
114
+ for (const key of path) {
115
+ if (!cur || typeof cur !== "object") return void 0;
116
+ cur = cur[key];
117
+ }
118
+ return cur;
119
+ }
120
+ function createEvolutionClient(cfg) {
121
+ const doFetch = cfg.fetchImpl ?? fetch;
122
+ const base = cfg.baseUrl.replace(/\/+$/, "");
123
+ const headers = { "content-type": "application/json", apikey: cfg.apiKey };
124
+ async function request(operation, method, path, body) {
125
+ const response = await doFetch(`${base}${path}`, {
126
+ method,
127
+ headers,
128
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
129
+ });
130
+ if (!response.ok) throw new EvolutionApiError(response.status, await response.text(), operation);
131
+ return response;
132
+ }
133
+ async function requestJson(operation, method, path, body) {
134
+ const response = await request(operation, method, path, body);
135
+ const text = await response.text();
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(text);
139
+ } catch {
140
+ throw new EvolutionApiError(response.status, text, operation);
141
+ }
142
+ if (!parsed || typeof parsed !== "object") throw new EvolutionApiError(response.status, text, operation);
143
+ return { operation, status: response.status, text, json: parsed };
144
+ }
145
+ function requireString(res, ...path) {
146
+ const value = readPath(res.json, ...path);
147
+ if (typeof value !== "string" || value === "") throw new EvolutionApiError(res.status, res.text, res.operation);
148
+ return value;
149
+ }
150
+ return {
151
+ async sendText(instance, number, text) {
152
+ const res = await requestJson("sendText", "POST", `/message/sendText/${instance}`, { number, text });
153
+ return { waMessageId: requireString(res, "key", "id") };
154
+ },
155
+ async createGroup(instance, subject, participants, description) {
156
+ const payload = description === void 0 ? { subject, participants } : { subject, participants, description };
157
+ const res = await requestJson("createGroup", "POST", `/group/create/${instance}`, payload);
158
+ const groupJid = readPath(res.json, "group", "id") ?? res.json.id;
159
+ if (typeof groupJid !== "string" || groupJid === "") {
160
+ throw new ChatError("Evolution createGroup: groupJid ausente/inv\xE1lido na resposta", "send_failed");
161
+ }
162
+ return { groupJid };
163
+ },
164
+ async leaveGroup(instance, groupJid) {
165
+ await request("leaveGroup", "DELETE", `/group/leave/${instance}`, { groupId: groupJid });
166
+ },
167
+ async getConnectionState(instance) {
168
+ const res = await requestJson("getConnectionState", "GET", `/instance/connectionState/${instance}`);
169
+ const state = readPath(res.json, "instance", "state");
170
+ if (state === "open" || state === "connecting" || state === "close") return state;
171
+ throw new EvolutionApiError(res.status, res.text, res.operation);
172
+ },
173
+ async connectQR(instance) {
174
+ const res = await requestJson("connectQR", "GET", `/instance/connect/${instance}`);
175
+ const base64 = readPath(res.json, "base64");
176
+ const pairingCode = readPath(res.json, "pairingCode");
177
+ const code = readPath(res.json, "code");
178
+ return {
179
+ qrBase64: typeof base64 === "string" ? base64 : null,
180
+ pairingCode: typeof pairingCode === "string" ? pairingCode : typeof code === "string" ? code : null
181
+ };
182
+ },
183
+ async setWebhook(instance, url, events) {
184
+ await request("setWebhook", "POST", `/webhook/set/${instance}`, {
185
+ webhook: { enabled: true, url, events, webhookByEvents: false, webhookBase64: false }
186
+ });
187
+ }
188
+ };
189
+ }
190
+
191
+ // src/api/webhook-parser.ts
192
+ function isRecord(value) {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value);
194
+ }
195
+ function textFromMessage(message) {
196
+ if (!isRecord(message)) return null;
197
+ const conversation = message["conversation"];
198
+ if (typeof conversation === "string") {
199
+ const trimmed = conversation.trim();
200
+ return trimmed.length > 0 ? trimmed : null;
201
+ }
202
+ const extended = message["extendedTextMessage"];
203
+ if (isRecord(extended)) {
204
+ const text = extended["text"];
205
+ if (typeof text === "string") {
206
+ const trimmed = text.trim();
207
+ return trimmed.length > 0 ? trimmed : null;
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ function timestampSeconds(value) {
213
+ const seconds = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
214
+ return Number.isFinite(seconds) ? Math.floor(seconds) : 0;
215
+ }
216
+ function parseMessageItem(item) {
217
+ if (!isRecord(item)) return { kind: "ignored", reason: "messages.upsert sem data v\xE1lido" };
218
+ const key = isRecord(item["key"]) ? item["key"] : null;
219
+ const waMessageId = key?.["id"];
220
+ const jid = key?.["remoteJid"];
221
+ if (typeof waMessageId !== "string" || waMessageId.length === 0) {
222
+ return { kind: "ignored", reason: "mensagem sem key.id" };
223
+ }
224
+ if (typeof jid !== "string" || jid.length === 0) {
225
+ return { kind: "ignored", reason: "mensagem sem key.remoteJid" };
226
+ }
227
+ const text = textFromMessage(item["message"]);
228
+ if (text === null) {
229
+ return { kind: "ignored", reason: "mensagem sem texto (conversation/extendedTextMessage)" };
230
+ }
231
+ const senderJid = key?.["participant"];
232
+ return {
233
+ kind: "message",
234
+ event: {
235
+ waMessageId,
236
+ jid,
237
+ fromMe: key?.["fromMe"] === true,
238
+ senderJid: typeof senderJid === "string" ? senderJid : null,
239
+ text,
240
+ timestamp: timestampSeconds(item["messageTimestamp"]),
241
+ raw: item
242
+ }
243
+ };
244
+ }
245
+ function parseWebhookEvent(payload) {
246
+ if (!isRecord(payload)) return { kind: "ignored", reason: "payload n\xE3o \xE9 um objeto" };
247
+ const event = payload["event"];
248
+ if (typeof event !== "string") return { kind: "ignored", reason: "payload sem campo event" };
249
+ const data = payload["data"];
250
+ if (event === "connection.update") {
251
+ const state = isRecord(data) ? data["state"] : void 0;
252
+ if (typeof state === "string" && state.length > 0) return { kind: "connection", state };
253
+ return { kind: "ignored", reason: "connection.update sem state" };
254
+ }
255
+ if (event !== "messages.upsert") {
256
+ return { kind: "ignored", reason: `evento n\xE3o tratado: ${event}` };
257
+ }
258
+ const items = Array.isArray(data) ? data : [data];
259
+ let lastIgnored = { kind: "ignored", reason: "messages.upsert sem data v\xE1lido" };
260
+ for (const item of items) {
261
+ const parsed = parseMessageItem(item);
262
+ if (parsed.kind !== "ignored") return parsed;
263
+ lastIgnored = parsed;
264
+ }
265
+ return lastIgnored;
266
+ }
267
+ // Annotate the CommonJS export names for ESM import in node:
268
+ 0 && (module.exports = {
269
+ EvolutionApiError,
270
+ createEvolutionClient,
271
+ generateRealtimeToken,
272
+ generateSessionCode,
273
+ isValidPhone,
274
+ normalizePhone,
275
+ parseWebhookEvent,
276
+ toWhatsappJid
277
+ });
278
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/index.ts","../src/errors.ts","../src/api/phone.ts","../src/api/ids.ts","../src/api/client.ts","../src/api/webhook-parser.ts"],"sourcesContent":["// src/api/index.ts — superfície pública do subpacote `evolution-chat/api`.\n//\n// Cliente HTTP tipado e framework-agnóstico da Evolution API v2. Auth por header\n// `apikey`. Sem dependências pesadas (fetch global). O erro de transporte\n// (EvolutionApiError) é exportado daqui para o painel distinguir evolution_error\n// de config_error.\n\nexport * from \"./phone\";\nexport * from \"./ids\";\nexport * from \"./client\";\nexport * from \"./webhook-parser\";\n\n// Tipos canônicos compartilhados (re-export para conveniência).\nexport type { InboundMessage } from \"../types\";\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","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/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","// src/api/client.ts — Adapter REST da Evolution API v2 (fetch injetável p/ testes).\nimport { ChatError } from \"../errors\";\n\nexport interface EvolutionFetch { (url: string, init: RequestInit): Promise<Response>; } // ponto de injeção (DI)\n\nexport interface SendTextResult { waMessageId: string; }\nexport interface CreateGroupResult { groupJid: string; }\n\nexport interface EvolutionClient {\n sendText(instance: string, number: string, text: string): Promise<SendTextResult>;\n createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;\n leaveGroup(instance: string, groupJid: string): Promise<void>;\n getConnectionState(instance: string): Promise<\"open\" | \"connecting\" | \"close\">;\n connectQR(instance: string): Promise<{ qrBase64: string | null; pairingCode: string | null }>;\n setWebhook(instance: string, url: string, events: string[]): Promise<void>;\n}\n\nexport class EvolutionApiError extends Error {\n constructor(readonly status: number, readonly body: string, operation: string) {\n super(`Evolution ${operation} falhou (${status})`);\n this.name = \"EvolutionApiError\";\n }\n}\n\ninterface JsonOk { operation: string; status: number; text: string; json: Record<string, unknown>; }\n\n// Lê path aninhado (ex.: \"key\", \"id\") sem lançar: devolve undefined se faltar ou não for objeto.\nfunction readPath(json: Record<string, unknown>, ...path: string[]): unknown {\n let cur: unknown = json;\n for (const key of path) {\n if (!cur || typeof cur !== \"object\") return undefined;\n cur = (cur as Record<string, unknown>)[key];\n }\n return cur;\n}\n\nexport function createEvolutionClient(cfg: { baseUrl: string; apiKey: string; fetchImpl?: EvolutionFetch }): EvolutionClient {\n const doFetch: EvolutionFetch = cfg.fetchImpl ?? fetch;\n const base = cfg.baseUrl.replace(/\\/+$/, \"\"); // normaliza p/ evitar double slash na concatenação\n const headers = { \"content-type\": \"application/json\", apikey: cfg.apiKey };\n\n async function request(operation: string, method: string, path: string, body?: unknown): Promise<Response> {\n const response = await doFetch(`${base}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw new EvolutionApiError(response.status, await response.text(), operation);\n return response;\n }\n\n // Contrato: toda falha de HTTP/parse/shape é EvolutionApiError (a ponte captura via instanceof).\n async function requestJson(operation: string, method: string, path: string, body?: unknown): Promise<JsonOk> {\n const response = await request(operation, method, path, body);\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new EvolutionApiError(response.status, text, operation);\n }\n if (!parsed || typeof parsed !== \"object\") throw new EvolutionApiError(response.status, text, operation);\n return { operation, status: response.status, text, json: parsed as Record<string, unknown> };\n }\n\n function requireString(res: JsonOk, ...path: string[]): string {\n const value = readPath(res.json, ...path);\n if (typeof value !== \"string\" || value === \"\") throw new EvolutionApiError(res.status, res.text, res.operation);\n return value;\n }\n\n return {\n async sendText(instance, number, text) {\n const res = await requestJson(\"sendText\", \"POST\", `/message/sendText/${instance}`, { number, text });\n return { waMessageId: requireString(res, \"key\", \"id\") };\n },\n\n async createGroup(instance, subject, participants, description) {\n const payload = description === undefined ? { subject, participants } : { subject, participants, description };\n const res = await requestJson(\"createGroup\", \"POST\", `/group/create/${instance}`, payload);\n const groupJid = readPath(res.json, \"group\", \"id\") ?? res.json.id; // mesmo derivador de antes, sem cast\n if (typeof groupJid !== \"string\" || groupJid === \"\") {\n throw new ChatError(\"Evolution createGroup: groupJid ausente/inválido na resposta\", \"send_failed\");\n }\n return { groupJid };\n },\n\n async leaveGroup(instance, groupJid) {\n await request(\"leaveGroup\", \"DELETE\", `/group/leave/${instance}`, { groupId: groupJid });\n },\n\n async getConnectionState(instance) {\n const res = await requestJson(\"getConnectionState\", \"GET\", `/instance/connectionState/${instance}`);\n const state = readPath(res.json, \"instance\", \"state\");\n if (state === \"open\" || state === \"connecting\" || state === \"close\") return state;\n throw new EvolutionApiError(res.status, res.text, res.operation);\n },\n\n async connectQR(instance) {\n const res = await requestJson(\"connectQR\", \"GET\", `/instance/connect/${instance}`);\n const base64 = readPath(res.json, \"base64\");\n const pairingCode = readPath(res.json, \"pairingCode\");\n const code = readPath(res.json, \"code\");\n return {\n qrBase64: typeof base64 === \"string\" ? base64 : null,\n pairingCode: typeof pairingCode === \"string\" ? pairingCode : typeof code === \"string\" ? code : null,\n };\n },\n\n async setWebhook(instance, url, events) {\n await request(\"setWebhook\", \"POST\", `/webhook/set/${instance}`, {\n webhook: { enabled: true, url, events, webhookByEvents: false, webhookBase64: false },\n });\n },\n };\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"],"mappings":"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","names":[]}
@@ -0,0 +1,65 @@
1
+ declare function normalizePhone(raw: string, defaultDdi?: string): string;
2
+ declare function isValidPhone(digits: string): boolean;
3
+ declare function toWhatsappJid(phoneDigits: string): string;
4
+
5
+ declare function generateSessionCode(): string;
6
+ declare function generateRealtimeToken(): string;
7
+
8
+ interface EvolutionFetch {
9
+ (url: string, init: RequestInit): Promise<Response>;
10
+ }
11
+ interface SendTextResult {
12
+ waMessageId: string;
13
+ }
14
+ interface CreateGroupResult {
15
+ groupJid: string;
16
+ }
17
+ interface EvolutionClient {
18
+ sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
19
+ createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
20
+ leaveGroup(instance: string, groupJid: string): Promise<void>;
21
+ getConnectionState(instance: string): Promise<"open" | "connecting" | "close">;
22
+ connectQR(instance: string): Promise<{
23
+ qrBase64: string | null;
24
+ pairingCode: string | null;
25
+ }>;
26
+ setWebhook(instance: string, url: string, events: string[]): Promise<void>;
27
+ }
28
+ declare class EvolutionApiError extends Error {
29
+ readonly status: number;
30
+ readonly body: string;
31
+ constructor(status: number, body: string, operation: string);
32
+ }
33
+ declare function createEvolutionClient(cfg: {
34
+ baseUrl: string;
35
+ apiKey: string;
36
+ fetchImpl?: EvolutionFetch;
37
+ }): EvolutionClient;
38
+
39
+ /**
40
+ * Mensagem de entrada normalizada do webhook da Evolution, independente do
41
+ * formato bruto do payload.
42
+ */
43
+ interface InboundMessage {
44
+ waMessageId: string;
45
+ jid: string;
46
+ fromMe: boolean;
47
+ senderJid: string | null;
48
+ text: string | null;
49
+ timestamp: number;
50
+ raw: unknown;
51
+ }
52
+
53
+ type ParsedWebhook = {
54
+ kind: "message";
55
+ event: InboundMessage;
56
+ } | {
57
+ kind: "connection";
58
+ state: string;
59
+ } | {
60
+ kind: "ignored";
61
+ reason: string;
62
+ };
63
+ declare function parseWebhookEvent(payload: unknown): ParsedWebhook;
64
+
65
+ export { type CreateGroupResult, EvolutionApiError, type EvolutionClient, type EvolutionFetch, type InboundMessage, type ParsedWebhook, type SendTextResult, createEvolutionClient, generateRealtimeToken, generateSessionCode, isValidPhone, normalizePhone, parseWebhookEvent, toWhatsappJid };
package/api/index.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ declare function normalizePhone(raw: string, defaultDdi?: string): string;
2
+ declare function isValidPhone(digits: string): boolean;
3
+ declare function toWhatsappJid(phoneDigits: string): string;
4
+
5
+ declare function generateSessionCode(): string;
6
+ declare function generateRealtimeToken(): string;
7
+
8
+ interface EvolutionFetch {
9
+ (url: string, init: RequestInit): Promise<Response>;
10
+ }
11
+ interface SendTextResult {
12
+ waMessageId: string;
13
+ }
14
+ interface CreateGroupResult {
15
+ groupJid: string;
16
+ }
17
+ interface EvolutionClient {
18
+ sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
19
+ createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
20
+ leaveGroup(instance: string, groupJid: string): Promise<void>;
21
+ getConnectionState(instance: string): Promise<"open" | "connecting" | "close">;
22
+ connectQR(instance: string): Promise<{
23
+ qrBase64: string | null;
24
+ pairingCode: string | null;
25
+ }>;
26
+ setWebhook(instance: string, url: string, events: string[]): Promise<void>;
27
+ }
28
+ declare class EvolutionApiError extends Error {
29
+ readonly status: number;
30
+ readonly body: string;
31
+ constructor(status: number, body: string, operation: string);
32
+ }
33
+ declare function createEvolutionClient(cfg: {
34
+ baseUrl: string;
35
+ apiKey: string;
36
+ fetchImpl?: EvolutionFetch;
37
+ }): EvolutionClient;
38
+
39
+ /**
40
+ * Mensagem de entrada normalizada do webhook da Evolution, independente do
41
+ * formato bruto do payload.
42
+ */
43
+ interface InboundMessage {
44
+ waMessageId: string;
45
+ jid: string;
46
+ fromMe: boolean;
47
+ senderJid: string | null;
48
+ text: string | null;
49
+ timestamp: number;
50
+ raw: unknown;
51
+ }
52
+
53
+ type ParsedWebhook = {
54
+ kind: "message";
55
+ event: InboundMessage;
56
+ } | {
57
+ kind: "connection";
58
+ state: string;
59
+ } | {
60
+ kind: "ignored";
61
+ reason: string;
62
+ };
63
+ declare function parseWebhookEvent(payload: unknown): ParsedWebhook;
64
+
65
+ export { type CreateGroupResult, EvolutionApiError, type EvolutionClient, type EvolutionFetch, type InboundMessage, type ParsedWebhook, type SendTextResult, createEvolutionClient, generateRealtimeToken, generateSessionCode, isValidPhone, normalizePhone, parseWebhookEvent, toWhatsappJid };