feat(ext-api): chave de satélite e-mail-capable (REST + WS)

A integration_key do satélite (nwk_) passa a resolver por x-nw-email quando
presente (preserva multi-conta: cada usuário vê o WhatsApp do seu e-mail),
caindo no tenant dono do par quando ausente — igual no middleware REST e no
handshake do WS bridge. Valida o par (revoke) antes de resolver por email.
Permite migrar o scoreodonto da EXT_MASTER_KEY global p/ chave própria
(revogável/auditada), sem mudança de comportamento.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-07-02 03:56:25 +02:00
parent 89758a0e6b
commit 35188b4f9a
6 changed files with 52 additions and 15 deletions
@@ -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, isMasterKey, resolveMasterEmail } from './apikey-auth'
import { resolveApiKey, isMasterKey, resolveMasterEmail, resolvePairKey } from './apikey-auth'
const WS_PATH = '/api/ext/v1/stream'
const PING_INTERVAL_MS = 25_000
@@ -59,16 +59,22 @@ export function buildWsBridge(
let tenantId: string | null
try {
// 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.
// Resolução do tenant no handshake (o túnel do satélite injeta x-nw-email):
// - chave mestra exige x-nw-email → resolve por email;
// - chave de satélite (nwk_) → confiável: com x-nw-email resolve por email
// (preserva multi-conta), senão cai no tenant dono do par;
// - chave normal → tabela apiKey.
const email = (req.headers['x-nw-email'] as string | undefined)?.trim().toLowerCase()
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 if (apiKey.startsWith('nwk_')) {
const owner = await resolvePairKey(prisma, apiKey)
tenantId = owner ? (email ? await resolveMasterEmail(prisma, email) : owner) : null
} else {
tenantId = await resolveApiKey(prisma, apiKey)
}