feat(ext-api): gerenciador de satélites — auth por integration_key + gestão

Torna o registry de satélites (PluginPair) o gatekeeper do ext-api, em vez de
uma única EXT_MASTER_KEY global:
- satellites.ts: serviço + router /api/ext/v1/satellites (listar/provisionar/
  revogar/rotacionar), montado fora do authMiddleware com gate de chave mestra.
- apikey-auth: resolvePairKey aceita a chave do satélite (nwk_) no REST e no WS,
  resolvendo o tenant do par; checa revoke no banco a cada request (revoke
  instantâneo) + throttle de lastSeenAt.
- Chave mestra e ApiKey por-tenant seguem funcionando (não-quebra).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-02 03:29:43 +02:00
parent c18b956a56
commit 89758a0e6b
9 changed files with 357 additions and 2 deletions
@@ -1,6 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.buildApiKeyAuth = buildApiKeyAuth; exports.buildApiKeyAuth = buildApiKeyAuth;
exports.resolvePairKey = resolvePairKey;
exports.isMasterKey = isMasterKey; exports.isMasterKey = isMasterKey;
exports.resolveMasterEmail = resolveMasterEmail; exports.resolveMasterEmail = resolveMasterEmail;
exports.resolveApiKey = resolveApiKey; exports.resolveApiKey = resolveApiKey;
@@ -69,6 +70,20 @@ function buildApiKeyAuth(prisma) {
next(); next();
return; return;
} }
// ── Chave de SATÉLITE (PluginPair) ─────────────────────────────────────────
// Cada satélite registrado tem sua própria integration_key (nwk_...). Resolve
// o tenant dono do par; honra revoke e atualiza lastSeenAt. É o registry — e
// não uma única chave mestra global — autenticando o satélite.
if (key.startsWith('nwk_')) {
const tenantId = await resolvePairKey(prisma, key);
if (!tenantId) {
res.status(401).json({ error: 'Chave de satélite inválida ou revogada' });
return;
}
req.extTenantId = tenantId;
next();
return;
}
// Verifica cache antes de ir ao banco // Verifica cache antes de ir ao banco
const cached = keyCache.get(key); const cached = keyCache.get(key);
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
@@ -97,6 +112,30 @@ function buildApiKeyAuth(prisma) {
next(); next();
}; };
} }
/**
* Resolve uma chave de SATÉLITE (PluginPair, nwk_...) para o tenantId dono do par.
* Retorna null se não existir, estiver revogada ou a conta inativa. Atualiza
* lastSeenAt em background e usa o mesmo cache das demais chaves.
*/
// Throttle das escritas de lastSeenAt (não cacheia a DECISÃO de auth — a checagem
// de revoke bate no banco a cada request, garantindo revoke instantâneo).
const pairTouch = new Map();
async function resolvePairKey(prisma, key) {
if (!key || !key.startsWith('nwk_'))
return null;
const pair = await prisma.pluginPair.findUnique({
where: { integrationKey: key },
select: { userId: true, revokedAt: true, user: { select: { isActive: true } } },
});
if (!pair || pair.revokedAt || !pair.user.isActive)
return null;
const now = Date.now();
if (now - (pairTouch.get(key) ?? 0) > 60000) {
pairTouch.set(key, now);
prisma.pluginPair.update({ where: { integrationKey: key }, data: { lastSeenAt: new Date() } }).catch(() => { });
}
return pair.userId;
}
/** /**
* Indica se a chave recebida é a chave MESTRA de integração (EXT_MASTER_KEY). * Indica se a chave recebida é a chave MESTRA de integração (EXT_MASTER_KEY).
* A resolução do tenant, nesse caso, é feita por email (x-nw-email) — ver * A resolução do tenant, nesse caso, é feita por email (x-nw-email) — ver
@@ -132,6 +171,9 @@ async function resolveMasterEmail(prisma, email) {
async function resolveApiKey(prisma, key) { async function resolveApiKey(prisma, key) {
if (!key) if (!key)
return null; return null;
// Chave de satélite (PluginPair) — resolve pelo registry.
if (key.startsWith('nwk_'))
return resolvePairKey(prisma, key);
const cached = keyCache.get(key); const cached = keyCache.get(key);
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
if (cached.expiresAt && cached.expiresAt < new Date()) if (cached.expiresAt && cached.expiresAt < new Date())
File diff suppressed because one or more lines are too long
@@ -88,6 +88,17 @@ export function buildApiKeyAuth(prisma: PrismaClient) {
next(); return next(); return
} }
// ── Chave de SATÉLITE (PluginPair) ─────────────────────────────────────────
// Cada satélite registrado tem sua própria integration_key (nwk_...). Resolve
// o tenant dono do par; honra revoke e atualiza lastSeenAt. É o registry — e
// não uma única chave mestra global — autenticando o satélite.
if (key.startsWith('nwk_')) {
const tenantId = await resolvePairKey(prisma, key)
if (!tenantId) { res.status(401).json({ error: 'Chave de satélite inválida ou revogada' }); return }
req.extTenantId = tenantId
next(); return
}
// Verifica cache antes de ir ao banco // Verifica cache antes de ir ao banco
const cached = keyCache.get(key) const cached = keyCache.get(key)
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
@@ -121,6 +132,29 @@ export function buildApiKeyAuth(prisma: PrismaClient) {
} }
} }
/**
* Resolve uma chave de SATÉLITE (PluginPair, nwk_...) para o tenantId dono do par.
* Retorna null se não existir, estiver revogada ou a conta inativa. Atualiza
* lastSeenAt em background e usa o mesmo cache das demais chaves.
*/
// Throttle das escritas de lastSeenAt (não cacheia a DECISÃO de auth — a checagem
// de revoke bate no banco a cada request, garantindo revoke instantâneo).
const pairTouch = new Map<string, number>()
export async function resolvePairKey(prisma: PrismaClient, key: string): Promise<string | null> {
if (!key || !key.startsWith('nwk_')) return null
const pair = await prisma.pluginPair.findUnique({
where: { integrationKey: key },
select: { userId: true, revokedAt: true, user: { select: { isActive: true } } },
})
if (!pair || pair.revokedAt || !pair.user.isActive) return null
const now = Date.now()
if (now - (pairTouch.get(key) ?? 0) > 60_000) {
pairTouch.set(key, now)
prisma.pluginPair.update({ where: { integrationKey: key }, data: { lastSeenAt: new Date() } }).catch(() => {})
}
return pair.userId
}
/** /**
* Indica se a chave recebida é a chave MESTRA de integração (EXT_MASTER_KEY). * Indica se a chave recebida é a chave MESTRA de integração (EXT_MASTER_KEY).
* A resolução do tenant, nesse caso, é feita por email (x-nw-email) — ver * A resolução do tenant, nesse caso, é feita por email (x-nw-email) — ver
@@ -160,6 +194,8 @@ export async function resolveApiKey(
key: string, key: string,
): Promise<string | null> { ): Promise<string | null> {
if (!key) return null if (!key) return null
// Chave de satélite (PluginPair) — resolve pelo registry.
if (key.startsWith('nwk_')) return resolvePairKey(prisma, key)
const cached = keyCache.get(key) const cached = keyCache.get(key)
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
if (cached.expiresAt && cached.expiresAt < new Date()) return null if (cached.expiresAt && cached.expiresAt < new Date()) return null
@@ -0,0 +1,133 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.provisionSatellite = provisionSatellite;
exports.listSatellites = listSatellites;
exports.revokeSatellite = revokeSatellite;
exports.rotateSatelliteKey = rotateSatelliteKey;
exports.buildSatelliteRoutes = buildSatelliteRoutes;
const express_1 = require("express");
const crypto_1 = __importDefault(require("crypto"));
const apikey_auth_1 = require("./apikey-auth");
const genKey = () => `nwk_${crypto_1.default.randomUUID().replace(/-/g, '')}`;
// A resolução da chave de satélite para o auth vive em apikey-auth.resolvePairKey
// (co-localizada com os demais resolvers + cache). Aqui ficam só as operações de gestão.
// ─── Operações de gestão ──────────────────────────────────────────────────────
async function provisionSatellite(prisma, input) {
const email = String(input.email ?? '').toLowerCase().trim();
if (!email || !input.clientSystem || !input.clientName) {
throw Object.assign(new Error('email, clientSystem e clientName obrigatórios'), { statusCode: 400 });
}
const user = await prisma.user.findUnique({
where: { email }, select: { id: true, isActive: true, name: true, email: true },
});
if (!user || !user.isActive)
throw Object.assign(new Error(`Conta não encontrada/inativa: ${email}`), { statusCode: 404 });
// Revoga par anterior do mesmo sistema (evita duplicatas ativas).
await prisma.pluginPair.updateMany({
where: { userId: user.id, clientSystem: input.clientSystem, revokedAt: null },
data: { revokedAt: new Date() },
});
const integrationKey = genKey();
const pair = await prisma.pluginPair.create({
data: {
userId: user.id,
clientSystem: input.clientSystem,
clientName: input.clientName,
clientUrl: input.clientUrl || null,
integrationKey,
},
});
return {
id: pair.id,
integration_key: integrationKey,
account: { name: user.name, email: user.email },
clientSystem: input.clientSystem,
clientName: input.clientName,
};
}
async function listSatellites(prisma) {
const pairs = await prisma.pluginPair.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true, clientSystem: true, clientName: true, clientUrl: true,
lastSeenAt: true, revokedAt: true, createdAt: true,
user: { select: { email: true, isActive: true } },
},
});
return pairs.map((p) => ({
id: p.id,
clientSystem: p.clientSystem,
clientName: p.clientName,
clientUrl: p.clientUrl,
account: p.user.email,
active: !p.revokedAt && p.user.isActive,
revokedAt: p.revokedAt,
lastSeenAt: p.lastSeenAt,
createdAt: p.createdAt,
}));
}
async function revokeSatellite(prisma, id) {
const r = await prisma.pluginPair.updateMany({ where: { id, revokedAt: null }, data: { revokedAt: new Date() } });
if (!r.count)
throw Object.assign(new Error('Satélite não encontrado ou já revogado'), { statusCode: 404 });
return { ok: true };
}
async function rotateSatelliteKey(prisma, id) {
const pair = await prisma.pluginPair.findFirst({ where: { id, revokedAt: null }, select: { id: true } });
if (!pair)
throw Object.assign(new Error('Satélite não encontrado'), { statusCode: 404 });
const integrationKey = genKey();
await prisma.pluginPair.update({ where: { id }, data: { integrationKey } });
return { id, integration_key: integrationKey };
}
// ─── Router de gestão (superadmin via chave mestra) ───────────────────────────
// Montado FORA do authMiddleware do ext-api: tem gate próprio (chave mestra,
// sem exigir x-nw-email como o fluxo de operação).
function buildSatelliteRoutes(prisma) {
const router = (0, express_1.Router)();
router.use((req, res, next) => {
const key = req.headers['x-nw-key']?.trim() ?? '';
if (!(0, apikey_auth_1.isMasterKey)(key)) {
res.status(403).json({ error: 'Apenas a chave mestra gerencia satélites.' });
return;
}
next();
});
router.get('/', async (_req, res) => {
try {
res.json({ satellites: await listSatellites(prisma) });
}
catch (e) {
res.status(500).json({ error: e.message });
}
});
router.post('/', async (req, res) => {
try {
res.json(await provisionSatellite(prisma, req.body ?? {}));
}
catch (e) {
res.status(e.statusCode ?? 500).json({ error: e.message });
}
});
router.post('/:id/revoke', async (req, res) => {
try {
res.json(await revokeSatellite(prisma, req.params['id']));
}
catch (e) {
res.status(e.statusCode ?? 500).json({ error: e.message });
}
});
router.post('/:id/rotate', async (req, res) => {
try {
res.json(await rotateSatelliteKey(prisma, req.params['id']));
}
catch (e) {
res.status(e.statusCode ?? 500).json({ error: e.message });
}
});
return router;
}
//# sourceMappingURL=satellites.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,132 @@
/**
* Gerenciador de plugins/satélites do motor.
*
* Um "satélite" é um sistema cliente (ex.: scoreodonto) registrado como PluginPair,
* com integration_key própria (nwk_...), clientUrl e clientSystem. Este módulo:
* (a) resolve a chave do satélite → tenant no auth do ext-api (honra revoke,
* atualiza lastSeenAt) — tornando o REGISTRY o gatekeeper, em vez de uma
* única EXT_MASTER_KEY global;
* (b) expõe gestão (listar / provisionar / revogar / rotacionar) para superadmin
* (autenticado pela chave mestra).
*
* A chave mestra (EXT_MASTER_KEY) permanece para bootstrap/superadmin.
*/
import type { PrismaClient } from '@prisma/client'
import { Router, type Request, type Response } from 'express'
import crypto from 'crypto'
import { isMasterKey } from './apikey-auth'
const genKey = () => `nwk_${crypto.randomUUID().replace(/-/g, '')}`
// A resolução da chave de satélite para o auth vive em apikey-auth.resolvePairKey
// (co-localizada com os demais resolvers + cache). Aqui ficam só as operações de gestão.
// ─── Operações de gestão ──────────────────────────────────────────────────────
export async function provisionSatellite(
prisma: PrismaClient,
input: { email: string; clientSystem: string; clientName: string; clientUrl?: string | null },
) {
const email = String(input.email ?? '').toLowerCase().trim()
if (!email || !input.clientSystem || !input.clientName) {
throw Object.assign(new Error('email, clientSystem e clientName obrigatórios'), { statusCode: 400 })
}
const user = await prisma.user.findUnique({
where: { email }, select: { id: true, isActive: true, name: true, email: true },
})
if (!user || !user.isActive) throw Object.assign(new Error(`Conta não encontrada/inativa: ${email}`), { statusCode: 404 })
// Revoga par anterior do mesmo sistema (evita duplicatas ativas).
await prisma.pluginPair.updateMany({
where: { userId: user.id, clientSystem: input.clientSystem, revokedAt: null },
data: { revokedAt: new Date() },
})
const integrationKey = genKey()
const pair = await prisma.pluginPair.create({
data: {
userId: user.id,
clientSystem: input.clientSystem,
clientName: input.clientName,
clientUrl: input.clientUrl || null,
integrationKey,
},
})
return {
id: pair.id,
integration_key: integrationKey,
account: { name: user.name, email: user.email },
clientSystem: input.clientSystem,
clientName: input.clientName,
}
}
export async function listSatellites(prisma: PrismaClient) {
const pairs = await prisma.pluginPair.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true, clientSystem: true, clientName: true, clientUrl: true,
lastSeenAt: true, revokedAt: true, createdAt: true,
user: { select: { email: true, isActive: true } },
},
})
return pairs.map((p) => ({
id: p.id,
clientSystem: p.clientSystem,
clientName: p.clientName,
clientUrl: p.clientUrl,
account: p.user.email,
active: !p.revokedAt && p.user.isActive,
revokedAt: p.revokedAt,
lastSeenAt: p.lastSeenAt,
createdAt: p.createdAt,
}))
}
export async function revokeSatellite(prisma: PrismaClient, id: string) {
const r = await prisma.pluginPair.updateMany({ where: { id, revokedAt: null }, data: { revokedAt: new Date() } })
if (!r.count) throw Object.assign(new Error('Satélite não encontrado ou já revogado'), { statusCode: 404 })
return { ok: true }
}
export async function rotateSatelliteKey(prisma: PrismaClient, id: string) {
const pair = await prisma.pluginPair.findFirst({ where: { id, revokedAt: null }, select: { id: true } })
if (!pair) throw Object.assign(new Error('Satélite não encontrado'), { statusCode: 404 })
const integrationKey = genKey()
await prisma.pluginPair.update({ where: { id }, data: { integrationKey } })
return { id, integration_key: integrationKey }
}
// ─── Router de gestão (superadmin via chave mestra) ───────────────────────────
// Montado FORA do authMiddleware do ext-api: tem gate próprio (chave mestra,
// sem exigir x-nw-email como o fluxo de operação).
export function buildSatelliteRoutes(prisma: PrismaClient): Router {
const router = Router()
router.use((req: Request, res: Response, next) => {
const key = (req.headers['x-nw-key'] as string | undefined)?.trim() ?? ''
if (!isMasterKey(key)) { res.status(403).json({ error: 'Apenas a chave mestra gerencia satélites.' }); return }
next()
})
router.get('/', async (_req, res) => {
try { res.json({ satellites: await listSatellites(prisma) }) }
catch (e: any) { res.status(500).json({ error: e.message }) }
})
router.post('/', async (req, res) => {
try { res.json(await provisionSatellite(prisma, req.body ?? {})) }
catch (e: any) { res.status(e.statusCode ?? 500).json({ error: e.message }) }
})
router.post('/:id/revoke', async (req, res) => {
try { res.json(await revokeSatellite(prisma, req.params['id']!)) }
catch (e: any) { res.status(e.statusCode ?? 500).json({ error: e.message }) }
})
router.post('/:id/rotate', async (req, res) => {
try { res.json(await rotateSatelliteKey(prisma, req.params['id']!)) }
catch (e: any) { res.status(e.statusCode ?? 500).json({ error: e.message }) }
})
return router
}
@@ -7,6 +7,7 @@ const apikey_auth_1 = require("./backend/apikey-auth");
const routes_1 = require("./backend/routes"); const routes_1 = require("./backend/routes");
const ws_bridge_1 = require("./backend/ws-bridge"); const ws_bridge_1 = require("./backend/ws-bridge");
const webhook_dispatcher_1 = require("./backend/webhook-dispatcher"); const webhook_dispatcher_1 = require("./backend/webhook-dispatcher");
const satellites_1 = require("./backend/satellites");
const manifest_json_1 = __importDefault(require("./manifest.json")); const manifest_json_1 = __importDefault(require("./manifest.json"));
const plugin = { const plugin = {
manifest: manifest_json_1.default, manifest: manifest_json_1.default,
@@ -19,6 +20,10 @@ const plugin = {
} }
const authMiddleware = (0, apikey_auth_1.buildApiKeyAuth)(prisma); const authMiddleware = (0, apikey_auth_1.buildApiKeyAuth)(prisma);
const extRouter = (0, routes_1.buildExtRoutes)(prisma, manager, db, config, hooks, io); const extRouter = (0, routes_1.buildExtRoutes)(prisma, manager, db, config, hooks, io);
// Gerenciador de satélites — montado ANTES do authMiddleware (gate próprio
// de chave mestra, sem exigir x-nw-email). Superadmin gerencia os PluginPairs.
app.use('/api/ext/v1/satellites', (0, satellites_1.buildSatelliteRoutes)(prisma));
logger.info('[ext-api] Gerenciador de satélites em /api/ext/v1/satellites (chave mestra)');
app.use('/api/ext/v1', authMiddleware, extRouter); app.use('/api/ext/v1', authMiddleware, extRouter);
logger.info('[ext-api] Rotas REST registradas em /api/ext/v1'); logger.info('[ext-api] Rotas REST registradas em /api/ext/v1');
// ── WS Bridge ─────────────────────────────────────────────────────────── // ── WS Bridge ───────────────────────────────────────────────────────────
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;AAKA,uDAAuD;AACvD,6CAAiD;AACjD,mDAAmD;AACnD,qEAAqE;AAErE,oEAAsC;AAWtC,MAAM,MAAM,GAAmB;IAC7B,QAAQ,EAAE,uBAAe;IAEzB,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC/B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;QAEtE,2EAA2E;QAC3E,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAA;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,qHAAqH,CAAC,CAAA;QACpI,CAAC;QAED,MAAM,cAAc,GAAG,IAAA,6BAAe,EAAC,MAAM,CAAC,CAAA;QAC9C,MAAM,SAAS,GAAQ,IAAA,uBAAc,EAAC,MAAM,EAAE,OAAoC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAE1G,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,cAAc,EAAE,SAAS,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAA;QAE9D,2EAA2E;QAC3E,IAAA,yBAAa,EAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAA;QAE9E,2EAA2E;QAC3E,IAAA,2CAAsB,EAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QACrC,MAAM,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAA;IACnD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAkB;QACjC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAA;IACrF,CAAC;CACF,CAAA;AAED,kBAAe,MAAM,CAAA"} {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;AAKA,uDAAuD;AACvD,6CAAiD;AACjD,mDAAmD;AACnD,qEAAqE;AACrE,qDAA2D;AAE3D,oEAAsC;AAWtC,MAAM,MAAM,GAAmB;IAC7B,QAAQ,EAAE,uBAAe;IAEzB,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC/B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;QAEtE,2EAA2E;QAC3E,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAA;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,qHAAqH,CAAC,CAAA;QACpI,CAAC;QAED,MAAM,cAAc,GAAG,IAAA,6BAAe,EAAC,MAAM,CAAC,CAAA;QAC9C,MAAM,SAAS,GAAQ,IAAA,uBAAc,EAAC,MAAM,EAAE,OAAoC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAE1G,2EAA2E;QAC3E,+EAA+E;QAC/E,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAA,iCAAoB,EAAC,MAAM,CAAC,CAAC,CAAA;QAC/D,MAAM,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAA;QAE1F,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,cAAc,EAAE,SAAS,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAA;QAE9D,2EAA2E;QAC3E,IAAA,yBAAa,EAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAA;QAE9E,2EAA2E;QAC3E,IAAA,2CAAsB,EAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QACrC,MAAM,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAA;IACnD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAkB;QACjC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAA;IACrF,CAAC;CACF,CAAA;AAED,kBAAe,MAAM,CAAA"}
@@ -7,6 +7,7 @@ import { buildApiKeyAuth } from './backend/apikey-auth'
import { buildExtRoutes } from './backend/routes' import { buildExtRoutes } from './backend/routes'
import { buildWsBridge } from './backend/ws-bridge' import { buildWsBridge } from './backend/ws-bridge'
import { buildWebhookDispatcher } from './backend/webhook-dispatcher' import { buildWebhookDispatcher } from './backend/webhook-dispatcher'
import { buildSatelliteRoutes } from './backend/satellites'
import { WhatsAppConnectionManager } from '../../backend/src/modules/whatsapp/connection/WhatsAppConnectionManager' import { WhatsAppConnectionManager } from '../../backend/src/modules/whatsapp/connection/WhatsAppConnectionManager'
import manifest from './manifest.json' import manifest from './manifest.json'
@@ -34,6 +35,11 @@ const plugin: PluginInstance = {
const authMiddleware = buildApiKeyAuth(prisma) const authMiddleware = buildApiKeyAuth(prisma)
const extRouter = buildExtRoutes(prisma, manager as WhatsAppConnectionManager, db, config, hooks, io) const extRouter = buildExtRoutes(prisma, manager as WhatsAppConnectionManager, db, config, hooks, io)
// Gerenciador de satélites — montado ANTES do authMiddleware (gate próprio
// de chave mestra, sem exigir x-nw-email). Superadmin gerencia os PluginPairs.
app.use('/api/ext/v1/satellites', buildSatelliteRoutes(prisma))
logger.info('[ext-api] Gerenciador de satélites em /api/ext/v1/satellites (chave mestra)')
app.use('/api/ext/v1', authMiddleware, extRouter) app.use('/api/ext/v1', authMiddleware, extRouter)
logger.info('[ext-api] Rotas REST registradas em /api/ext/v1') logger.info('[ext-api] Rotas REST registradas em /api/ext/v1')