Files
clube67_newwhats.local/clube67/newwhats.local/plugins/ext-api/backend/webhook-dispatcher.ts
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

111 lines
3.4 KiB
TypeScript

/**
* 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.
*/
import { createHmac } from 'crypto'
import type { PrismaClient } from '@prisma/client'
import type { HookBus } from '../../../backend/src/core/hook-bus'
let rootLogger: any
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 = 10_000
// Eventos hookBus → nome de evento no payload enviado ao receptor
const HOOK_TO_EVENT: Record<string, string> = {
'ext:message.new': 'message.new',
'ext:session.status': 'session.status',
}
function sign(secret: string, body: string): string {
return 'sha256=' + createHmac('sha256', secret).update(body).digest('hex')
}
async function dispatch(
url: string,
secret: string,
event: string,
data: Record<string, unknown>,
): Promise<void> {
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: any) {
logger.warn({ url, event, err: err.message }, '[webhook] Falha ao entregar')
} finally {
clearTimeout(timer)
}
}
export function buildWebhookDispatcher(prisma: PrismaClient, hooks: HookBus): void {
for (const [hookEvent, wsEvent] of Object.entries(HOOK_TO_EVENT)) {
hooks.register(hookEvent, async (raw: any) => {
const tenantId: string | undefined = raw.tenantId
if (!tenantId) return
// Carregar apenas webhooks ativos que subscrevem este evento
let webhooks: Array<{ url: string; secret: string }>
try {
webhooks = await prisma.extWebhook.findMany({
where: {
tenantId,
active: true,
events: { has: wsEvent },
},
select: { url: true, secret: true },
})
} catch (err: any) {
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(', '))
}