@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 +202 -0
- package/api/index.cjs +278 -0
- package/api/index.cjs.map +1 -0
- package/api/index.d.cts +65 -0
- package/api/index.d.ts +65 -0
- package/api/index.js +244 -0
- package/api/index.js.map +1 -0
- package/bridge/index.cjs +412 -0
- package/bridge/index.cjs.map +1 -0
- package/bridge/index.d.cts +245 -0
- package/bridge/index.d.ts +245 -0
- package/bridge/index.js +381 -0
- package/bridge/index.js.map +1 -0
- package/next/index.cjs +218 -0
- package/next/index.cjs.map +1 -0
- package/next/index.d.cts +205 -0
- package/next/index.d.ts +205 -0
- package/next/index.js +190 -0
- package/next/index.js.map +1 -0
- package/package.json +44 -0
- package/transports/supabase/index.cjs +74 -0
- package/transports/supabase/index.cjs.map +1 -0
- package/transports/supabase/index.d.cts +45 -0
- package/transports/supabase/index.d.ts +45 -0
- package/transports/supabase/index.js +48 -0
- package/transports/supabase/index.js.map +1 -0
- package/widget/index.cjs +1045 -0
- package/widget/index.cjs.map +1 -0
- package/widget/index.d.cts +122 -0
- package/widget/index.d.ts +122 -0
- package/widget/index.js +1009 -0
- package/widget/index.js.map +1 -0
- package/widget/styles.css +355 -0
- package/widget/styles.css.map +1 -0
- package/widget/styles.d.cts +2 -0
- package/widget/styles.d.ts +2 -0
- package/widget-embed/evolution-chat.iife.js +28706 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/next/chat-routes.ts"],"sourcesContent":["// 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/next/chat-routes.ts — factories de rotas para o App Router do Next, SEM importar \"next\".\n//\n// O plano proíbe dependência de \"next\"/\"next/server\": o App Router aceita os handlers\n// padrão Web `Request → Promise<Response>`, então as fábricas devolvem funções puras e o\n// arquivo de rota do consumidor apenas encaminha. Padrão de uso no LMS\n// (app/api/chat/route.ts):\n//\n// import { createChatRoutes } from \"@erlancarreira/evolution-chat/next\";\n// const handlers = createChatRoutes(deps);\n// export const GET = (req: Request) => handlers.GET(req);\n// export const POST = (req: Request) => handlers.POST(req);\n//\n// e em app/api/chat/webhook/route.ts:\n//\n// import { createWebhookRoute } from \"@erlancarreira/evolution-chat/next\";\n// const webhook = createWebhookRoute({ bridge, getConfig });\n// export const POST = (req: Request) => webhook.POST(req);\n//\n// Mapeamento ChatError.code → HTTP (contrato da Task 8):\n// invalid_input → 422 {error, field?} rate_limited → 429 {error}\n// session_not_found → 404 session_closed → 409\n// group_create_failed / send_failed → 502 {error}\n// store_error → 502 inesperado → 500 (log server-side)\n//\n// O mapeamento lê o `code` por DUCK-TYPING, nunca por `instanceof ChatError`. Motivo:\n// o tsup emite uma cópia própria de `ChatError` em CADA bundle de entry (sem shared\n// chunks), então a classe que vive no bundle /bridge NÃO é a mesma do bundle /next —\n// um ChatError lançado pela bridge falha o `instanceof` na rota e degradava para 500.\n// Qualquer objeto `{ code, message }` (esta classe, outra cópia dela, ou um erro\n// serializado) produz o status correto. Ver statusForError().\n//\n// Segurança: as respostas NUNCA devolvem a sessão completa — só os campos que o widget\n// precisa (code/status/visitorName/realtimeToken). Telefone do visitante e groupJid não\n// saem do servidor. Webhook sempre responde 200: um 5xx faz a Evolution reenviar para\n// sempre; token errado é ignorado com `{ignored:true}` (registra log, não vaza motivo).\n\nimport { ChatError } from \"../errors\";\nimport type { ChatBridge } from \"../bridge\";\nimport type { ChatLimiter } from \"../bridge/types\";\nimport type { ChatConfig } from \"../types\";\n\nexport interface ChatRoutesDeps {\n bridge: ChatBridge;\n /** DI; default: permite tudo (o limite de sessões/IP de domínio vive no ChatBridge). */\n limiter?: ChatLimiter;\n /** DI (LMS: sha256(ip+salt)). Default: `() => null` → limiter nunca é acionado. */\n getIpHash?: (req: Request) => string | null;\n /** Resolvida (await) no início de cada request; o handler faz bridge.setConfig. */\n getConfig: () => Promise<ChatConfig> | ChatConfig;\n}\n\nexport interface ChatRoutes {\n GET: (req: Request) => Promise<Response>;\n POST: (req: Request) => Promise<Response>;\n}\n\nexport interface WebhookRouteDeps {\n bridge: ChatBridge;\n getConfig: ChatRoutesDeps[\"getConfig\"];\n}\n\nexport interface WebhookRoute {\n POST: (req: Request) => Promise<Response>;\n}\n\n// Guarda de transporte por IP (a de domínio — 5 sessões/10min — é do ChatBridge).\nconst POST_LIMIT = 30;\nconst POST_WINDOW_MS = 60 * 1000;\n\nfunction json(status: number, body: Record<string, unknown>): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json; charset=utf-8\" },\n });\n}\n\nfunction asText(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction isFilled(value: unknown): boolean {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nasync function readJsonBody(req: Request): Promise<Record<string, unknown>> {\n let parsed: unknown;\n try {\n parsed = await req.json();\n } catch {\n throw new ChatError(\"JSON inválido\", \"invalid_input\");\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new ChatError(\"O corpo da requisição deve ser um objeto JSON\", \"invalid_input\");\n }\n return parsed as Record<string, unknown>;\n}\n\n// O bridge nomeia o campo na mensagem (\"Nome deve ter…\", \"Telefone inválido\"); a rota\n// traduz para o nome do campo do corpo (name/phone/message) para o widget destacar.\nconst FIELD_BY_LABEL: Record<string, string> = { Nome: \"name\", Mensagem: \"message\" };\n\nfunction fieldFromMessage(message: string): string | null {\n if (message.startsWith(\"Telefone\")) return \"phone\";\n const label = /^(\\w+) deve ter\\b/.exec(message)?.[1];\n return label === undefined ? null : FIELD_BY_LABEL[label] ?? null;\n}\n\ninterface StatusForError {\n status: number;\n error: string;\n field?: string;\n}\n\n// DUCK-TYPE (ver cabeçalho do arquivo): o `code` é lido por propriedade, nunca por\n// `instanceof ChatError` — a classe é duplicada nos bundles do tsup e a comparação de\n// identidade falharia entre /bridge e /next. Erro sem `code` reconhecível → 500.\nfunction statusForError(err: unknown): StatusForError {\n const code =\n typeof err === \"object\" && err !== null && \"code\" in err\n ? (err as { code?: unknown }).code\n : undefined;\n const message =\n typeof err === \"object\" && err !== null && typeof (err as { message?: unknown }).message === \"string\"\n ? (err as { message: string }).message\n : \"\";\n // Objeto duck-typed sem `message` (erro serializado, plain object) ainda responde com o\n // próprio code no corpo — nunca uma string vazia que o widget não conseguiria exibir.\n const text = message !== \"\" || typeof code !== \"string\" ? message : code;\n\n switch (code) {\n case \"invalid_input\": {\n const field = fieldFromMessage(text);\n return field === null\n ? { status: 422, error: text }\n : { status: 422, error: text, field };\n }\n case \"rate_limited\":\n return { status: 429, error: text };\n case \"session_not_found\":\n return { status: 404, error: text };\n case \"session_closed\":\n return { status: 409, error: text };\n case \"group_create_failed\":\n case \"send_failed\":\n return { status: 502, error: text };\n case \"disabled\":\n return { status: 404, error: \"not_found\" };\n case \"unauthorized\":\n return { status: 401, error: text };\n case \"store_error\":\n return { status: 502, error: text };\n default:\n return { status: 500, error: \"erro interno\" };\n }\n}\n\nfunction errorResponse(error: unknown): Response {\n const { status, error: message, field } = statusForError(error);\n // 500 só sai do ramo default (nenhum code mapeado responde 500) → é sempre inesperado,\n // e o detalhe fica apenas no log do servidor: o corpo nunca vaza a mensagem original.\n if (status === 500) {\n console.error(\"[evolution-chat] erro inesperado na rota:\", error);\n return json(500, { error: message });\n }\n const body: Record<string, unknown> = { error: message };\n if (field !== undefined) body[\"field\"] = field;\n return json(status, body);\n}\n\nexport function createChatRoutes(deps: ChatRoutesDeps): ChatRoutes {\n const { bridge } = deps;\n const getIpHash = deps.getIpHash ?? (() => null);\n\n // GET /api/chat?token=…&after=… — replay de histórico para o widget.\n async function GET(req: Request): Promise<Response> {\n try {\n const url = new URL(req.url);\n const token = url.searchParams.get(\"token\");\n if (token === null || token === \"\") {\n return json(400, { error: \"token é obrigatório\" });\n }\n bridge.setConfig(await deps.getConfig());\n const after = url.searchParams.get(\"after\");\n const { session, messages } = await bridge.history(token, after);\n if (session === null) return json(404, { error: \"session_not_found\" });\n return json(200, {\n session: { code: session.code, status: session.status, visitorName: session.visitorName },\n messages,\n });\n } catch (error) {\n return errorResponse(error);\n }\n }\n\n // POST /api/chat — sem token abre chat; com token envia mensagem na sessão.\n async function POST(req: Request): Promise<Response> {\n let body: Record<string, unknown>;\n try {\n body = await readJsonBody(req);\n } catch (error) {\n return errorResponse(error);\n }\n try {\n const cfg = await deps.getConfig();\n bridge.setConfig(cfg);\n\n const token = asText(body[\"token\"]);\n const ipHash = getIpHash(req);\n\n if (token === \"\") {\n // Feature oculta: 404 genérico, nada confirma que o chat existe desligado.\n if (!cfg.enabled) return json(404, { error: \"not_found\" });\n // Anti-bot silencioso: finge sucesso sem tocar em store/Evolution/limiter.\n if (isFilled(body[\"honeypot\"])) {\n return json(200, { session: { code: \"XXXX\", status: \"closed\" }, messages: [] });\n }\n }\n\n if (deps.limiter !== undefined && ipHash !== null && ipHash !== \"\") {\n const result = await deps.limiter(ipHash, POST_LIMIT, POST_WINDOW_MS);\n if (!result.success) {\n return json(429, { error: \"Muitas requisições. Tente novamente em alguns minutos.\" });\n }\n }\n\n if (token !== \"\") {\n const message = await bridge.sendVisitorMessage(token, asText(body[\"message\"]));\n return json(200, { message });\n }\n\n const { session, messages } = await bridge.startChat({\n name: asText(body[\"name\"]),\n phone: asText(body[\"phone\"]),\n message: asText(body[\"message\"]),\n contact: typeof body[\"contact\"] === \"string\" ? body[\"contact\"] : null,\n ipHash,\n userAgent: req.headers.get(\"user-agent\") ?? null,\n honeypot: typeof body[\"honeypot\"] === \"string\" ? body[\"honeypot\"] : null,\n });\n return json(200, {\n session: {\n code: session.code,\n status: session.status,\n realtimeToken: session.realtimeToken,\n visitorName: session.visitorName,\n },\n messages,\n });\n } catch (error) {\n return errorResponse(error);\n }\n }\n\n return { GET, POST };\n}\n\nexport function createWebhookRoute(deps: WebhookRouteDeps): WebhookRoute {\n const { bridge } = deps;\n\n // POST /api/chat/webhook — entrada da Evolution. SEMPRE 200 (ver cabeçalho do arquivo).\n async function POST(req: Request): Promise<Response> {\n try {\n const cfg = await deps.getConfig();\n bridge.setConfig(cfg);\n\n const url = new URL(req.url);\n const token = url.searchParams.get(\"token\") ?? req.headers.get(\"x-webhook-token\") ?? \"\";\n if (token === \"\" || token !== cfg.webhookToken) {\n // Log server-side para auditoria; a resposta é neutra e não vaza o motivo.\n console.warn(\"[evolution-chat] webhook rejeitado: token inválido ou ausente\");\n return json(200, { ignored: true });\n }\n\n let payload: unknown;\n try {\n payload = await req.json();\n } catch {\n console.warn(\"[evolution-chat] webhook rejeitado: corpo não é JSON válido\");\n return json(200, { ignored: true });\n }\n\n const { handled } = await bridge.handleWebhook(payload);\n return json(200, { handled });\n } catch (error) {\n // getConfig pode falhar (store fora): ainda assim 200, senão a Evolution reenvia.\n console.error(\"[evolution-chat] webhook falhou (respondendo 200):\", error);\n return json(200, { ignored: true });\n }\n }\n\n return { POST };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erlancarreira/evolution-chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chat widget + Evolution API bridge (WhatsApp) — reutilizável, hexagonal, SOLID",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./api/index.js",
|
|
8
|
+
"types": "./api/index.d.ts",
|
|
9
|
+
"files": ["api", "bridge", "next", "transports", "widget", "widget-embed", "README.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"lint": "eslint src test",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@supabase/supabase-js": ">=2.45.0",
|
|
18
|
+
"react": ">=18.0.0",
|
|
19
|
+
"react-dom": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependenciesMeta": {
|
|
22
|
+
"@supabase/supabase-js": { "optional": true },
|
|
23
|
+
"react": { "optional": true },
|
|
24
|
+
"react-dom": { "optional": true }
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@testing-library/dom": "^10.4.0",
|
|
28
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
29
|
+
"@testing-library/react": "^16.1.0",
|
|
30
|
+
"@eslint/js": "^9",
|
|
31
|
+
"@types/node": "^20.19.0",
|
|
32
|
+
"@types/react": "^18.3.0",
|
|
33
|
+
"@types/react-dom": "^18.3.0",
|
|
34
|
+
"eslint": "^9",
|
|
35
|
+
"jsdom": "^25.0.0",
|
|
36
|
+
"react": "^18.3.1",
|
|
37
|
+
"react-dom": "^18.3.1",
|
|
38
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
39
|
+
"tsup": "^8.3.0",
|
|
40
|
+
"typescript": "^5.6.0",
|
|
41
|
+
"typescript-eslint": "^8",
|
|
42
|
+
"vitest": "^2.1.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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/transports/supabase/index.ts
|
|
21
|
+
var supabase_exports = {};
|
|
22
|
+
__export(supabase_exports, {
|
|
23
|
+
createSupabaseRealtimeHandle: () => createSupabaseRealtimeHandle,
|
|
24
|
+
createSupabaseTransport: () => createSupabaseTransport
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(supabase_exports);
|
|
27
|
+
var import_supabase_js = require("@supabase/supabase-js");
|
|
28
|
+
var CHANNEL_PREFIX = "chat:";
|
|
29
|
+
var BROADCAST_EVENT = "chat";
|
|
30
|
+
var channelName = (realtimeToken) => `${CHANNEL_PREFIX}${realtimeToken}`;
|
|
31
|
+
var STATUS_BY_REALTIME_STATE = {
|
|
32
|
+
SUBSCRIBED: "open",
|
|
33
|
+
CHANNEL_ERROR: "closed",
|
|
34
|
+
TIMED_OUT: "closed",
|
|
35
|
+
CLOSED: "closed"
|
|
36
|
+
};
|
|
37
|
+
function createSupabaseTransport(admin) {
|
|
38
|
+
return {
|
|
39
|
+
async publish(realtimeToken, event) {
|
|
40
|
+
const channel = channelName(realtimeToken);
|
|
41
|
+
const response = await admin.channel(channel).send({
|
|
42
|
+
type: "broadcast",
|
|
43
|
+
event: BROADCAST_EVENT,
|
|
44
|
+
payload: event
|
|
45
|
+
});
|
|
46
|
+
if (response !== "ok") {
|
|
47
|
+
throw new Error(`Supabase broadcast "${channel}" n\xE3o entregue: ${response}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createSupabaseRealtimeHandle(url, anonKey) {
|
|
53
|
+
const client = (0, import_supabase_js.createClient)(url, anonKey);
|
|
54
|
+
return {
|
|
55
|
+
subscribe(realtimeToken, onEvent, onStatus) {
|
|
56
|
+
const channel = client.channel(channelName(realtimeToken));
|
|
57
|
+
channel.on("broadcast", { event: BROADCAST_EVENT }, (message) => {
|
|
58
|
+
onEvent(message.payload);
|
|
59
|
+
});
|
|
60
|
+
channel.subscribe((status) => {
|
|
61
|
+
onStatus?.(STATUS_BY_REALTIME_STATE[status] ?? "closed");
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
void channel.unsubscribe();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
70
|
+
0 && (module.exports = {
|
|
71
|
+
createSupabaseRealtimeHandle,
|
|
72
|
+
createSupabaseTransport
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transports/supabase/index.ts"],"sourcesContent":["// src/transports/supabase/index.ts — adapter Supabase Realtime (broadcast) para as portas\n// RealtimeTransport (server publica) e RealtimeHandle (widget assina).\n//\n// Canal por sessão: `chat:<realtimeToken>`, evento broadcast `chat`, payload = ChatEvent.\n// O token aleatório de 24 bytes (192 bits, src/api/ids.ts) gerado na criação da sessão é a\n// única credencial do canal (broadcast não passa por RLS), então quem não tem o token não escuta.\n//\n// Server (createSupabaseTransport): recebe um SupabaseClient já construído pelo hospedeiro\n// (tipicamente com a service key). Não assinamos o canal: no supabase-js v2, `send()` num\n// canal não-joined cai no endpoint REST de broadcast (POST /realtime/v1/api/broadcast), que\n// é exatamente o caminho recomendado para publicar sem manter um WebSocket aberto. Basta\n// aguardar o `send()` — ele resolve com 'ok' | 'timed out' | 'error' | …\n//\n// Widget (createSupabaseRealtimeHandle): constrói seu próprio client com a anon key e\n// assina o canal; `subscribe` devolve o `unsubscribe` (deixa o canal) e reporta a conexão\n// via onStatus (\"open\" em SUBSCRIBED, \"closed\" em CHANNEL_ERROR/TIMED_OUT/CLOSED).\n\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ChatEvent, RealtimeHandle, RealtimeTransport } from \"../../bridge\";\n\n/** Prefixo do canal por sessão: `chat:<realtimeToken>`. */\nconst CHANNEL_PREFIX = \"chat:\";\n/** Nome do evento broadcast (o único usado pelo SDK). */\nconst BROADCAST_EVENT = \"chat\";\n\nconst channelName = (realtimeToken: string): string => `${CHANNEL_PREFIX}${realtimeToken}`;\n\n/**\n * Estados de assinatura do Realtime → \"open\"/\"closed\" da porta.\n * Comparar via lookup (e não `status === \"SUBSCRIBED\"`) porque REALTIME_SUBSCRIBE_STATES é\n * um enum string não reexportado por @supabase/supabase-js; o valor em runtime é a string.\n * Desconhecidos caem em \"closed\" (conservador: o widget mostra \"reconectando\").\n */\nconst STATUS_BY_REALTIME_STATE: Record<string, \"open\" | \"closed\"> = {\n SUBSCRIBED: \"open\",\n CHANNEL_ERROR: \"closed\",\n TIMED_OUT: \"closed\",\n CLOSED: \"closed\",\n};\n\n/** Publica `event` no canal broadcast da sessão. Rejeita se o broadcast não for aceito. */\nexport function createSupabaseTransport(admin: SupabaseClient): RealtimeTransport {\n return {\n async publish(realtimeToken: string, event: ChatEvent): Promise<void> {\n const channel = channelName(realtimeToken);\n const response = await admin.channel(channel).send({\n type: \"broadcast\",\n event: BROADCAST_EVENT,\n payload: event,\n });\n // 'ok' | 'timed out' | 'error' | 'rate limited' | 'channel error' | …\n // Rejeitar em não-'ok' é intencional: o bridge (Task 6) captura e devolve\n // handled:false, acionando a reentrega idempotente do webhook.\n if (response !== \"ok\") {\n throw new Error(`Supabase broadcast \"${channel}\" não entregue: ${response}`);\n }\n },\n };\n}\n\n/**\n * Handle client-side (widget): `subscribe(token, onEvent, onStatus?)` assina\n * `chat:<token>` e devolve o unsubscribe. O client é criado uma única vez na fábrica.\n */\nexport function createSupabaseRealtimeHandle(url: string, anonKey: string): RealtimeHandle {\n const client = createClient(url, anonKey);\n\n return {\n subscribe(realtimeToken, onEvent, onStatus) {\n const channel = client.channel(channelName(realtimeToken));\n\n channel.on(\"broadcast\", { event: BROADCAST_EVENT }, (message: { payload: unknown }) => {\n onEvent(message.payload as ChatEvent);\n });\n\n channel.subscribe((status: string) => {\n onStatus?.(STATUS_BY_REALTIME_STATE[status] ?? \"closed\");\n });\n\n return () => {\n void channel.unsubscribe();\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,yBAAkD;AAIlD,IAAM,iBAAiB;AAEvB,IAAM,kBAAkB;AAExB,IAAM,cAAc,CAAC,kBAAkC,GAAG,cAAc,GAAG,aAAa;AAQxF,IAAM,2BAA8D;AAAA,EAClE,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,WAAW;AAAA,EACX,QAAQ;AACV;AAGO,SAAS,wBAAwB,OAA0C;AAChF,SAAO;AAAA,IACL,MAAM,QAAQ,eAAuB,OAAiC;AACpE,YAAM,UAAU,YAAY,aAAa;AACzC,YAAM,WAAW,MAAM,MAAM,QAAQ,OAAO,EAAE,KAAK;AAAA,QACjD,MAAM;AAAA,QACN,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAID,UAAI,aAAa,MAAM;AACrB,cAAM,IAAI,MAAM,uBAAuB,OAAO,sBAAmB,QAAQ,EAAE;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,6BAA6B,KAAa,SAAiC;AACzF,QAAM,aAAS,iCAAa,KAAK,OAAO;AAExC,SAAO;AAAA,IACL,UAAU,eAAe,SAAS,UAAU;AAC1C,YAAM,UAAU,OAAO,QAAQ,YAAY,aAAa,CAAC;AAEzD,cAAQ,GAAG,aAAa,EAAE,OAAO,gBAAgB,GAAG,CAAC,YAAkC;AACrF,gBAAQ,QAAQ,OAAoB;AAAA,MACtC,CAAC;AAED,cAAQ,UAAU,CAAC,WAAmB;AACpC,mBAAW,yBAAyB,MAAM,KAAK,QAAQ;AAAA,MACzD,CAAC;AAED,aAAO,MAAM;AACX,aAAK,QAAQ,YAAY;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
type ChatMessageDirection = "visitor" | "owner";
|
|
4
|
+
type ChatMessageStatus = "pending" | "sent" | "failed";
|
|
5
|
+
type ChatSessionStatus = "active" | "closed" | "failed";
|
|
6
|
+
/** Mensagem persistida (domínio). */
|
|
7
|
+
interface ChatMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
direction: ChatMessageDirection;
|
|
11
|
+
body: string;
|
|
12
|
+
status: ChatMessageStatus;
|
|
13
|
+
waMessageId: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
/** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
|
|
17
|
+
type ChatEvent = {
|
|
18
|
+
type: "message";
|
|
19
|
+
message: ChatMessage;
|
|
20
|
+
} | {
|
|
21
|
+
type: "session";
|
|
22
|
+
status: ChatSessionStatus;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Publicação server-side de eventos para o canal de uma sessão (fire-and-forget). */
|
|
26
|
+
interface RealtimeTransport {
|
|
27
|
+
publish(realtimeToken: string, event: ChatEvent): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Assinatura client-side do mesmo canal (usada pelo widget — Task 9).
|
|
31
|
+
* `subscribe` devolve o unsubscribe; `onStatus` reporta conexão do transporte.
|
|
32
|
+
*/
|
|
33
|
+
interface RealtimeHandle {
|
|
34
|
+
subscribe(realtimeToken: string, onEvent: (e: ChatEvent) => void, onStatus?: (s: "open" | "closed") => void): () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Publica `event` no canal broadcast da sessão. Rejeita se o broadcast não for aceito. */
|
|
38
|
+
declare function createSupabaseTransport(admin: SupabaseClient): RealtimeTransport;
|
|
39
|
+
/**
|
|
40
|
+
* Handle client-side (widget): `subscribe(token, onEvent, onStatus?)` assina
|
|
41
|
+
* `chat:<token>` e devolve o unsubscribe. O client é criado uma única vez na fábrica.
|
|
42
|
+
*/
|
|
43
|
+
declare function createSupabaseRealtimeHandle(url: string, anonKey: string): RealtimeHandle;
|
|
44
|
+
|
|
45
|
+
export { createSupabaseRealtimeHandle, createSupabaseTransport };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
type ChatMessageDirection = "visitor" | "owner";
|
|
4
|
+
type ChatMessageStatus = "pending" | "sent" | "failed";
|
|
5
|
+
type ChatSessionStatus = "active" | "closed" | "failed";
|
|
6
|
+
/** Mensagem persistida (domínio). */
|
|
7
|
+
interface ChatMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
direction: ChatMessageDirection;
|
|
11
|
+
body: string;
|
|
12
|
+
status: ChatMessageStatus;
|
|
13
|
+
waMessageId: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
/** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
|
|
17
|
+
type ChatEvent = {
|
|
18
|
+
type: "message";
|
|
19
|
+
message: ChatMessage;
|
|
20
|
+
} | {
|
|
21
|
+
type: "session";
|
|
22
|
+
status: ChatSessionStatus;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Publicação server-side de eventos para o canal de uma sessão (fire-and-forget). */
|
|
26
|
+
interface RealtimeTransport {
|
|
27
|
+
publish(realtimeToken: string, event: ChatEvent): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Assinatura client-side do mesmo canal (usada pelo widget — Task 9).
|
|
31
|
+
* `subscribe` devolve o unsubscribe; `onStatus` reporta conexão do transporte.
|
|
32
|
+
*/
|
|
33
|
+
interface RealtimeHandle {
|
|
34
|
+
subscribe(realtimeToken: string, onEvent: (e: ChatEvent) => void, onStatus?: (s: "open" | "closed") => void): () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Publica `event` no canal broadcast da sessão. Rejeita se o broadcast não for aceito. */
|
|
38
|
+
declare function createSupabaseTransport(admin: SupabaseClient): RealtimeTransport;
|
|
39
|
+
/**
|
|
40
|
+
* Handle client-side (widget): `subscribe(token, onEvent, onStatus?)` assina
|
|
41
|
+
* `chat:<token>` e devolve o unsubscribe. O client é criado uma única vez na fábrica.
|
|
42
|
+
*/
|
|
43
|
+
declare function createSupabaseRealtimeHandle(url: string, anonKey: string): RealtimeHandle;
|
|
44
|
+
|
|
45
|
+
export { createSupabaseRealtimeHandle, createSupabaseTransport };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/transports/supabase/index.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
var CHANNEL_PREFIX = "chat:";
|
|
4
|
+
var BROADCAST_EVENT = "chat";
|
|
5
|
+
var channelName = (realtimeToken) => `${CHANNEL_PREFIX}${realtimeToken}`;
|
|
6
|
+
var STATUS_BY_REALTIME_STATE = {
|
|
7
|
+
SUBSCRIBED: "open",
|
|
8
|
+
CHANNEL_ERROR: "closed",
|
|
9
|
+
TIMED_OUT: "closed",
|
|
10
|
+
CLOSED: "closed"
|
|
11
|
+
};
|
|
12
|
+
function createSupabaseTransport(admin) {
|
|
13
|
+
return {
|
|
14
|
+
async publish(realtimeToken, event) {
|
|
15
|
+
const channel = channelName(realtimeToken);
|
|
16
|
+
const response = await admin.channel(channel).send({
|
|
17
|
+
type: "broadcast",
|
|
18
|
+
event: BROADCAST_EVENT,
|
|
19
|
+
payload: event
|
|
20
|
+
});
|
|
21
|
+
if (response !== "ok") {
|
|
22
|
+
throw new Error(`Supabase broadcast "${channel}" n\xE3o entregue: ${response}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createSupabaseRealtimeHandle(url, anonKey) {
|
|
28
|
+
const client = createClient(url, anonKey);
|
|
29
|
+
return {
|
|
30
|
+
subscribe(realtimeToken, onEvent, onStatus) {
|
|
31
|
+
const channel = client.channel(channelName(realtimeToken));
|
|
32
|
+
channel.on("broadcast", { event: BROADCAST_EVENT }, (message) => {
|
|
33
|
+
onEvent(message.payload);
|
|
34
|
+
});
|
|
35
|
+
channel.subscribe((status) => {
|
|
36
|
+
onStatus?.(STATUS_BY_REALTIME_STATE[status] ?? "closed");
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
void channel.unsubscribe();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
createSupabaseRealtimeHandle,
|
|
46
|
+
createSupabaseTransport
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transports/supabase/index.ts"],"sourcesContent":["// src/transports/supabase/index.ts — adapter Supabase Realtime (broadcast) para as portas\n// RealtimeTransport (server publica) e RealtimeHandle (widget assina).\n//\n// Canal por sessão: `chat:<realtimeToken>`, evento broadcast `chat`, payload = ChatEvent.\n// O token aleatório de 24 bytes (192 bits, src/api/ids.ts) gerado na criação da sessão é a\n// única credencial do canal (broadcast não passa por RLS), então quem não tem o token não escuta.\n//\n// Server (createSupabaseTransport): recebe um SupabaseClient já construído pelo hospedeiro\n// (tipicamente com a service key). Não assinamos o canal: no supabase-js v2, `send()` num\n// canal não-joined cai no endpoint REST de broadcast (POST /realtime/v1/api/broadcast), que\n// é exatamente o caminho recomendado para publicar sem manter um WebSocket aberto. Basta\n// aguardar o `send()` — ele resolve com 'ok' | 'timed out' | 'error' | …\n//\n// Widget (createSupabaseRealtimeHandle): constrói seu próprio client com a anon key e\n// assina o canal; `subscribe` devolve o `unsubscribe` (deixa o canal) e reporta a conexão\n// via onStatus (\"open\" em SUBSCRIBED, \"closed\" em CHANNEL_ERROR/TIMED_OUT/CLOSED).\n\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ChatEvent, RealtimeHandle, RealtimeTransport } from \"../../bridge\";\n\n/** Prefixo do canal por sessão: `chat:<realtimeToken>`. */\nconst CHANNEL_PREFIX = \"chat:\";\n/** Nome do evento broadcast (o único usado pelo SDK). */\nconst BROADCAST_EVENT = \"chat\";\n\nconst channelName = (realtimeToken: string): string => `${CHANNEL_PREFIX}${realtimeToken}`;\n\n/**\n * Estados de assinatura do Realtime → \"open\"/\"closed\" da porta.\n * Comparar via lookup (e não `status === \"SUBSCRIBED\"`) porque REALTIME_SUBSCRIBE_STATES é\n * um enum string não reexportado por @supabase/supabase-js; o valor em runtime é a string.\n * Desconhecidos caem em \"closed\" (conservador: o widget mostra \"reconectando\").\n */\nconst STATUS_BY_REALTIME_STATE: Record<string, \"open\" | \"closed\"> = {\n SUBSCRIBED: \"open\",\n CHANNEL_ERROR: \"closed\",\n TIMED_OUT: \"closed\",\n CLOSED: \"closed\",\n};\n\n/** Publica `event` no canal broadcast da sessão. Rejeita se o broadcast não for aceito. */\nexport function createSupabaseTransport(admin: SupabaseClient): RealtimeTransport {\n return {\n async publish(realtimeToken: string, event: ChatEvent): Promise<void> {\n const channel = channelName(realtimeToken);\n const response = await admin.channel(channel).send({\n type: \"broadcast\",\n event: BROADCAST_EVENT,\n payload: event,\n });\n // 'ok' | 'timed out' | 'error' | 'rate limited' | 'channel error' | …\n // Rejeitar em não-'ok' é intencional: o bridge (Task 6) captura e devolve\n // handled:false, acionando a reentrega idempotente do webhook.\n if (response !== \"ok\") {\n throw new Error(`Supabase broadcast \"${channel}\" não entregue: ${response}`);\n }\n },\n };\n}\n\n/**\n * Handle client-side (widget): `subscribe(token, onEvent, onStatus?)` assina\n * `chat:<token>` e devolve o unsubscribe. O client é criado uma única vez na fábrica.\n */\nexport function createSupabaseRealtimeHandle(url: string, anonKey: string): RealtimeHandle {\n const client = createClient(url, anonKey);\n\n return {\n subscribe(realtimeToken, onEvent, onStatus) {\n const channel = client.channel(channelName(realtimeToken));\n\n channel.on(\"broadcast\", { event: BROADCAST_EVENT }, (message: { payload: unknown }) => {\n onEvent(message.payload as ChatEvent);\n });\n\n channel.subscribe((status: string) => {\n onStatus?.(STATUS_BY_REALTIME_STATE[status] ?? \"closed\");\n });\n\n return () => {\n void channel.unsubscribe();\n };\n },\n };\n}\n"],"mappings":";AAiBA,SAAS,oBAAyC;AAIlD,IAAM,iBAAiB;AAEvB,IAAM,kBAAkB;AAExB,IAAM,cAAc,CAAC,kBAAkC,GAAG,cAAc,GAAG,aAAa;AAQxF,IAAM,2BAA8D;AAAA,EAClE,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,WAAW;AAAA,EACX,QAAQ;AACV;AAGO,SAAS,wBAAwB,OAA0C;AAChF,SAAO;AAAA,IACL,MAAM,QAAQ,eAAuB,OAAiC;AACpE,YAAM,UAAU,YAAY,aAAa;AACzC,YAAM,WAAW,MAAM,MAAM,QAAQ,OAAO,EAAE,KAAK;AAAA,QACjD,MAAM;AAAA,QACN,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAID,UAAI,aAAa,MAAM;AACrB,cAAM,IAAI,MAAM,uBAAuB,OAAO,sBAAmB,QAAQ,EAAE;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,6BAA6B,KAAa,SAAiC;AACzF,QAAM,SAAS,aAAa,KAAK,OAAO;AAExC,SAAO;AAAA,IACL,UAAU,eAAe,SAAS,UAAU;AAC1C,YAAM,UAAU,OAAO,QAAQ,YAAY,aAAa,CAAC;AAEzD,cAAQ,GAAG,aAAa,EAAE,OAAO,gBAAgB,GAAG,CAAC,YAAkC;AACrF,gBAAQ,QAAQ,OAAoB;AAAA,MACtC,CAAC;AAED,cAAQ,UAAU,CAAC,WAAmB;AACpC,mBAAW,yBAAyB,MAAM,KAAK,QAAQ;AAAA,MACzD,CAAC;AAED,aAAO,MAAM;AACX,aAAK,QAAQ,YAAY;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|