← back to La Socrata Ingester
rate-limit: finalize apply-ratelimit.sh — token-free DRY_RUN preview, dedup rule, verified JSON+zone
e9415b828b2880dbdb59c28d15d5bfe1a442bc02 · 2026-08-12 09:07:16 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M scripts/apply-ratelimit.sh
Diff
commit e9415b828b2880dbdb59c28d15d5bfe1a442bc02
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 09:07:16 2026 -0700
rate-limit: finalize apply-ratelimit.sh — token-free DRY_RUN preview, dedup rule, verified JSON+zone
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/apply-ratelimit.sh | 30 +++++++++++++++++++++++-------
1 file changed, 23 insertions(+), 7 deletions(-)
diff --git a/scripts/apply-ratelimit.sh b/scripts/apply-ratelimit.sh
index eb5d2e5..1724baa 100755
--- a/scripts/apply-ratelimit.sh
+++ b/scripts/apply-ratelimit.sh
@@ -1,16 +1,34 @@
#!/bin/bash
# Apply a Cloudflare rate-limit rule to protect buildingpermits.agentabrams.com /api/.
-# Needs a WAF-scoped token (Zone > WAF > Edit). Non-destructive: reads the existing
-# http_ratelimit entrypoint and only adds our rule if it isn't already present.
+# Needs a WAF-scoped token (Zone > WAF > Edit, scoped to agentabrams.com).
+# Non-destructive: reads the existing http_ratelimit entrypoint and only adds our
+# rule if it isn't already present.
#
-# Usage: CLOUDFLARE_WAF_TOKEN=xxxx bash scripts/apply-ratelimit.sh
-# (or pass the token as $1)
+# Usage:
+# DRY_RUN=1 bash scripts/apply-ratelimit.sh # preview rule+target, no token, no writes
+# CLOUDFLARE_WAF_TOKEN=xxxx bash scripts/apply-ratelimit.sh # apply (GATED — Steve only)
+# (token may also be passed as $1)
set -euo pipefail
TOKEN="${CLOUDFLARE_WAF_TOKEN:-${1:-}}"
ZONE="0121a01d603b55dd23cb23d78987e16e" # agentabrams.com
API="https://api.cloudflare.com/client/v4"
DESC="buildingpermits API rate limit"
-[ -n "$TOKEN" ] || { echo "Set CLOUDFLARE_WAF_TOKEN (Zone>WAF>Edit scope)"; exit 1; }
+
+# The rule: block an IP that exceeds 120 req/60s to /api/ on the public host,
+# for 60s. cf.colo.id is required by the ratelimit schema; ip.src is the counter.
+RULE='{"action":"block","description":"'"$DESC"'","expression":"(http.host eq \"buildingpermits.agentabrams.com\" and starts_with(http.request.uri.path, \"/api/\"))","ratelimit":{"characteristics":["ip.src","cf.colo.id"],"period":60,"requests_per_period":120,"mitigation_timeout":60}}'
+
+# DRY_RUN — print exactly what WOULD be sent, touch nothing, need no token.
+if [ "${DRY_RUN:-0}" = "1" ]; then
+ echo "DRY RUN — no token used, no Cloudflare write."
+ echo "Zone: agentabrams.com ($ZONE)"
+ echo "Phase: http_ratelimit Endpoint: $API/zones/$ZONE/rulesets/..."
+ echo "Rule to add:"
+ echo "$RULE" | node -e 'const d=JSON.parse(require("fs").readFileSync(0));console.log(JSON.stringify(d,null,2))'
+ exit 0
+fi
+
+[ -n "$TOKEN" ] || { echo "Set CLOUDFLARE_WAF_TOKEN (Zone>WAF>Edit scope) or run DRY_RUN=1 to preview"; exit 1; }
auth=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json")
# 1. read the current http_ratelimit entrypoint (ruleset id + rules)
@@ -21,8 +39,6 @@ fi
RID=$(echo "$EP" | node -e 'const d=JSON.parse(require("fs").readFileSync(0));console.log(d.result?.id||"")')
if echo "$EP" | grep -q "$DESC"; then echo "Rule already present — nothing to do."; exit 0; fi
-RULE='{"action":"block","description":"'"$DESC"'","expression":"(http.host eq \"buildingpermits.agentabrams.com\" and starts_with(http.request.uri.path, \"/api/\"))","ratelimit":{"characteristics":["ip.src","cf.colo.id"],"period":60,"requests_per_period":120,"mitigation_timeout":60}}'
-
if [ -n "$RID" ]; then
echo "Appending rule to existing ruleset $RID …"
curl -s "${auth[@]}" -X POST "$API/zones/$ZONE/rulesets/$RID/rules" -d "$RULE" \
← c9ccae5 Unified property drill-down: address -> property + trade str
·
back to La Socrata Ingester
·
Pre-stage lint-clean launchd plists for daily canary (08:00) 4c7b267 →