Spaces:
Sleeping
Sleeping
Update src/server.js
Browse files- src/server.js +46 -58
src/server.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
| 4 |
* Secure HTTP Gateway + n8n Bridge
|
| 5 |
* Scientific-grade operational constraints:
|
| 6 |
* - Minimal public surface area
|
| 7 |
-
* - API key
|
| 8 |
* - Rate limiting
|
| 9 |
-
* -
|
| 10 |
* - Strict input validation (Zod)
|
|
|
|
| 11 |
* ============================================================
|
| 12 |
*/
|
| 13 |
|
|
@@ -15,9 +16,13 @@ const express = require("express");
|
|
| 15 |
const helmet = require("helmet");
|
| 16 |
const rateLimit = require("express-rate-limit");
|
| 17 |
const axios = require("axios");
|
| 18 |
-
const
|
| 19 |
const { z } = require("zod");
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
const { startWhatsApp } = require("./whatsapp");
|
| 22 |
const { requireApiKey, signPayload } = require("./security");
|
| 23 |
|
|
@@ -25,7 +30,7 @@ const app = express();
|
|
| 25 |
app.disable("x-powered-by");
|
| 26 |
app.use(helmet());
|
| 27 |
app.use(express.json({ limit: "256kb" }));
|
| 28 |
-
app.use(
|
| 29 |
|
| 30 |
const limiter = rateLimit({
|
| 31 |
windowMs: 60 * 1000,
|
|
@@ -42,71 +47,54 @@ 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 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
app.post("/v1/send", requireApiKey, async (req, res) => {
|
| 55 |
try {
|
| 56 |
-
|
| 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 |
-
|
| 75 |
}
|
| 76 |
-
});
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
await axios.post(N8N_WEBHOOK_INBOUND, event, { headers, timeout: 15000 });
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
async function main() {
|
| 90 |
-
console.log("Steny Bridge booting...");
|
| 91 |
-
sock = await startWhatsApp({
|
| 92 |
-
onIncomingText: async ({ from, text }) => {
|
| 93 |
-
// Conservative policy: only handle inbound user messages.
|
| 94 |
-
const event = { from, text, timestamp: Date.now() };
|
| 95 |
-
|
| 96 |
-
try {
|
| 97 |
-
await postToN8n(event);
|
| 98 |
-
} catch (e) {
|
| 99 |
-
// Do not leak secrets or stack traces
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
});
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
console.log(`Steny Bridge listening on port ${PORT}`);
|
| 107 |
-
});
|
| 108 |
-
}
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
|
|
|
| 112 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
* Secure HTTP Gateway + n8n Bridge
|
| 5 |
* Scientific-grade operational constraints:
|
| 6 |
* - Minimal public surface area
|
| 7 |
+
* - API key auth for outbound send requests
|
| 8 |
* - Rate limiting
|
| 9 |
+
* - HMAC signature to n8n (optional but recommended)
|
| 10 |
* - Strict input validation (Zod)
|
| 11 |
+
* - Runtime diagnostics endpoint (/diag) for network validation
|
| 12 |
* ============================================================
|
| 13 |
*/
|
| 14 |
|
|
|
|
| 16 |
const helmet = require("helmet");
|
| 17 |
const rateLimit = require("express-rate-limit");
|
| 18 |
const axios = require("axios");
|
| 19 |
+
const pinoHttp = require("pino-http");
|
| 20 |
const { z } = require("zod");
|
| 21 |
|
| 22 |
+
// Diagnostics
|
| 23 |
+
const dns = require("dns").promises;
|
| 24 |
+
const https = require("https");
|
| 25 |
+
|
| 26 |
const { startWhatsApp } = require("./whatsapp");
|
| 27 |
const { requireApiKey, signPayload } = require("./security");
|
| 28 |
|
|
|
|
| 30 |
app.disable("x-powered-by");
|
| 31 |
app.use(helmet());
|
| 32 |
app.use(express.json({ limit: "256kb" }));
|
| 33 |
+
app.use(pinoHttp());
|
| 34 |
|
| 35 |
const limiter = rateLimit({
|
| 36 |
windowMs: 60 * 1000,
|
|
|
|
| 47 |
|
| 48 |
let sock = null;
|
| 49 |
|
| 50 |
+
/**
|
| 51 |
+
* Root endpoint (prevents HF "connection not allowed" confusion)
|
| 52 |
+
*/
|
| 53 |
+
app.get("/", (req, res) => {
|
| 54 |
+
res.status(200).send("Steny Bridge is running.");
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
app.get("/health", (req, res) => {
|
| 58 |
res.json({ ok: true, whatsappReady: Boolean(sock) });
|
| 59 |
});
|
| 60 |
|
| 61 |
+
/**
|
| 62 |
+
* Diagnostics endpoint
|
| 63 |
+
* Use it to confirm if the container can resolve and reach WhatsApp Web.
|
| 64 |
+
* - DNS check for web.whatsapp.com
|
| 65 |
+
* - HTTPS check to a neutral endpoint (google.com)
|
| 66 |
+
*/
|
| 67 |
+
app.get("/diag", async (req, res) => {
|
| 68 |
+
const out = {};
|
| 69 |
|
|
|
|
| 70 |
try {
|
| 71 |
+
out.dns_web_whatsapp = await dns.lookup("web.whatsapp.com");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
} catch (e) {
|
| 73 |
+
out.dns_web_whatsapp_error = e.message;
|
| 74 |
}
|
|
|
|
| 75 |
|
| 76 |
+
out.https_google = await new Promise((resolve) => {
|
| 77 |
+
const r = https.get("https://www.google.com", (resp) => {
|
| 78 |
+
resolve({ status: resp.statusCode });
|
| 79 |
+
resp.resume();
|
| 80 |
+
});
|
| 81 |
|
| 82 |
+
r.on("error", (e) => resolve({ error: e.message }));
|
| 83 |
+
r.setTimeout(8000, () => {
|
| 84 |
+
r.destroy(new Error("timeout"));
|
| 85 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
});
|
| 87 |
|
| 88 |
+
res.json(out);
|
| 89 |
+
});
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
const SendSchema = z.object({
|
| 92 |
+
to: z.string().min(10).max(60),
|
| 93 |
+
text: z.string().min(1).max(3000)
|
| 94 |
});
|
| 95 |
+
|
| 96 |
+
app.post("/v1/send", requireApiKey, async (req, res) => {
|
| 97 |
+
try {
|
| 98 |
+
if (!sock) return res.status(503).json({ error: "WhatsApp not ready" });
|
| 99 |
+
|
| 100 |
+
const parse
|