feat(ext-api): chave mestra + x-nw-email (middleware REST + WS bridge)

Satélite confiável acessa a conta de qualquer email via EXT_MASTER_KEY:
- apikey-auth: helpers isMasterKey/resolveMasterEmail resolvem o tenant pelo
  User.email; middleware REST refatorado para usá-los.
- ws-bridge: handshake do /api/ext/v1/stream trata a chave mestra + x-nw-email
  (antes só tabela apiKey → o túnel WS tomava 401).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-01 20:55:10 +02:00
parent 7c54309aa8
commit dbb4da8220
8 changed files with 127 additions and 10 deletions
@@ -1,6 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildApiKeyAuth = buildApiKeyAuth;
exports.isMasterKey = isMasterKey;
exports.resolveMasterEmail = resolveMasterEmail;
exports.resolveApiKey = resolveApiKey;
// ─── Cache de API key (evita Prisma query em cada requisição) ────────────────
// Chaves são imutáveis durante a vida da sessão — TTL de 5 min é suficiente.
@@ -48,6 +50,25 @@ function buildApiKeyAuth(prisma) {
res.status(429).json({ error: 'Rate limit excedido (120 req/min por chave)' });
return;
}
// ── Chave MESTRA + x-nw-email: integração multi-conta por email ────────────
// Permite que um satélite confiável acesse a conta de QUALQUER email (resolve
// o tenant pelo User.email). O satélite deve enviar SEMPRE o email do usuário
// autenticado dele — nunca um email arbitrário do cliente.
if (isMasterKey(key)) {
const email = req.headers['x-nw-email']?.trim().toLowerCase();
if (!email) {
res.status(400).json({ error: 'x-nw-email ausente (chave mestra)' });
return;
}
const tenantId = await resolveMasterEmail(prisma, email);
if (!tenantId) {
res.status(404).json({ error: `Conta não encontrada no motor para ${email}` });
return;
}
req.extTenantId = tenantId;
next();
return;
}
// Verifica cache antes de ir ao banco
const cached = keyCache.get(key);
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
@@ -76,6 +97,34 @@ function buildApiKeyAuth(prisma) {
next();
};
}
/**
* 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
* resolveMasterEmail — nunca pela tabela apiKey.
*/
function isMasterKey(key) {
const MASTER = process.env.EXT_MASTER_KEY;
return Boolean(MASTER && key === MASTER);
}
/**
* Resolve o tenant a partir do email (fluxo da chave mestra). Compartilhado
* entre o middleware REST e o handshake do WS. Retorna null se o email não
* tiver conta ativa no motor. Usa o mesmo cache (prefixo master:).
*/
async function resolveMasterEmail(prisma, email) {
const e = email?.trim().toLowerCase();
if (!e)
return null;
const cacheK = `master:${e}`;
const c = keyCache.get(cacheK);
if (c && Date.now() - c.cachedAt < KEY_CACHE_TTL)
return c.tenantId;
const user = await prisma.user.findUnique({ where: { email: e }, select: { id: true, isActive: true } });
if (!user || !user.isActive)
return null;
keyCache.set(cacheK, { tenantId: user.id, expiresAt: null, cachedAt: Date.now() });
return user.id;
}
/**
* Versão standalone que valida a chave sem passar pelo Express.
* Usada no handshake do WS (upgrade request).
File diff suppressed because one or more lines are too long
@@ -75,6 +75,19 @@ export function buildApiKeyAuth(prisma: PrismaClient) {
return
}
// ── Chave MESTRA + x-nw-email: integração multi-conta por email ────────────
// Permite que um satélite confiável acesse a conta de QUALQUER email (resolve
// o tenant pelo User.email). O satélite deve enviar SEMPRE o email do usuário
// autenticado dele — nunca um email arbitrário do cliente.
if (isMasterKey(key)) {
const email = (req.headers['x-nw-email'] as string | undefined)?.trim().toLowerCase()
if (!email) { res.status(400).json({ error: 'x-nw-email ausente (chave mestra)' }); return }
const tenantId = await resolveMasterEmail(prisma, email)
if (!tenantId) { res.status(404).json({ error: `Conta não encontrada no motor para ${email}` }); return }
req.extTenantId = tenantId
next(); return
}
// Verifica cache antes de ir ao banco
const cached = keyCache.get(key)
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
@@ -108,6 +121,36 @@ export function buildApiKeyAuth(prisma: PrismaClient) {
}
}
/**
* 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
* resolveMasterEmail — nunca pela tabela apiKey.
*/
export function isMasterKey(key: string): boolean {
const MASTER = process.env.EXT_MASTER_KEY
return Boolean(MASTER && key === MASTER)
}
/**
* Resolve o tenant a partir do email (fluxo da chave mestra). Compartilhado
* entre o middleware REST e o handshake do WS. Retorna null se o email não
* tiver conta ativa no motor. Usa o mesmo cache (prefixo master:).
*/
export async function resolveMasterEmail(
prisma: PrismaClient,
email: string | undefined | null,
): Promise<string | null> {
const e = email?.trim().toLowerCase()
if (!e) return null
const cacheK = `master:${e}`
const c = keyCache.get(cacheK)
if (c && Date.now() - c.cachedAt < KEY_CACHE_TTL) return c.tenantId
const user = await prisma.user.findUnique({ where: { email: e }, select: { id: true, isActive: true } })
if (!user || !user.isActive) return null
keyCache.set(cacheK, { tenantId: user.id, expiresAt: null, cachedAt: Date.now() })
return user.id
}
/**
* Versão standalone que valida a chave sem passar pelo Express.
* Usada no handshake do WS (upgrade request).
@@ -43,7 +43,20 @@ function buildWsBridge(httpServer, prisma, hooks) {
}
let tenantId;
try {
tenantId = await (0, apikey_auth_1.resolveApiKey)(prisma, apiKey);
// Chave mestra → resolve o tenant pelo x-nw-email do handshake (o túnel do
// satélite injeta o email do usuário autenticado). Chave normal → tabela apiKey.
if ((0, apikey_auth_1.isMasterKey)(apiKey)) {
const email = req.headers['x-nw-email']?.trim().toLowerCase();
if (!email) {
socket.write('HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nx-nw-email ausente (chave mestra)');
socket.destroy();
return;
}
tenantId = await (0, apikey_auth_1.resolveMasterEmail)(prisma, email);
}
else {
tenantId = await (0, apikey_auth_1.resolveApiKey)(prisma, apiKey);
}
}
catch {
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
@@ -51,7 +64,7 @@ function buildWsBridge(httpServer, prisma, hooks) {
return;
}
if (!tenantId) {
socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\n\r\nChave inválida, inativa ou expirada');
socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\n\r\nChave inválida/inativa ou conta não encontrada');
socket.destroy();
return;
}
@@ -1 +1 @@
{"version":3,"file":"ws-bridge.js","sourceRoot":"","sources":["ws-bridge.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"ws-bridge.js","sourceRoot":"","sources":["ws-bridge.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,7 @@ import type { Server as HttpServer } from 'http'
import type { IncomingMessage } from 'http'
import type { PrismaClient } from '@prisma/client'
import type { HookBus } from '../../../backend/src/core/hook-bus'
import { resolveApiKey } from './apikey-auth'
import { resolveApiKey, isMasterKey, resolveMasterEmail } from './apikey-auth'
const WS_PATH = '/api/ext/v1/stream'
const PING_INTERVAL_MS = 25_000
@@ -59,7 +59,19 @@ export function buildWsBridge(
let tenantId: string | null
try {
tenantId = await resolveApiKey(prisma, apiKey)
// Chave mestra → resolve o tenant pelo x-nw-email do handshake (o túnel do
// satélite injeta o email do usuário autenticado). Chave normal → tabela apiKey.
if (isMasterKey(apiKey)) {
const email = (req.headers['x-nw-email'] as string | undefined)?.trim().toLowerCase()
if (!email) {
socket.write('HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nx-nw-email ausente (chave mestra)')
socket.destroy()
return
}
tenantId = await resolveMasterEmail(prisma, email)
} else {
tenantId = await resolveApiKey(prisma, apiKey)
}
} catch {
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n')
socket.destroy()
@@ -67,7 +79,7 @@ export function buildWsBridge(
}
if (!tenantId) {
socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\n\r\nChave inválida, inativa ou expirada')
socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\n\r\nChave inválida/inativa ou conta não encontrada')
socket.destroy()
return
}
@@ -11,14 +11,14 @@ const manifest_json_1 = __importDefault(require("./manifest.json"));
const plugin = {
manifest: manifest_json_1.default,
async activate(ctx) {
const { app, prisma, db, config, hooks, logger, httpServer } = ctx;
const { app, prisma, db, config, hooks, logger, httpServer, io } = ctx;
// ── REST ────────────────────────────────────────────────────────────────
const manager = globalThis.__whatsAppManager;
if (!manager) {
logger.warn('[ext-api] WhatsAppConnectionManager não disponível via globalThis.__whatsAppManager — endpoints de send desativados');
}
const authMiddleware = (0, apikey_auth_1.buildApiKeyAuth)(prisma);
const extRouter = (0, routes_1.buildExtRoutes)(prisma, manager, db, config, hooks);
const extRouter = (0, routes_1.buildExtRoutes)(prisma, manager, db, config, hooks, io);
app.use('/api/ext/v1', authMiddleware, extRouter);
logger.info('[ext-api] Rotas REST registradas em /api/ext/v1');
// ── 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,GAAG,GAAG,CAAA;QAElE,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,CAAC,CAAA;QAEtG,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;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"}