[object Object]

← 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

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 →