Files
clube67_newwhats.local/clube67/newwhats.local/plugins/ext-api/backend/webhook-dispatcher.js
T
VPS 4 Deploy Agent 45b7b91b04 fix(plugins): use conditional require for production Docker compatibility
Replace static require('backend/src/...') with NODE_ENV-conditional
requires in compiled plugin JS and TypeScript sources.
In production Docker /app/backend/src/ does not exist — only /app/dist/.
Fixes MODULE_NOT_FOUND errors on UnifiedStorageProvider, ext-api,
leads, nanobana, and uploads plugins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:34:34 +02:00

98 lines
3.6 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildWebhookDispatcher = buildWebhookDispatcher;
/**
* Webhook Dispatcher — envia eventos para URLs registradas na tabela ext_webhooks.
*
* Fluxo:
* hookBus emite ext:message.new ou ext:session.status
* → dispatcher consulta ext_webhooks do tenant
* → para cada webhook ativo que subscreve o evento, faz POST com envelope + assinatura HMAC
*
* Envelope enviado ao receptor:
* { event, data, timestamp }
*
* Header de assinatura:
* x-nw-signature: sha256=<hmac-hex>
* HMAC-SHA256(secret, JSON.stringify(body))
*
* Retry: sem retry automático nesta fase (Fase 5 scope mínimo).
* Timeout: 10s por requisição para não bloquear o event loop.
*/
const crypto_1 = require("crypto");
let rootLogger;
if (process.env.NODE_ENV === 'production') {
rootLogger = require('../../../dist/config/logger').logger;
}
else {
rootLogger = require('../../../backend/src/config/logger').logger;
}
const logger = rootLogger.child({ module: 'webhook-dispatcher' });
const DISPATCH_TIMEOUT_MS = 10000;
// Eventos hookBus → nome de evento no payload enviado ao receptor
const HOOK_TO_EVENT = {
'ext:message.new': 'message.new',
'ext:session.status': 'session.status',
};
function sign(secret, body) {
return 'sha256=' + (0, crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
}
async function dispatch(url, secret, event, data) {
const body = JSON.stringify({ event, data, timestamp: Date.now() });
const sig = sign(secret, body);
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), DISPATCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-nw-signature': sig,
},
body,
signal: ctrl.signal,
});
if (!res.ok) {
logger.warn({ url, event, status: res.status }, '[webhook] Receptor retornou erro');
}
}
catch (err) {
logger.warn({ url, event, err: err.message }, '[webhook] Falha ao entregar');
}
finally {
clearTimeout(timer);
}
}
function buildWebhookDispatcher(prisma, hooks) {
for (const [hookEvent, wsEvent] of Object.entries(HOOK_TO_EVENT)) {
hooks.register(hookEvent, async (raw) => {
const tenantId = raw.tenantId;
if (!tenantId)
return;
// Carregar apenas webhooks ativos que subscrevem este evento
let webhooks;
try {
webhooks = await prisma.extWebhook.findMany({
where: {
tenantId,
active: true,
events: { has: wsEvent },
},
select: { url: true, secret: true },
});
}
catch (err) {
logger.error({ err, tenantId, event: wsEvent }, '[webhook] Erro ao buscar webhooks');
return;
}
if (webhooks.length === 0)
return;
// Retira tenantId do payload antes de enviar ao receptor
const { tenantId: _tid, ...data } = raw;
// Dispara em paralelo sem bloquear o hookBus
Promise.allSettled(webhooks.map(wh => dispatch(wh.url, wh.secret, wsEvent, data))).catch(() => { });
});
}
logger.info('[webhook] Dispatcher ativo para eventos: ' + Object.values(HOOK_TO_EVENT).join(', '));
}
//# sourceMappingURL=webhook-dispatcher.js.map