// Proxy REST: /api/nw/v1/* (frontend) → motor /api/ext/v1/*. // // Segurança: valida o JWT do scoreodonto (só usuário logado acessa). // Resolução de conta: SEMPRE via x-nw-email (chave mestra resolve o tenant pelo // email). Todo usuário da clínica tem conta própria no motor — se o motor não // conhecer o email, é erro (404 "conta não encontrada"), não há fallback. // O email vem SEMPRE do token verificado — nunca do cliente. import net from 'node:net'; import tls from 'node:tls'; import jwt from 'jsonwebtoken'; import { getConfig } from './config.js'; function emailFromReq(req) { try { const tk = (req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim(); if (!tk) return null; const p = jwt.verify(tk, process.env.JWT_SECRET); return (p.email || '').toLowerCase() || null; } catch { return null; } } export function createRestProxy() { return async function restProxy(req, res) { const cfg = getConfig(); if (!cfg.motorUrl || !cfg.integrationKey) { return res.status(503).json({ error: 'Integração NewWhats não configurada.' }); } const email = emailFromReq(req); if (!email) return res.status(401).json({ error: 'Sessão inválida (faça login no scoreodonto).' }); const tail = req.originalUrl.replace(/^\/api\/nw\/v1/, ''); const url = `${cfg.motorUrl}/api/ext/v1${tail}`; const opts = { method: req.method, headers: { 'Content-Type': 'application/json', 'x-nw-key': cfg.integrationKey, 'x-nw-email': email, } }; if (!['GET', 'HEAD'].includes(req.method) && req.body && Object.keys(req.body).length) { opts.body = JSON.stringify(req.body); } try { const r = await fetch(url, opts); const text = await r.text(); res.status(r.status); try { res.json(JSON.parse(text)); } catch { res.send(text); } } catch (e) { res.status(502).json({ error: `Motor indisponível: ${e.message}` }); } }; } // ── Túnel WebSocket: /api/nw/v1/stream (browser) → motor /api/ext/v1/stream ── // // O inbox em tempo real do motor (QR, status da sessão, mensagens novas) roda // sobre WebSocket. Aqui abrimos um túnel TCP bruto socket↔socket e injetamos a // x-nw-key no handshake com o motor — a chave nunca chega ao browser. // // Auth do usuário: o browser NÃO envia header Authorization no upgrade WS, então // o JWT do scoreodonto vem por ?token= (mesmo esquema do /api/ws do app). Só // usuário logado abre o túnel; o token fica local — não é repassado ao motor. // // Motor: autentica o /api/ext/v1/stream pela x-nw-key + x-nw-email (a chave é a // mestra; o tenant é resolvido pelo email do usuário) e exige o path EXATO, sem // query — por isso reescrevemos um request-line limpo. O email vem do JWT. export function createWsTunnel() { return function wsTunnel(req, clientSocket, head) { const fail = (statusLine) => { try { clientSocket.write(`HTTP/1.1 ${statusLine}\r\n\r\n`); } catch { /* socket já morto */ } clientSocket.destroy(); }; // Auth: JWT do scoreodonto no query string (?token=). let token; try { token = new URL(req.url, 'http://localhost').searchParams.get('token'); } catch { return fail('400 Bad Request'); } let email; try { const p = jwt.verify(token, process.env.JWT_SECRET); email = (p.email || '').toLowerCase(); } catch { return fail('401 Unauthorized'); } if (!email) return fail('401 Unauthorized'); const cfg = getConfig(); if (!cfg.motorUrl || !cfg.integrationKey) return fail('503 Service Unavailable'); let target; try { target = new URL(cfg.motorUrl); } catch { return fail('502 Bad Gateway'); } const isHttps = target.protocol === 'https:'; const host = target.hostname; const port = Number(target.port) || (isHttps ? 443 : 80); const onConnect = () => { // Reescreve o handshake: path EXATO + Host do motor + x-nw-key. // Preserva os headers do WS (sec-websocket-*, upgrade, connection). const headers = Object.entries(req.headers) .filter(([k]) => !['host', 'x-nw-key', 'x-nw-email'].includes(k.toLowerCase())) .map(([k, v]) => `${k}: ${v}`) .join('\r\n'); proxySocket.write( `${req.method} /api/ext/v1/stream HTTP/1.1\r\n` + `Host: ${host}\r\n` + `x-nw-key: ${cfg.integrationKey}\r\n` + `x-nw-email: ${email}\r\n` + `${headers}\r\n\r\n` ); if (head && head.length) proxySocket.write(head); proxySocket.pipe(clientSocket); clientSocket.pipe(proxySocket); }; const proxySocket = isHttps ? tls.connect({ host, port, servername: host }, onConnect) : net.connect(port, host, onConnect); proxySocket.on('error', (err) => { console.error('[newwhats] túnel WS erro:', err.message); clientSocket.destroy(); }); clientSocket.on('error', () => proxySocket.destroy()); clientSocket.on('close', () => proxySocket.destroy()); proxySocket.on('close', () => clientSocket.destroy()); }; }