import express from 'express' import cors from 'cors' import helmet from 'helmet' import http from 'node:http' import 'dotenv/config' import { auditRouter } from './routes/audit.js' import { env } from './services/env.js' const app = express() // Behind a reverse proxy, so trust exactly one proxy hop. Without this the // rate limiter would see the proxy's IP for everyone and throttle all // visitors together. app.set('trust proxy', 1) app.use(helmet()) // An allowlist, never '*'. ALLOWED_ORIGINS is comma separated. const allowedOrigins = (env('ALLOWED_ORIGINS', '') ?? '') .split(',') .map((origin) => origin.trim().replace(/\/$/, '')) .filter(Boolean) // An empty allowlist refuses every browser request, which looks exactly like // a broken endpoint. Say so at startup instead of at 3am. if (allowedOrigins.length === 0) { console.warn( 'WARNING: ALLOWED_ORIGINS is empty - all cross-origin requests will be refused.' ) } app.use( cors({ origin(origin, callback) { // Same-origin and curl send no Origin header at all. if (!origin) return callback(null, true) if (allowedOrigins.includes(origin)) return callback(null, true) callback(new Error('Origin not allowed')) }, methods: ['POST'], allowedHeaders: ['Content-Type'], }) ) // The form payload is a few hundred bytes. Anything near this cap is abuse. app.use(express.json({ limit: '10kb' })) app.get('/health', (_req, res) => res.json({ status: 'ok' })) app.use('/api/audit-request', auditRouter) // Keep internals out of the response body. app.use((error, _req, res, _next) => { console.error('Unhandled error:', error.message) res.status(500).json({ success: false, message: 'Internal server error.' }) }) const port = Number(env('PORT', '3001')) app.listen(port, '127.0.0.1', () => { console.log(`Audit endpoint listening on 127.0.0.1:${port}`) }) // Optional second listener for a reverse proxy that itself runs in Docker // (e.g. Caddy). 127.0.0.1 inside that proxy's container is the container's // own loopback, not this host's, so it cannot reach the 127.0.0.1 listener // above at all. The fix is NOT to bind 0.0.0.0 - on a VPS with a public IP // and no confirmed firewall, that would also accept connections arriving on // the public interface, bypassing the proxy's TLS termination entirely. // Binding this second, additional listener to the Docker bridge's gateway // address instead only accepts traffic arriving over that specific virtual // network. Find the right value with: // docker inspect --format \ // '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.Gateway}}{{"\n"}}{{end}}' const dockerBridgeHost = env('DOCKER_BRIDGE_HOST') if (dockerBridgeHost) { http.createServer(app).listen(port, dockerBridgeHost, () => { console.log(`Audit endpoint also listening on ${dockerBridgeHost}:${port} (Docker bridge)`) }) }