You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c05d7d2
Browse filesBrowse the repository at this point in the historyBrowse files
feat(sdk): a typed decision, and characteristics for who the caller is
protect() returned { allowed, detection } and the adapters typed the value
they handed to onBlocked as `any`, so integrating meant destructuring a
blob and string-matching to find out which rule fired. Three things the
SDK already knew had nowhere to go:
- Which rules ran. The engine collapsed to one deciding rule plus a
violations array, so a filter rule that never ran for want of enrichment
was indistinguishable from one that ran and passed. Both reported ALLOW.
Every rule now appears in results[] with RUN / DRY_RUN / NOT_RUN /
CACHED, and a dry-run rule that matched reports DENY -- what it WOULD
have done is the reason you turned it on.
- "Challenge this one." We ship a proof-of-work captcha and the decision
type could not express it, so it was reachable only by hand.
- A stable id. 'rule_' + Date.now() is not unique under concurrency and
correlates with nothing.
characteristics answers a separate question the SDK had also hard-coded:
rate limits keyed on IP, which is the wrong subject for an authenticated
API and the wrong subject for us specifically, since the actor model
exists because IP is not identity. An absent characteristic falls back to
the IP rather than keying under empty -- otherwise one tenant's limit
takes out all anonymous traffic, invisibly.
Decision caching is deliberately narrow: server-derived DENY and CHALLENGE
only. Caching ALLOW is how a client that has since started misbehaving
keeps sailing through, and rule outcomes must not be cached because a rate
limiter has to see every request.
allowed is unchanged, including failing open on error. 327 tests pass, and
the any budgets drop (fastify to zero).
ClosesWebDecoy/app#730ClosesWebDecoy/app#731
Copy file name to clipboardExpand all lines: CHANGELOG.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
10
+
### Added
11
+
12
+
-**`protect()` returns a typed decision.** It used to return `{ allowed, detection }`, and the adapters typed the value handed to `onBlocked` as `any`.
13
+
-`conclusion: 'ALLOW' | 'DENY' | 'CHALLENGE' | 'ERROR'`, with `isAllowed()` / `isDenied()` / `isChallenged()` / `isErrored()` and `deniedBy(rule)`. `ERROR` is a distinct conclusion, so a caller can tell "allowed" from "never decided" — both still serve the request.
14
+
-`results` — every configured rule in evaluation order with a `state` of `RUN`, `DRY_RUN`, `NOT_RUN` or `CACHED`. `NOT_RUN` is new information: a `filter()` rule with no IP enrichment, or a `webBotAuth()` rule on a request with no host, used to report ALLOW, which reads as "checked and fine" rather than "never checked". A dry-run rule that matched now reports `conclusion: 'DENY'` with `state: 'DRY_RUN'`, rather than the ALLOW its action said.
15
+
-`id` — a random `dec_…` id, also stamped on `detection.detection_id`. The old `'rule_' + Date.now()` was not unique under concurrency and correlated with nothing.
16
+
-`onBlocked` receives the full decision as a trailing argument in all three adapters, and `detection` is typed. Existing handlers are unaffected.
17
+
-`allowed` is unchanged, including failing open on error, so existing middleware keeps working.
18
+
19
+
-**`characteristics`** — what the SDK treats as the same caller, for keyed rules and the decision cache. Defaults to `['ip']`; accepts `'path'`, `'method'`, `'userAgent'`, or a function over the rule context. A rule's own `keyBy` still wins. When a characteristic is absent the key falls back to the IP, rather than bucketing every request missing that field into one bucket — which is how a limit meant for one tenant takes out anonymous traffic site-wide.
20
+
21
+
-**Decision caching.** A server-derived `DENY` or `CHALLENGE` is reused for its TTL instead of re-asking the service about a caller it just answered for. Deliberately narrow: `ALLOW` is never cached (that is how a client that has since started misbehaving keeps sailing through, and it saves the cheap request), and rule outcomes are never cached (a rate limiter has to see every request, and a cached tripwire hit would stop the violation being reported). Configure with `decisionCache: { ttl, max }` or disable with `false`.
Copy file name to clipboardExpand all lines: README.md
+47-15Lines changed: 47 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -255,26 +255,58 @@ Additional local rules for the `rules` array. `filter()` requires an API key for
255
255
256
256
Local Web Bot Auth verification (RFC 9421). `webBotAuth()` returns a `Rule` that denies agent impersonation; `detectBot(request)` returns the verdict directly for custom handling. See the [Web Bot Auth guide](docs/verify-ai-agents-web-bot-auth.md). Exported types: `AgentVerdict`, `AgentStatus`, `AgentCategory`, `WebBotAuthConfig`, `AgentVerifierOptions`, `SignedAgentDirectory`.
0 commit comments