← back to Bounce Studio
Pre-flight for go-live: rate-limit /api/waitlist (429 after 5/10min) + async write + target sixskills.agentabrams.com
904982e674115491f63fdff1ca5f1a2c05cc5170 · 2026-07-13 15:44:10 -0700 · Steve
Files touched
M .deploy.confM server.js
Diff
commit 904982e674115491f63fdff1ca5f1a2c05cc5170
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 13 15:44:10 2026 -0700
Pre-flight for go-live: rate-limit /api/waitlist (429 after 5/10min) + async write + target sixskills.agentabrams.com
---
.deploy.conf | 3 ++-
server.js | 26 ++++++++++++++++++++++----
2 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/.deploy.conf b/.deploy.conf
index d588eb0..64129d1 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -1,6 +1,7 @@
# Deploy config for bounce-studio (consumed by /deploy → ~/Projects/_shared/scripts/deploy.sh)
-# DRAFT — nothing deploys until Steve approves. Port is provisional; confirm it's free on Kamatera.
+# Target: sixskills.agentabrams.com (Steve-approved subdomain). Confirm PORT is free on Kamatera.
PROJECT_NAME=bounce-studio
DEPLOY_PATH=/root/public-projects/bounce-studio
HEALTH_URL=http://127.0.0.1:9948/healthz
PORT=9948
+DOMAIN=sixskills.agentabrams.com
diff --git a/server.js b/server.js
index 2a39fef..1cc2414 100644
--- a/server.js
+++ b/server.js
@@ -37,9 +37,25 @@ function serveStatic(req, res) {
res.end('not found');
}
+// Simple in-memory rate limiter for the waitlist endpoint (bot / disk-fill guard).
+// Max WL_MAX submissions per IP per WL_WINDOW ms. Not a substitute for a WAF, but
+// stops a single script from filling the JSONL.
+const WL_MAX = 5, WL_WINDOW = 10 * 60 * 1000;
+const wlHits = new Map();
+function wlAllowed(ip) {
+ const now = Date.now();
+ const arr = (wlHits.get(ip) || []).filter((t) => now - t < WL_WINDOW);
+ if (arr.length >= WL_MAX) { wlHits.set(ip, arr); return false; }
+ arr.push(now); wlHits.set(ip, arr);
+ if (wlHits.size > 5000) wlHits.clear(); // crude unbounded-growth guard
+ return true;
+}
+
const server = http.createServer((req, res) => {
- // Local-only waitlist capture. Appends to data/waitlist.jsonl. No email is sent.
+ // Local waitlist capture. Appends to data/waitlist.jsonl. No email is sent.
if (req.method === 'POST' && req.url === '/api/waitlist') {
+ const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || '').split(',')[0].trim();
+ if (!wlAllowed(ip)) { res.writeHead(429, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'too many requests' })); }
let body = '';
req.on('data', (c) => { body += c; if (body.length > 1e4) req.destroy(); });
req.on('end', () => {
@@ -48,9 +64,11 @@ const server = http.createServer((req, res) => {
const ok = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
if (!ok) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'invalid email' })); }
const rec = { ts: new Date().toISOString(), email, budget, note, ua: (req.headers['user-agent'] || '').slice(0, 200) };
- fs.appendFileSync(path.join(DATA, 'waitlist.jsonl'), JSON.stringify(rec) + '\n');
- res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ ok: true }));
+ fs.appendFile(path.join(DATA, 'waitlist.jsonl'), JSON.stringify(rec) + '\n', (e) => {
+ if (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'write failed' })); }
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ ok: true }));
+ });
});
return;
}
← 73eb721 Contrarian FIX FIRST: add real source-tweet proof + self-ref
·
back to Bounce Studio
·
deploy.conf: skip npm install (no deps), exclude data/ from 43d4030 →