[object Object]

← back to NationalPaperHangers

Split geoLimiter — map JSON and helpful-votes get independent buckets

71aa66dd37139ff04cd43663967f86a22df76ec9 · 2026-05-18 16:15:51 -0700 · SteveStudio2

Same shared-bucket fix as claimLimiter: /api/installers.geo and the
paper-comment helpful-vote route shared one 60/min bucket, so heavy map
browsing could exhaust a reader's vote budget. Added a minuteLimiter(max)
factory; geoLimiter and voteLimiter are now separate instances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 71aa66dd37139ff04cd43663967f86a22df76ec9
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 16:15:51 2026 -0700

    Split geoLimiter — map JSON and helpful-votes get independent buckets
    
    Same shared-bucket fix as claimLimiter: /api/installers.geo and the
    paper-comment helpful-vote route shared one 60/min bucket, so heavy map
    browsing could exhaust a reader's vote budget. Added a minuteLimiter(max)
    factory; geoLimiter and voteLimiter are now separate instances.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/server.js b/server.js
index 36acb42..7b7b4b8 100644
--- a/server.js
+++ b/server.js
@@ -186,14 +186,18 @@ const bookLimiter = rateLimit({
   standardHeaders: true, legacyHeaders: false,
   message: { error: 'too_many_requests' }
 });
-// Public map JSON. Browsers load it once per /map view; abusive scrapers
-// hit it in tight loops. 60 req/min/IP keeps the legitimate UX snappy
-// while making bulk-extraction expensive.
-const geoLimiter = rateLimit({
-  windowMs: 60 * 1000, max: 60,
+// Per-minute limiters — independent buckets (see hourLimiter rationale above).
+// geoLimiter guards the public map JSON: browsers load it once per /map view,
+// abusive scrapers hit it in tight loops. voteLimiter guards helpful-votes on
+// paper comments. They're split so heavy map browsing can't exhaust a reader's
+// vote budget. 60/min/IP keeps legit UX snappy while making abuse expensive.
+const minuteLimiter = (max) => rateLimit({
+  windowMs: 60 * 1000, max,
   standardHeaders: true, legacyHeaders: false,
   message: { error: 'too_many_requests' }
 });
+const geoLimiter  = minuteLimiter(60);
+const voteLimiter = minuteLimiter(60);
 app.use('/login', loginLimiter);
 app.use('/signup', loginLimiter);
 app.use(['/installer/:slug/claim', '/installer/:slug/claim/complete'], claimLimiter);
@@ -202,7 +206,7 @@ app.use('/api/installers.geo', geoLimiter);
 app.use('/installer/:slug/notify-when-live', notifyLimiter);
 app.use('/installer/:slug/coi-request', coiLimiter);
 app.use('/papers/:slug/comments', commentLimiter);
-app.use('/papers/:slug/comments/:id/helpful', geoLimiter);
+app.use('/papers/:slug/comments/:id/helpful', voteLimiter);
                                              // 60 votes/min/IP — high enough
                                              // to not block real browsing,
                                              // low enough to make scripted

← 868356a Split shared claimLimiter into per-route buckets (claim/noti  ·  back to NationalPaperHangers  ·  Clear all 6 npm audit vulns — audit fix + bcrypt 5.x→6.0.0 283685a →