StenyBridge / src /server.js
dieumercimvemba's picture
Update src/server.js
5227133 verified
"use strict";
/**
* ============================================================
* Mvemba Research Systems — Steny Bridge
* Secure HTTP Gateway + n8n Bridge
* Scientific-grade operational constraints:
* - Minimal public surface area
* - API key auth for outbound send requests
* - Rate limiting
* - HMAC signature to n8n (optional but recommended)
* - Strict input validation (Zod)
* - Diagnostics endpoint (/diag) for network validation
* ============================================================
*/
const express = require("express");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const axios = require("axios");
const pinoHttp = require("pino-http");
const { z } = require("zod");
// Diagnostics
const dns = require("dns").promises;
const https = require("https");
const { startWhatsApp } = require("./whatsapp");
const { requireApiKey, signPayload } = require("./security");
const app = express();
app.disable("x-powered-by");
app.use(helmet());
app.use(express.json({ limit: "256kb" }));
app.use(pinoHttp());
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false
});
app.use(limiter);
const PORT = Number(process.env.PORT || 7860);
const N8N_WEBHOOK_INBOUND = process.env.N8N_WEBHOOK_INBOUND || "";
const N8N_HMAC_SECRET = process.env.N8N_HMAC_SECRET || "";
const ALLOWED_TO_PREFIX = process.env.ALLOWED_TO_PREFIX || "";
let sock = null;
app.get("/", (req, res) => {
res.status(200).send("Steny Bridge is running.");
});
app.get("/health", (req, res) => {
res.json({ ok: true, whatsappReady: Boolean(sock) });
});
app.get("/diag", async (req, res) => {
const out = {};
try {
out.dns_web_whatsapp = await dns.lookup("web.whatsapp.com");
} catch (e) {
out.dns_web_whatsapp_error = e.message;
}
out.https_google = await new Promise((resolve) => {
const r = https.get("https://www.google.com", (resp) => {
resolve({ status: resp.statusCode });
resp.resume();
});
r.on("error", (e) => resolve({ error: e.message }));
r.setTimeout(8000, () => {
r.destroy(new Error("timeout"));
});
});
res.json(out);
});
const SendSchema = z.object({
to: z.string().min(10).max(60),
text: z.string().min(1).max(3000)
});
app.post("/v1/send", requireApiKey, async (req, res) => {
try {
if (!sock) return res.status(503).json({ error: "WhatsApp not ready" });
const parsed = SendSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: "Invalid payload" });
const { to, text } = parsed.data;
if (ALLOWED_TO_PREFIX) {
if (!to.startsWith(ALLOWED_TO_PREFIX)) {
return res.status(403).json({ error: "Recipient not allowed" });
}
}
await sock.sendMessage(to, { text });
return res.json({ sent: true });
} catch (_) {
return res.status(500).json({ error: "Send failed" });
}
});
async function postToN8n(event) {
if (!N8N_WEBHOOK_INBOUND) return;
const headers = {};
if (N8N_HMAC_SECRET) {
headers["x-steny-signature"] = signPayload(event, N8N_HMAC_SECRET);
}
await axios.post(N8N_WEBHOOK_INBOUND, event, {
headers,
timeout: 15000
});
}
async function main() {
console.log("Steny Bridge booting...");
sock = await startWhatsApp({
onIncomingText: async ({ from, text }) => {
const event = { from, text, timestamp: Date.now() };
try {
await postToN8n(event);
} catch (_) {}
}
});
app.listen(PORT, () => {
console.log(`Steny Bridge listening on port ${PORT}`);
});
}
main().catch(() => process.exit(1));