dieumercimvemba commited on
Commit
427288e
·
verified ·
1 Parent(s): 483c3ac

Create src/server.js

Browse files
Files changed (1) hide show
  1. src/server.js +111 -0
src/server.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ============================================================
3
+ * Mvemba Research Systems — Steny Bridge
4
+ * Secure HTTP Gateway + n8n Bridge
5
+ * Scientific-grade operational constraints:
6
+ * - Minimal public surface area
7
+ * - API key authentication for outbound send requests
8
+ * - Rate limiting
9
+ * - Optional HMAC signature to n8n
10
+ * - Strict input validation (Zod)
11
+ * ============================================================
12
+ */
13
+
14
+ const express = require("express");
15
+ const helmet = require("helmet");
16
+ const rateLimit = require("express-rate-limit");
17
+ const axios = require("axios");
18
+ const pino = require("pino-http");
19
+ const { z } = require("zod");
20
+
21
+ const { startWhatsApp } = require("./whatsapp");
22
+ const { requireApiKey, signPayload } = require("./security");
23
+
24
+ const app = express();
25
+ app.disable("x-powered-by");
26
+ app.use(helmet());
27
+ app.use(express.json({ limit: "256kb" }));
28
+ app.use(pino());
29
+
30
+ const limiter = rateLimit({
31
+ windowMs: 60 * 1000,
32
+ max: 60,
33
+ standardHeaders: true,
34
+ legacyHeaders: false
35
+ });
36
+ app.use(limiter);
37
+
38
+ const PORT = Number(process.env.PORT || 7860);
39
+ const N8N_WEBHOOK_INBOUND = process.env.N8N_WEBHOOK_INBOUND || "";
40
+ const N8N_HMAC_SECRET = process.env.N8N_HMAC_SECRET || "";
41
+ const ALLOWED_TO_PREFIX = process.env.ALLOWED_TO_PREFIX || "";
42
+
43
+ let sock = null;
44
+
45
+ app.get("/health", (req, res) => {
46
+ res.json({ ok: true, whatsappReady: Boolean(sock) });
47
+ });
48
+
49
+ const SendSchema = z.object({
50
+ to: z.string().min(10).max(40),
51
+ text: z.string().min(1).max(3000)
52
+ });
53
+
54
+ app.post("/v1/send", requireApiKey, async (req, res) => {
55
+ try {
56
+ if (!sock) return res.status(503).json({ error: "WhatsApp not ready" });
57
+
58
+ const parsed = SendSchema.safeParse(req.body);
59
+ if (!parsed.success) return res.status(400).json({ error: "Invalid payload" });
60
+
61
+ const { to, text } = parsed.data;
62
+
63
+ if (ALLOWED_TO_PREFIX) {
64
+ // Basic safety: allow only JIDs matching your expected prefix
65
+ // Example: 243xxxx@s.whatsapp.net
66
+ if (!to.startsWith(ALLOWED_TO_PREFIX) && !to.startsWith(`${ALLOWED_TO_PREFIX}`)) {
67
+ return res.status(403).json({ error: "Recipient not allowed" });
68
+ }
69
+ }
70
+
71
+ await sock.sendMessage(to, { text });
72
+ return res.json({ sent: true });
73
+ } catch (e) {
74
+ return res.status(500).json({ error: "Send failed" });
75
+ }
76
+ });
77
+
78
+ async function postToN8n(event) {
79
+ if (!N8N_WEBHOOK_INBOUND) return;
80
+
81
+ const headers = {};
82
+ if (N8N_HMAC_SECRET) {
83
+ headers["x-steny-signature"] = signPayload(event, N8N_HMAC_SECRET);
84
+ }
85
+
86
+ await axios.post(N8N_WEBHOOK_INBOUND, event, { headers, timeout: 15000 });
87
+ }
88
+
89
+ async function main() {
90
+ sock = await startWhatsApp({
91
+ onIncomingText: async ({ from, text }) => {
92
+ // Conservative policy: only handle inbound user messages.
93
+ const event = { from, text, timestamp: Date.now() };
94
+
95
+ try {
96
+ await postToN8n(event);
97
+ } catch (e) {
98
+ // Do not leak secrets or stack traces
99
+ }
100
+ }
101
+ });
102
+
103
+ app.listen(PORT, () => {
104
+ // No console secrets; logs only operational signals
105
+ console.log(`Steny Bridge listening on port ${PORT}`);
106
+ });
107
+ }
108
+
109
+ main().catch(() => {
110
+ process.exit(1);
111
+ });