@tillstack/shield-cloudflare
v0.3.1
Published
TillShield inline WAF for Cloudflare Workers — evaluate, block, challenge and rate-limit requests at the edge, with an optional KV-backed distributed rate limiter.
Downloads
37
Maintainers
Readme
@tillstack/shield-cloudflare
TillShield inline WAF for Cloudflare Workers. Evaluate, block, challenge and rate-limit requests at the edge using rules you manage in the TillDev dashboard, plus the cross-customer threat-intel deny list.
Uses Cloudflare's own request signals (CF-Connecting-IP, request.cf.country).
Rules are fetched from TillDev and evaluated locally per request; decisions are
reported via ctx.waitUntil so they never delay the response.
Install
npm install @tillstack/shield-cloudflareSet up
Edge keys bind to a Pulse project — the shared org project that anchors
telemetry across TillDev. It is not a secrets project or a forge repo; if
you pass one of those ids you'll get 400 Project not in org.
Zero → key, entirely from the CLI:
# 1. If your org has no Pulse project yet, create one:
tilldev pulse projects create my-app --platform web
# → prints the project id
# 2. Mint the edge key against that project id:
tilldev shield edge-keys create --project <pulse-project-id> --name production
# → prints the tse_… secret ONCE
# 3. Store it as a Worker secret:
wrangler secret put SHIELD_EDGE_KEYOr mint it in the dashboard under Shield → Inline WAF.
Wrap your fetch handler
import { createShield } from '@tillstack/shield-cloudflare'
export default {
async fetch(request, env, ctx) {
const shield = createShield({ edgeKey: env.SHIELD_EDGE_KEY, kv: env.SHIELD_KV })
return shield.wrap((req, env, ctx) => {
// …your Worker…
return new Response('ok')
})(request, env, ctx)
},
}Or gate manually
const shield = createShield({ edgeKey: env.SHIELD_EDGE_KEY })
const blocked = await shield.handle(request, ctx)
if (blocked) return blocked
// …continue…Options
| Option | Default | Description |
| --- | --- | --- |
| edgeKey | — | Required. Your tse_… project edge key. |
| apiBase | https://tilldev.dev | TillDev API base. |
| failMode | 'open' | 'open' serves traffic if TillDev is unreachable; 'closed' blocks it. |
| kv | — | A KVNamespace for a distributed rate limiter shared across edge locations. Omit for per-isolate limits. |
| onDecision | — | (result, req) => void — observe every non-allow decision. |
| defaultBlockBody | "Request blocked by TillShield." | Body when a rule sets none. |
Distributed rate limiting
Bind a KV namespace to share rate-limit counters across Cloudflare's edge:
# wrangler.toml
[[kv_namespaces]]
binding = "SHIELD_KV"
id = "<your-namespace-id>"Without KV, limits are counted per isolate (approximate, but zero-config). KV counters are eventually consistent and enforce a 60-second minimum window; for strict low-window limits use a Durable Object.
TillGate human-check on challenge
Pass a tillgate config and any rule that decides challenge serves a TillGate
interstitial (a privacy-preserving human check) instead of a bare status. Once a
visitor clears it, a short-lived signed pass cookie lets them through until it
expires.
const shield = createShield({
edgeKey: env.SHIELD_EDGE_KEY,
tillgate: {
sitekey: env.TILLGATE_SITEKEY, // tg_site_…
secret: env.TILLGATE_SECRET, // tg_secret_… (used server-side for /siteverify)
},
})The edge claims one path — /__tillshield/verify — to exchange the TillGate
token for the pass cookie. block decisions are unaffected (they always block).
Notes
- Fail-open by design (configurable to
'closed'). - Config is cached per isolate and refreshed on a TTL.
- No raw IPs leave the edge — decision reports carry
sha256(ip)only.
