Files
clube67_newwhats.local/newwhats.clube67.com/newwhats.local/plugins/ext-api/backend/satellites.js
T
VPS 4 Deploy Agent 89758a0e6b 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>
2026-07-02 03:29:43 +02:00

133 lines
5.5 KiB
JavaScript

"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