Adaptive auto-ban (fail2ban-style)
As of 0.37.0 DaloyJS ships autoBan() — a reusable, escalating, decaying ban primitive. Where loginThrottle() only protects credential-entry routes, autoBan() watches any response and temporarily bans a client that trips too many suspicious statuses (by default 401 / 403 / 429) inside a rolling window. Repeat offenders earn exponentially longer bans; the record decays once the client goes quiet, so a one-off burst is forgiven while a persistent attacker is locked out for progressively longer. It is dependency-free and runtime-portable.
Quick start
Mount it globally with app.use() so it observes every route. Because it reads the outgoing status, it counts failures produced by any downstream middleware or handler (auth rejections, rate-limit429s, your own 403s) — not just its own.
Identity is mandatory
autoBan() refuses to construct unless it can identify clients — pass a keyGenerator or set trustProxyHeaders: true. This is deliberate: a shared "global" bucket would let a single offender ban every caller at once. A request the key generator cannot attribute (returns undefined) is skipped — never counted, never banned.
How escalation & decay work
- Each watched response is a strike. Strikes accumulate inside
windowMs(default 10 min) and decay when the window passes. - Reaching
maxStrikes(default 5) issues a ban forbanMs(default 15 min). - With
escalate: true(default) each repeat ban doubles —banMs,2×,4×, … capped atmaxBanMs(default 24 h) — for as long as the record stays alive. - Once the client stops tripping statuses, the record expires and the escalation counter resets — the ban decays.
Responses
A banned request is rejected in beforeHandle before the handler runs. By default it returns 429 Too Many Requests with a Retry-After header and Cache-Control: no-store. Set banStatus: 403 for a 403 Forbidden with your own message instead.
Observability
Wire onBan and onStrike into your logger, alerting, or an external denylist feed:
Pluggable store (multi-instance)
The default store is in-memory and single-process. For a horizontally-scaled deployment, implement AutoBanStore (mirroring the rateLimit() store contract) against Redis or another shared backend so a ban applies across every instance:
Implementations must treat an entry past its ttlMs as absent so bans and escalation decay automatically. To lift a ban manually, call store.delete(key).
Sharing across route groups
Every autoBan() with the same groupId (default "auto-ban") shares one in-memory store, so a client banned on one group is banned on all of them — an attacker can't dodge the ban by rotating endpoints.