Spaces:
Sleeping
Sleeping
| ; | |
| /** | |
| * ============================================================ | |
| * 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)); | |