← back to Ca Donations
ca-donations: FIX-FIRST hardening (Cody gate) — rate limit + data-quality guard
8340af84fec2f5887c5013db965d3bd15994e309 · 2026-08-22 11:34:48 -0700 · Steve
- dependency-free in-memory rate limiter on /api/* (120 req/60s/IP -> 429): caps bulk donor-row scraping
- POL_CLEAN guard: never serve future-dated (17) or null-donor (125) political artifacts as fact about named people
- GRANT_CLEAN guard: filter placeholder grantees (See Schedule/Attached/Eligible Patients)
- verified: 0 future/null rows served, 429 past cap, auth still 401 fail-closed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 8340af84fec2f5887c5013db965d3bd15994e309
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Aug 22 11:34:48 2026 -0700
ca-donations: FIX-FIRST hardening (Cody gate) — rate limit + data-quality guard
- dependency-free in-memory rate limiter on /api/* (120 req/60s/IP -> 429): caps bulk donor-row scraping
- POL_CLEAN guard: never serve future-dated (17) or null-donor (125) political artifacts as fact about named people
- GRANT_CLEAN guard: filter placeholder grantees (See Schedule/Attached/Eligible Patients)
- verified: 0 future/null rows served, 429 past cap, auth still 401 fail-closed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
server.js | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index 0d125b1..ceffa40 100644
--- a/server.js
+++ b/server.js
@@ -21,7 +21,23 @@ app.use((req, res, next) => {
return res.status(401).send('Auth required');
});
+// Dependency-free in-memory rate limiter on /api/* — caps bulk scraping of donor rows.
+const RL_WINDOW = 60_000, RL_MAX = 120, rl = new Map();
+app.use('/api', (req, res, next) => {
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
+ const now = Date.now();
+ const rec = rl.get(ip) || { n: 0, reset: now + RL_WINDOW };
+ if (now > rec.reset) { rec.n = 0; rec.reset = now + RL_WINDOW; }
+ rec.n++; rl.set(ip, rec);
+ if (rl.size > 5000) for (const [k, v] of rl) if (now > v.reset) rl.delete(k); // GC
+ if (rec.n > RL_MAX) { res.set('Retry-After', '60'); return res.status(429).json({ error: 'rate limited' }); }
+ next();
+});
+
const like = (s) => `%${String(s).trim()}%`;
+// Baseline data-quality guard — never serve raw-feed artifacts as fact about named people.
+const POL_CLEAN = `donor_name IS NOT NULL AND donor_name <> '' AND (contribution_date IS NULL OR contribution_date <= CURRENT_DATE)`;
+const GRANT_CLEAN = `grantee_name IS NOT NULL AND grantee_name !~* 'see (schedule|attached|statement)|eligible patients|various'`;
// Whitelisted ORDER BY per endpoint — the UI's sort dropdown must actually sort.
const ORDER = {
orgs: { name: 'name ASC', status: 'ca_ag_status ASC, name ASC', ntee: 'ntee_code ASC NULLS LAST, name ASC' },
@@ -75,7 +91,7 @@ app.get('/api/org/:ein', async (req, res) => {
app.get('/api/grants', async (req, res) => {
try {
const { grantor = '', grantee = '', year = '', sort = 'amount', limit = 100 } = req.query;
- const where = [], params = [];
+ const where = [GRANT_CLEAN], params = [];
if (grantor) { params.push(like(grantor)); where.push(`grantor_name ILIKE $${params.length}`); }
if (grantee) { params.push(like(grantee)); where.push(`grantee_name ILIKE $${params.length}`); }
if (year) { params.push(+year); where.push(`tax_year = $${params.length}`); }
@@ -92,7 +108,7 @@ app.get('/api/grants', async (req, res) => {
app.get('/api/political', async (req, res) => {
try {
const { donor = '', recipient = '', jurisdiction = '', employer = '', sort = 'date', limit = 100 } = req.query;
- const where = [], params = [];
+ const where = [POL_CLEAN], params = [];
if (donor) { params.push(like(donor)); where.push(`donor_name ILIKE $${params.length}`); }
if (recipient) { params.push(like(recipient)); where.push(`recipient_name ILIKE $${params.length}`); }
if (employer) { params.push(like(employer)); where.push(`donor_employer ILIKE $${params.length}`); }
← 9e7508b ca-donations: wire server-side sort (mandatory sort rule) ac
·
back to Ca Donations
·
ca-donations: freshness+completeness canary + data-quality a e220fe9 →