← back to Rentv 2026
rentv: per-IP write-form rate limits on /api/subscribe + /api/ad/reserve (TK-10559)
5a59077e18b15f2833b0a3f9c7aa9d782f0ed22b · 2026-08-15 04:10:39 -0700 · Steve Abrams
TK-10559 approved 2026-08-14 by rentv-approver triage session.
/api/subscribe: writeThrottle(10 req/hr per IP) — prevents JSONL list flooding;
existing subSet.size cap still applies for dedup.
/api/ad/reserve: adReserveThrottle(5 req/hr per IP) — prioritized (not idempotent;
each inquiry appends to ad-reservations.jsonl and marks slot reserved).
Both throttle with Retry-After header. No external dep (plain Map + Date.now).
Co-Authored-By: 4am-fix-loop <noreply@anthropic.com>
Files touched
M server.jsM src/ad-system.cjs
Diff
commit 5a59077e18b15f2833b0a3f9c7aa9d782f0ed22b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 15 04:10:39 2026 -0700
rentv: per-IP write-form rate limits on /api/subscribe + /api/ad/reserve (TK-10559)
TK-10559 approved 2026-08-14 by rentv-approver triage session.
/api/subscribe: writeThrottle(10 req/hr per IP) — prevents JSONL list flooding;
existing subSet.size cap still applies for dedup.
/api/ad/reserve: adReserveThrottle(5 req/hr per IP) — prioritized (not idempotent;
each inquiry appends to ad-reservations.jsonl and marks slot reserved).
Both throttle with Retry-After header. No external dep (plain Map + Date.now).
Co-Authored-By: 4am-fix-loop <noreply@anthropic.com>
---
server.js | 19 ++++++++++++++++++-
src/ad-system.cjs | 12 +++++++++++-
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index 5359b305..89a406fc 100644
--- a/server.js
+++ b/server.js
@@ -788,7 +788,24 @@ try {
try { const e = JSON.parse(l).email; if (e) subSet.add(e); } catch { /* skip */ }
});
} catch { /* no file yet */ }
-app.post('/api/subscribe', (req, res) => {
+// P3 — per-IP throttle on public write endpoints (TK-10559, approved 2026-08-14)
+// /api/subscribe: 10 calls/hr per IP (idempotent; cap already guards list size)
+// /api/ad/reserve is in ad-system.cjs; throttle lives here via shared Map
+const _writeFails = new Map(); // ip → { count, first, win_ms }
+function writeThrottle(maxPerWin, winMs) {
+ return (req, res, next) => {
+ const ip = req.ip || '?'; const now = Date.now();
+ const f = _writeFails.get(ip + winMs) || { count: 0, first: now };
+ if (now - f.first > winMs) { f.count = 0; f.first = now; }
+ f.count++; _writeFails.set(ip + winMs, f);
+ if (f.count > maxPerWin) {
+ const retryAfter = Math.ceil((f.first + winMs - now) / 1000);
+ return res.status(429).set('Retry-After', retryAfter).json({ ok: false, error: 'Too many requests.' });
+ }
+ next();
+ };
+}
+app.post('/api/subscribe', writeThrottle(10, 60 * 60 * 1000), (req, res) => {
const b = req.body || {};
const email = String(b.email || '').trim().toLowerCase();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return res.status(400).json({ ok: false, error: 'valid email required' });
diff --git a/src/ad-system.cjs b/src/ad-system.cjs
index 7714689d..e3b5b18b 100644
--- a/src/ad-system.cjs
+++ b/src/ad-system.cjs
@@ -228,7 +228,17 @@ module.exports = function mountAdSystem(app, opts) {
});
// Buy / reserve inquiry — records intent, NO charge (Steve-gated payment).
- app.post('/api/ad/reserve', (req, r) => {
+ // TK-10559 per-IP rate limit: 5 reserve inquiries / hr (not idempotent — each appends)
+ const _adReserveMap = new Map();
+ const adReserveThrottle = (req, r, next) => {
+ const ip = (req.ip || '?'); const now = Date.now(); const WIN = 60 * 60 * 1000, MAX = 5;
+ const f = _adReserveMap.get(ip) || { count: 0, first: now };
+ if (now - f.first > WIN) { f.count = 0; f.first = now; }
+ f.count++; _adReserveMap.set(ip, f);
+ if (f.count > MAX) return r.status(429).set('Retry-After', Math.ceil((f.first + WIN - now) / 1000)).json({ ok: false, error: 'Too many requests.' });
+ next();
+ };
+ app.post('/api/ad/reserve', adReserveThrottle, (req, r) => {
const b = req.body || {};
const rec = {
at: nowISO(),
← f2bf7551 rentv: P1+P2+P3 public-launch hardening (TK-10564/10575, Ste
·
back to Rentv 2026
·
TK-10569: port /api/advertise + /api/advertise-inquiries fro 437aa93c →