[object Object]

← 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

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 →