@tillstack/shield-node
v0.3.0
Published
TillShield inline WAF for Node.js — Express/Connect/Fastify middleware and a raw http handler that block, challenge or rate-limit requests at your origin.
Maintainers
Readme
@tillstack/shield-node
TillShield inline WAF for Node.js. Block, challenge and rate-limit requests at your origin using rules you manage in the TillDev dashboard — plus the cross-customer threat-intel deny list.
Rules and the deny list are fetched from TillDev and evaluated locally on every request (no network call on the hot path). Decisions are reported back asynchronously so blocks show up in your TillShield dashboard.
Install
npm install @tillstack/shield-nodeRequires Node 18+ (Node 20+ recommended for built-in WebCrypto).
Set up
Mint a project edge key in the dashboard under Shield → Inline WAF
(tse_…). It's a server-side secret — keep it in an env var, never ship it to a
browser.
Express / Connect
import { createShield } from '@tillstack/shield-node'
const shield = createShield({ edgeKey: process.env.SHIELD_EDGE_KEY })
app.use(shield.express())Fastify
const shield = createShield({ edgeKey: process.env.SHIELD_EDGE_KEY })
fastify.addHook('onRequest', shield.fastifyHook)Raw http / anything else
const server = http.createServer(async (req, res) => {
if (await shield.handle(req, res)) return // shield served a block/challenge
// …your handler…
})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. |
| trustProxy | true | Read the client IP from the leftmost X-Forwarded-For. Set false behind no proxy. |
| countryHeader | — | Header carrying an ISO country code (e.g. 'cf-ipcountry') to enable country rules. |
| report | true | Report decisions back to TillDev. |
| onDecision | — | (result, req) => void — observe every non-allow decision. |
| defaultBlockBody | "Request blocked by TillShield." | Body when a rule sets none. |
How it decides
Rules run in ascending priority; the first match wins. Each rule matches on
any combination of: client IP (CIDR, IPv4/IPv6), URL path glob, HTTP method,
country, User-Agent regex, and a threat_intel flag (match when sha256(ip) is
in the shared deny list). A rule can also carry a rate limit (requests per
window, keyed by IP or IP+path) — it fires only once the limit is exceeded.
A matching rule's mode decides the outcome: block (serves its status, default
403), challenge (serves e.g. 429 with Retry-After), or log (allows through
but records the match).
Notes
- Fail-open by design. Any internal error, or an unreachable TillDev, results
in the request being allowed (unless
failMode: 'closed'). TillShield can't take your site down. - Rate limits are per-process. The built-in limiter counts within a single
Node instance. For a globally-consistent limit across many instances, front
your origin with
@tillstack/shield-cloudflareor the drop-in TillShield edge worker. - No raw IPs leave your server — decision reports carry
sha256(ip)only.
