@osqd/bothandlerjs 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +516 -0
- package/LICENSE +102 -0
- package/README.md +308 -0
- package/bin/bothandlerjs.mjs +14 -0
- package/dist/actions/index.d.ts +22 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/adapters/fastify.d.ts +43 -0
- package/dist/adapters/fetch.d.ts +51 -0
- package/dist/adapters/index.cjs +723 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.js +692 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/koa.d.ts +27 -0
- package/dist/adapters/node.d.ts +29 -0
- package/dist/adapters/shared.d.ts +39 -0
- package/dist/audit.d.ts +172 -0
- package/dist/challenge/index.d.ts +176 -0
- package/dist/challenge/language.d.ts +57 -0
- package/dist/challenge/page.d.ts +42 -0
- package/dist/challenge/pow.d.ts +33 -0
- package/dist/challenge/token.d.ts +72 -0
- package/dist/cli.cjs +13388 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +13358 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/index.cjs +139 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +112 -0
- package/dist/client/index.js.map +1 -0
- package/dist/config.d.ts +316 -0
- package/dist/core.d.ts +443 -0
- package/dist/corpus/adversarial.d.ts +2 -0
- package/dist/corpus/advertising-email.d.ts +2 -0
- package/dist/corpus/ai-crawlers.d.ts +2 -0
- package/dist/corpus/benign-bots.d.ts +2 -0
- package/dist/corpus/cdn-gateways.d.ts +2 -0
- package/dist/corpus/cookies.d.ts +49 -0
- package/dist/corpus/crawlers-regional.d.ts +2 -0
- package/dist/corpus/crawlers-vertical.d.ts +2 -0
- package/dist/corpus/headers.d.ts +403 -0
- package/dist/corpus/humans-apps.d.ts +2 -0
- package/dist/corpus/humans-browsers.d.ts +2 -0
- package/dist/corpus/humans.d.ts +2 -0
- package/dist/corpus/index.cjs +4196 -0
- package/dist/corpus/index.cjs.map +1 -0
- package/dist/corpus/index.d.ts +37 -0
- package/dist/corpus/index.js +4132 -0
- package/dist/corpus/index.js.map +1 -0
- package/dist/corpus/infrastructure.d.ts +17 -0
- package/dist/corpus/libraries-extended.d.ts +2 -0
- package/dist/corpus/ranges.d.ts +31 -0
- package/dist/corpus/reputation.d.ts +21 -0
- package/dist/corpus/runner.d.ts +130 -0
- package/dist/corpus/schema.d.ts +185 -0
- package/dist/corpus/tooling.d.ts +2 -0
- package/dist/corpus/unwanted.d.ts +2 -0
- package/dist/crawler-ranges.d.ts +113 -0
- package/dist/dashboard/client.generated.d.ts +7 -0
- package/dist/dashboard/feed.d.ts +186 -0
- package/dist/dashboard/index.d.ts +5 -0
- package/dist/dashboard/page.d.ts +51 -0
- package/dist/dashboard/parse-request.d.ts +28 -0
- package/dist/dashboard/preview.d.ts +45 -0
- package/dist/dashboard/server.d.ts +70 -0
- package/dist/dashboard/types.d.ts +714 -0
- package/dist/detectors/accept-signature.d.ts +6 -0
- package/dist/detectors/browsing-coherence.d.ts +44 -0
- package/dist/detectors/cadence.d.ts +28 -0
- package/dist/detectors/clearance.d.ts +25 -0
- package/dist/detectors/client-hints.d.ts +19 -0
- package/dist/detectors/client-signals.d.ts +27 -0
- package/dist/detectors/crawl-breadth.d.ts +28 -0
- package/dist/detectors/crawler-verification.d.ts +39 -0
- package/dist/detectors/fetch-metadata.d.ts +20 -0
- package/dist/detectors/header-integrity.d.ts +26 -0
- package/dist/detectors/header-order.d.ts +27 -0
- package/dist/detectors/identity-rotation.d.ts +34 -0
- package/dist/detectors/index.d.ts +56 -0
- package/dist/detectors/ip-intelligence.d.ts +30 -0
- package/dist/detectors/known-bots.d.ts +94 -0
- package/dist/detectors/probe-signature.d.ts +50 -0
- package/dist/detectors/rate-anomaly.d.ts +36 -0
- package/dist/detectors/self-identified.d.ts +33 -0
- package/dist/detectors/session-integrity.d.ts +24 -0
- package/dist/detectors/tls-fingerprint.d.ts +34 -0
- package/dist/detectors/trap.d.ts +98 -0
- package/dist/detectors/types.d.ts +70 -0
- package/dist/detectors/ua-coherence.d.ts +32 -0
- package/dist/evidence.d.ts +45 -0
- package/dist/facts.d.ts +46 -0
- package/dist/index.cjs +9600 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +9444 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/async.d.ts +16 -0
- package/dist/internal/clock.d.ts +17 -0
- package/dist/internal/crypto.d.ts +26 -0
- package/dist/internal/dns.d.ts +100 -0
- package/dist/internal/emitter.d.ts +18 -0
- package/dist/internal/http.d.ts +22 -0
- package/dist/internal/ip.d.ts +73 -0
- package/dist/internal/lru.d.ts +34 -0
- package/dist/internal/matcher.d.ts +41 -0
- package/dist/internal/pattern.d.ts +14 -0
- package/dist/internal/ua.d.ts +92 -0
- package/dist/metrics.d.ts +120 -0
- package/dist/notify/hub.d.ts +70 -0
- package/dist/notify/index.d.ts +7 -0
- package/dist/notify/redact.d.ts +48 -0
- package/dist/notify/sinks.d.ts +62 -0
- package/dist/notify/types.d.ts +56 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/match.d.ts +20 -0
- package/dist/policy/policy.d.ts +123 -0
- package/dist/policy/presets.d.ts +161 -0
- package/dist/policy/types.d.ts +155 -0
- package/dist/robots.d.ts +83 -0
- package/dist/state.d.ts +161 -0
- package/dist/stores/index.d.ts +5 -0
- package/dist/stores/memory.d.ts +33 -0
- package/dist/stores/redis.d.ts +59 -0
- package/dist/stores/types.d.ts +37 -0
- package/dist/types.d.ts +250 -0
- package/docs/challenge/index.md +152 -0
- package/docs/challenge/localisation.md +105 -0
- package/docs/concepts/actors.md +113 -0
- package/docs/concepts/evidence.md +133 -0
- package/docs/concepts/the-guard.md +121 -0
- package/docs/concepts/threat-model.md +120 -0
- package/docs/concepts/verdicts.md +108 -0
- package/docs/course/01-first-assessment.md +178 -0
- package/docs/course/02-proof-and-suspicion.md +164 -0
- package/docs/course/03-verdicts-and-scores.md +160 -0
- package/docs/course/04-the-guard.md +200 -0
- package/docs/course/05-detectors.md +178 -0
- package/docs/course/06-identity.md +161 -0
- package/docs/course/07-actors.md +178 -0
- package/docs/course/08-traps.md +183 -0
- package/docs/course/09-rules.md +224 -0
- package/docs/course/10-actions-and-presets.md +216 -0
- package/docs/course/11-the-challenge.md +227 -0
- package/docs/course/12-going-live.md +211 -0
- package/docs/course/13-operating-it.md +238 -0
- package/docs/course/14-scaling.md +195 -0
- package/docs/course/15-extending.md +259 -0
- package/docs/course/16-proving-it.md +266 -0
- package/docs/course/index.md +112 -0
- package/docs/design/decisions.md +216 -0
- package/docs/detection/client-signals.md +96 -0
- package/docs/detection/detectors.md +376 -0
- package/docs/detection/index.md +134 -0
- package/docs/detection/signatures.md +111 -0
- package/docs/detection/verification.md +123 -0
- package/docs/detection/writing-a-detector.md +145 -0
- package/docs/index.md +100 -0
- package/docs/integration/adapters.md +138 -0
- package/docs/integration/client-ip.md +114 -0
- package/docs/integration/index.md +55 -0
- package/docs/integration/stores.md +76 -0
- package/docs/operations/audit.md +116 -0
- package/docs/operations/dashboard.md +725 -0
- package/docs/operations/index.md +101 -0
- package/docs/operations/metrics.md +74 -0
- package/docs/operations/notifications.md +72 -0
- package/docs/operations/runtime-changes.md +118 -0
- package/docs/policy/actions.md +196 -0
- package/docs/policy/index.md +84 -0
- package/docs/policy/presets.md +191 -0
- package/docs/policy/robots.md +128 -0
- package/docs/policy/rules.md +134 -0
- package/docs/reference/api.md +126 -0
- package/docs/reference/configuration.md +175 -0
- package/docs/start/choosing-a-policy.md +127 -0
- package/docs/start/first-integration.md +127 -0
- package/docs/start/installation.md +63 -0
- package/docs/start/upgrading.md +72 -0
- package/docs/testing/cli.md +119 -0
- package/docs/testing/corpus.md +129 -0
- package/docs/testing/index.md +61 -0
- package/docs/testing/replay.md +102 -0
- package/docs/testing/try-it.md +189 -0
- package/package.json +145 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BotEvent } from "./types.js";
|
|
2
|
+
export interface RedactionOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Replace client addresses with a coarse network (`/24`, `/64`) before the event
|
|
5
|
+
* leaves the process. Default true.
|
|
6
|
+
*
|
|
7
|
+
* On by default because a notification sink is usually a third party — Slack, a
|
|
8
|
+
* webhook, an aggregator — and an IP address is personal data in most of the world.
|
|
9
|
+
* The network is enough to recognise a pattern and to correlate repeat offenders,
|
|
10
|
+
* which is what an alert is for; the exact address is only needed when you are
|
|
11
|
+
* ready to act on one, and that belongs in your own logs.
|
|
12
|
+
*/
|
|
13
|
+
maskIp?: boolean;
|
|
14
|
+
/** Drop the full User-Agent, keeping only the parsed shape. Default false. */
|
|
15
|
+
dropUserAgent?: boolean;
|
|
16
|
+
/** Headers never included in an outbound event, whatever else is configured. */
|
|
17
|
+
neverSend?: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* Replace query-string *values* with a placeholder, keeping the parameter names.
|
|
20
|
+
* Default true.
|
|
21
|
+
*
|
|
22
|
+
* A query string is where password-reset tokens, invitation links, email addresses
|
|
23
|
+
* and session ids actually live, and it was the one client-supplied structure this
|
|
24
|
+
* module passed through untouched while stripping cookies and credential headers
|
|
25
|
+
* beside it. The names are what make an alert legible — a burst of `?export=` is the
|
|
26
|
+
* pattern worth seeing — and the values are what you do not want in a third party's
|
|
27
|
+
* message history.
|
|
28
|
+
*/
|
|
29
|
+
maskQuery?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Always stripped. Each of these carries a credential or a session. */
|
|
32
|
+
/**
|
|
33
|
+
* Always stripped, wherever a request is shown to somebody other than the engine.
|
|
34
|
+
*
|
|
35
|
+
* Exported because the dashboard's header inspector needs the same list: a panel that
|
|
36
|
+
* prints the request as it arrived would otherwise print session cookies and bearer
|
|
37
|
+
* tokens onto an operator's screen, and into whatever screenshot they paste into a
|
|
38
|
+
* ticket.
|
|
39
|
+
*/
|
|
40
|
+
export declare const CREDENTIAL_HEADERS: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Produces the version of an event that is safe to send somewhere else.
|
|
43
|
+
*
|
|
44
|
+
* This runs on the way *out*, not on the way in: detection sees everything, and only
|
|
45
|
+
* the copy handed to a sink is reduced. Getting that order wrong would trade
|
|
46
|
+
* detection quality for a privacy property you can have for free.
|
|
47
|
+
*/
|
|
48
|
+
export declare function redactEvent(event: BotEvent, options?: RedactionOptions): BotEvent;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Notifier } from "./types.js";
|
|
2
|
+
export interface ConsoleNotifierOptions {
|
|
3
|
+
/** `"pretty"` for humans, `"json"` for a log pipeline. Default `"pretty"`. */
|
|
4
|
+
format?: "pretty" | "json";
|
|
5
|
+
/** Where to write. Default `console`. */
|
|
6
|
+
target?: Pick<Console, "log" | "warn" | "error">;
|
|
7
|
+
}
|
|
8
|
+
/** Writes events to the console. The default sink, and the right one to start with. */
|
|
9
|
+
export declare function consoleNotifier(options?: ConsoleNotifierOptions): Notifier;
|
|
10
|
+
export interface WebhookNotifierOptions {
|
|
11
|
+
url: string;
|
|
12
|
+
/**
|
|
13
|
+
* Shared secret. When set, each request carries `X-BotHandler-Signature` as
|
|
14
|
+
* `sha256=<hex-free base64url HMAC>` over the exact body, plus a timestamp header.
|
|
15
|
+
*
|
|
16
|
+
* Sign your webhooks. An unsigned endpoint accepting bot alerts is an endpoint
|
|
17
|
+
* anyone on the internet can fill with fabricated ones.
|
|
18
|
+
*/
|
|
19
|
+
secret?: string;
|
|
20
|
+
/** Extra headers, e.g. an API token. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
/** Per-attempt timeout, ms. Default 5000. */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
/** Retries after the first attempt. Default 2, with exponential backoff. */
|
|
25
|
+
retries?: number;
|
|
26
|
+
/** `fetch` implementation. Defaults to the global. */
|
|
27
|
+
fetch?: typeof globalThis.fetch;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* POSTs events as JSON.
|
|
31
|
+
*
|
|
32
|
+
* Delivery is best-effort by design. This runs outside the request path, so a slow or
|
|
33
|
+
* unreachable endpoint costs a visitor nothing — but it also means an alert can be
|
|
34
|
+
* lost, and it is not a substitute for logging events where you can query them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function webhookNotifier(options: WebhookNotifierOptions): Notifier;
|
|
37
|
+
export interface SlackNotifierOptions {
|
|
38
|
+
/** Incoming-webhook URL. */
|
|
39
|
+
url: string;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
fetch?: typeof globalThis.fetch;
|
|
42
|
+
}
|
|
43
|
+
/** Posts a compact, readable message to a Slack incoming webhook. */
|
|
44
|
+
export declare function slackNotifier(options: SlackNotifierOptions): Notifier;
|
|
45
|
+
export interface NotifyJsOptions {
|
|
46
|
+
/** Base URL of your NotifyJS hub. */
|
|
47
|
+
endpoint: string;
|
|
48
|
+
/** Hub API token. */
|
|
49
|
+
token: string;
|
|
50
|
+
/** Channel or topic to publish under. Default "bothandler". */
|
|
51
|
+
topic?: string;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
fetch?: typeof globalThis.fetch;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Publishes to a NotifyJS hub, so bot alerts arrive on the same devices as the rest
|
|
57
|
+
* of your operational notifications.
|
|
58
|
+
*
|
|
59
|
+
* Written against the hub's HTTP surface rather than importing the client, so this
|
|
60
|
+
* adds no dependency and works whichever version of the hub you run.
|
|
61
|
+
*/
|
|
62
|
+
export declare function notifyJsNotifier(options: NotifyJsOptions): Notifier;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Assessment } from "../types.js";
|
|
2
|
+
import type { Decision } from "../policy/types.js";
|
|
3
|
+
import type { TrafficAnomaly } from "../audit.js";
|
|
4
|
+
/** Why a notification was raised. */
|
|
5
|
+
export type BotEventType =
|
|
6
|
+
/** An assessment concluded something other than "unknown". */
|
|
7
|
+
"detection"
|
|
8
|
+
/** An action was taken that withheld or altered the response. */
|
|
9
|
+
| "action"
|
|
10
|
+
/** The safety guard replaced a terminal action with something recoverable. */
|
|
11
|
+
| "downgrade"
|
|
12
|
+
/** A detector, sink or store failed. Operational, not about traffic. */
|
|
13
|
+
| "error"
|
|
14
|
+
/**
|
|
15
|
+
* The audit noticed the *shape* of your traffic change — a spike in automation, a
|
|
16
|
+
* collapse in human traffic, a policy suddenly denying far more than usual.
|
|
17
|
+
*
|
|
18
|
+
* The one event type that is not about a single request, which is why
|
|
19
|
+
* {@link BotEvent.assessment} is optional.
|
|
20
|
+
*/
|
|
21
|
+
| "anomaly";
|
|
22
|
+
export interface BotEvent {
|
|
23
|
+
type: BotEventType;
|
|
24
|
+
/** ISO-8601 timestamp. */
|
|
25
|
+
at: string;
|
|
26
|
+
/** The request this is about. Absent on `anomaly`, which is about a stretch of time. */
|
|
27
|
+
assessment?: Assessment | undefined;
|
|
28
|
+
decision?: Decision | undefined;
|
|
29
|
+
/** Present on `error` events. */
|
|
30
|
+
error?: {
|
|
31
|
+
source: string;
|
|
32
|
+
message: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
/** Present on `anomaly` events. */
|
|
35
|
+
anomaly?: TrafficAnomaly | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface Notifier {
|
|
38
|
+
id: string;
|
|
39
|
+
/**
|
|
40
|
+
* Delivers an event.
|
|
41
|
+
*
|
|
42
|
+
* Called off the request path — the engine never awaits a sink — so taking time
|
|
43
|
+
* here is fine. Throwing is also fine: failures are caught, reported once through
|
|
44
|
+
* `onError`, and never surfaced to the client.
|
|
45
|
+
*/
|
|
46
|
+
notify(event: BotEvent): void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** Controls what a sink is told about. */
|
|
49
|
+
export interface NotifyFilter {
|
|
50
|
+
/** Event types to deliver. Default: everything except `detection`. */
|
|
51
|
+
types?: readonly BotEventType[];
|
|
52
|
+
/** Minimum score for a `detection` event. Default 60. */
|
|
53
|
+
minScore?: number;
|
|
54
|
+
/** Only deliver proven detections. Default false. */
|
|
55
|
+
certainOnly?: boolean;
|
|
56
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Policy } from "./policy.js";
|
|
2
|
+
export type { GuardSettings } from "./policy.js";
|
|
3
|
+
export { compileMatch, independentStrongSignals } from "./match.js";
|
|
4
|
+
export { monitorOnly, allowCrawlers, protectContent, declineAiTraining, protectData, protectApi, protectAuth, underAttack, PRESETS } from "./presets.js";
|
|
5
|
+
export type { PresetName } from "./presets.js";
|
|
6
|
+
export { ACTION_NAMES, TERMINAL_ACTIONS } from "./types.js";
|
|
7
|
+
export type { ActionName, ActionParams, Decision, FalsePositivePolicy, MatchSpec, PolicyOptions, Rule } from "./types.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Assessment } from "../types.js";
|
|
2
|
+
import type { MatchSpec } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compiles a {@link MatchSpec} into a predicate.
|
|
5
|
+
*
|
|
6
|
+
* Compiling once at policy-construction time rather than interpreting the spec per
|
|
7
|
+
* request keeps rule evaluation to a handful of set lookups, which matters when the
|
|
8
|
+
* policy is consulted on every request to the site.
|
|
9
|
+
*/
|
|
10
|
+
export declare function compileMatch(spec: MatchSpec): (assessment: Assessment) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Counts independent `strong`-or-better bot signals.
|
|
13
|
+
*
|
|
14
|
+
* "Independent" is approximated as "from different detectors", which is imperfect —
|
|
15
|
+
* `header-integrity` and `accept-signature` both read the same header block and are
|
|
16
|
+
* correlated — but it is a real bar that a single misfiring check cannot clear on its
|
|
17
|
+
* own. Used by the `balanced` guard, where it is the difference between "the score
|
|
18
|
+
* got high" and "several different things went wrong".
|
|
19
|
+
*/
|
|
20
|
+
export declare function independentStrongSignals(assessment: Assessment): number;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Assessment } from "../types.js";
|
|
2
|
+
import type { ActionName, Decision, FalsePositivePolicy, PolicyOptions, Rule } from "./types.js";
|
|
3
|
+
/** The settings that decide how far a rule may go. See {@link Policy.replaceGuard}. */
|
|
4
|
+
export interface GuardSettings {
|
|
5
|
+
falsePositivePolicy: FalsePositivePolicy;
|
|
6
|
+
fallbackAction: ActionName;
|
|
7
|
+
defaultAction: ActionName;
|
|
8
|
+
terminalScoreThreshold: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Chooses an action for an assessment, then refuses to let that action be harsher
|
|
12
|
+
* than the evidence supports.
|
|
13
|
+
*
|
|
14
|
+
* The second half is the point. Rules are ordinary first-match-wins configuration and
|
|
15
|
+
* there is nothing clever about them. What makes the policy trustworthy is the guard
|
|
16
|
+
* that runs *after* a rule has been selected: under the default `strict` mode a
|
|
17
|
+
* terminal action survives only if `assessment.certain` is true, and otherwise is
|
|
18
|
+
* replaced by something recoverable, with the substitution recorded on the decision.
|
|
19
|
+
*
|
|
20
|
+
* That ordering is deliberate. The guard cannot be forgotten in a rule, cannot be
|
|
21
|
+
* bypassed by a cleverly-worded predicate, and does not depend on whoever wrote the
|
|
22
|
+
* rules understanding the certainty model. Turning it off is a single, explicit,
|
|
23
|
+
* greppable configuration change.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Policy {
|
|
26
|
+
/**
|
|
27
|
+
* Compiled rules, in evaluation order.
|
|
28
|
+
*
|
|
29
|
+
* Mutable — and only ever replaced wholesale, never spliced. {@link replaceRules}
|
|
30
|
+
* swaps the array in one assignment, so a request evaluated during an update sees
|
|
31
|
+
* either the old list or the new one and never a half-applied policy.
|
|
32
|
+
*/
|
|
33
|
+
private compiled;
|
|
34
|
+
/**
|
|
35
|
+
* The guard settings.
|
|
36
|
+
*
|
|
37
|
+
* Not `readonly` any more, and the reason is narrow enough to write down.
|
|
38
|
+
* {@link replaceGuard} exists so that an operator who has been *given* the power to
|
|
39
|
+
* change these can change them without a deploy — the dashboard gates it behind a
|
|
40
|
+
* control flag of its own that is off by default. Everything about how they are
|
|
41
|
+
* *used* is unchanged: `decide()` reads them on every request, so a change takes
|
|
42
|
+
* effect on the next one, and it can only ever be a whole valid set replacing
|
|
43
|
+
* another whole valid set.
|
|
44
|
+
*/
|
|
45
|
+
private defaultAction;
|
|
46
|
+
private defaultParams;
|
|
47
|
+
private mode;
|
|
48
|
+
private fallbackAction;
|
|
49
|
+
private terminalScoreThreshold;
|
|
50
|
+
private readonly onDowngrade;
|
|
51
|
+
constructor(options?: PolicyOptions);
|
|
52
|
+
private static compile;
|
|
53
|
+
/** Ids of every configured rule, in evaluation order. For diagnostics and tests. */
|
|
54
|
+
get ruleIds(): string[];
|
|
55
|
+
/**
|
|
56
|
+
* The rule definitions themselves, in evaluation order.
|
|
57
|
+
*
|
|
58
|
+
* Exposed because two things outside the policy need to *read* rules rather than
|
|
59
|
+
* apply them: `robotsFromRules`, which turns the ones that deny crawlers into a
|
|
60
|
+
* `robots.txt`, and the dashboard, which shows an operator what is actually
|
|
61
|
+
* installed. Both want the spec as written, not the compiled predicate.
|
|
62
|
+
*/
|
|
63
|
+
get rules(): readonly Rule[];
|
|
64
|
+
/**
|
|
65
|
+
* Replaces the rule list while the process runs.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately narrow: this changes *which* rules exist, and nothing about how far
|
|
68
|
+
* they are allowed to go. The guard settings are a separate, separately-gated
|
|
69
|
+
* operation — see {@link replaceGuard} — so that "edit the rules" and "change what a
|
|
70
|
+
* rule is allowed to do" are never the same permission.
|
|
71
|
+
*/
|
|
72
|
+
replaceRules(rules: readonly Rule[]): void;
|
|
73
|
+
/**
|
|
74
|
+
* Replaces the guard settings while the process runs.
|
|
75
|
+
*
|
|
76
|
+
* This is the one edit that can start denying people, so it is separated from
|
|
77
|
+
* {@link replaceRules} at every level: a different method here, a different control
|
|
78
|
+
* flag on the dashboard, a different endpoint, and a different event when it lands.
|
|
79
|
+
* A caller that may change rules does not thereby become a caller that may change
|
|
80
|
+
* this.
|
|
81
|
+
*
|
|
82
|
+
* Two things are refused outright rather than warned about, because both turn the
|
|
83
|
+
* guard into scenery while leaving it apparently switched on:
|
|
84
|
+
*
|
|
85
|
+
* - **A terminal `fallbackAction`.** The fallback is what a downgrade *becomes*.
|
|
86
|
+
* Set it to `block` and every downgrade blocks, which is the exact outcome the
|
|
87
|
+
* downgrade exists to prevent — and the decision would still be recorded as a
|
|
88
|
+
* guard stop, so the metric that is supposed to catch this would report success.
|
|
89
|
+
* - **A `terminalScoreThreshold` outside 1–100.** Zero would let balanced mode
|
|
90
|
+
* deny on any score at all.
|
|
91
|
+
*
|
|
92
|
+
* Fields left undefined keep their current value, so a caller can move one setting
|
|
93
|
+
* without restating the rest.
|
|
94
|
+
*/
|
|
95
|
+
replaceGuard(settings: Partial<GuardSettings>): void;
|
|
96
|
+
/** Just the guard half of {@link describe}, in the shape {@link replaceGuard} accepts back. */
|
|
97
|
+
describeGuard(): GuardSettings;
|
|
98
|
+
/**
|
|
99
|
+
* The settings that decide how far a rule may go, for anything that reports on the
|
|
100
|
+
* policy rather than applying it — a dashboard, a diagnostic, a startup log.
|
|
101
|
+
*
|
|
102
|
+
* Worth surfacing rather than keeping private: `falsePositivePolicy` is the single
|
|
103
|
+
* setting that decides whether an unproven verdict can deny anybody, and a reader
|
|
104
|
+
* looking at a screen full of verdicts cannot interpret one without knowing it.
|
|
105
|
+
*/
|
|
106
|
+
describe(): {
|
|
107
|
+
falsePositivePolicy: FalsePositivePolicy;
|
|
108
|
+
fallbackAction: ActionName;
|
|
109
|
+
defaultAction: ActionName;
|
|
110
|
+
terminalScoreThreshold: number;
|
|
111
|
+
rules: string[];
|
|
112
|
+
};
|
|
113
|
+
decide(assessment: Assessment): Decision;
|
|
114
|
+
/**
|
|
115
|
+
* The safety guard.
|
|
116
|
+
*
|
|
117
|
+
* Note what it does *not* consider: how confident the score is, how many detectors
|
|
118
|
+
* fired, or how badly the operator wants to block. Under `strict` there is exactly
|
|
119
|
+
* one question — is there proof? — and everything else is a downgrade.
|
|
120
|
+
*/
|
|
121
|
+
private guard;
|
|
122
|
+
private downgrade;
|
|
123
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Rule } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ready-made rule sets.
|
|
4
|
+
*
|
|
5
|
+
* Each is a starting point to read, adapt and own — not a black box. Every rule
|
|
6
|
+
* carries an id, so you can see exactly which one fired in a decision and replace
|
|
7
|
+
* just that one.
|
|
8
|
+
*
|
|
9
|
+
* All of them assume the default `strict` guard, which means even the harshest rule
|
|
10
|
+
* here cannot deny an unproven request. Choose a preset for the *shape* of the policy;
|
|
11
|
+
* the guard decides how far it is allowed to go.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Watch and learn. Nothing is ever withheld from anybody.
|
|
15
|
+
*
|
|
16
|
+
* Run this first, for at least a week, on real traffic. Every bot policy that has
|
|
17
|
+
* ever caused an outage was deployed straight to enforcement by someone who was sure
|
|
18
|
+
* they knew what their traffic looked like.
|
|
19
|
+
*/
|
|
20
|
+
export declare function monitorOnly(): Rule[];
|
|
21
|
+
/**
|
|
22
|
+
* Sensible defaults for a public content site.
|
|
23
|
+
*
|
|
24
|
+
* Keeps the crawlers that bring traffic, slows the ones that only take it, challenges
|
|
25
|
+
* what is probably automated, and blocks only what has proven itself.
|
|
26
|
+
*/
|
|
27
|
+
export declare function protectContent(): Rule[];
|
|
28
|
+
/**
|
|
29
|
+
* For an application whose value is in its data — pricing, listings, inventory.
|
|
30
|
+
*
|
|
31
|
+
* The distinguishing move is that AI and SEO crawlers are challenged rather than
|
|
32
|
+
* allowed. That is a business decision rather than a security one, and it is exactly
|
|
33
|
+
* the kind of decision that should be a visible rule you can point at.
|
|
34
|
+
*/
|
|
35
|
+
export declare function protectData(): Rule[];
|
|
36
|
+
/**
|
|
37
|
+
* For login, signup, password reset, checkout and anything else where automation is
|
|
38
|
+
* expensive to be wrong about in both directions.
|
|
39
|
+
*
|
|
40
|
+
* **Mount this on those routes only.** Applied site-wide it blocks your payment
|
|
41
|
+
* webhooks, your own server-side renderer and every honest crawler you have — all of
|
|
42
|
+
* which are proven automation, which is exactly what this preset refuses. That is
|
|
43
|
+
* correct behaviour on a login form and an outage anywhere else; the traffic corpus
|
|
44
|
+
* catches it, and it is the kind of mistake whose symptom is a support ticket about
|
|
45
|
+
* missing orders three days later.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* app.use("/login", botHandler(authDetector));
|
|
49
|
+
* app.use("/checkout", botHandler(authDetector));
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* The unusual choice here is `delay` on merely-suspected traffic. A quarter of a
|
|
53
|
+
* second is imperceptible to a person filling in a form and ruinous to a credential
|
|
54
|
+
* stuffer working through a list — and, unlike a challenge, it excludes nobody.
|
|
55
|
+
*/
|
|
56
|
+
export declare function protectAuth(): Rule[];
|
|
57
|
+
/**
|
|
58
|
+
* For a publisher whose problem is being *found*, not being scraped.
|
|
59
|
+
*
|
|
60
|
+
* The distinguishing move is that it allows more than it stops, explicitly and by
|
|
61
|
+
* name. Verified crawlers are allowed ahead of everything; declared benign
|
|
62
|
+
* automation — link unfurlers, feed readers, uptime monitors, the specialist search
|
|
63
|
+
* fleets — is allowed rather than merely tolerated; and the only things refused are
|
|
64
|
+
* the three that are refused everywhere, because they are proven and cannot be
|
|
65
|
+
* anything else.
|
|
66
|
+
*
|
|
67
|
+
* Reach for this when a bot policy has already cost you traffic, or when the site's
|
|
68
|
+
* whole purpose is to be indexed, quoted and shared. The cost is honest: bulk
|
|
69
|
+
* extraction gets rate-limited rather than challenged, so a determined scraper will
|
|
70
|
+
* get your content. On a site that wants to be read, that was always going to be
|
|
71
|
+
* true; what this preset refuses to do is trade away your search traffic to make it
|
|
72
|
+
* slightly less true.
|
|
73
|
+
*/
|
|
74
|
+
export declare function allowCrawlers(): Rule[];
|
|
75
|
+
/**
|
|
76
|
+
* Keep the search engines. Decline the model trainers.
|
|
77
|
+
*
|
|
78
|
+
* This is the policy question of the moment, and it is a *business* decision rather
|
|
79
|
+
* than a security one — which is exactly the kind of decision this library thinks
|
|
80
|
+
* should be a visible rule you can point at rather than a threshold somebody tuned.
|
|
81
|
+
*
|
|
82
|
+
* The split it draws is the one the AI crawlers themselves publish. A crawler
|
|
83
|
+
* collecting a training corpus and a crawler fetching one page because a person asked
|
|
84
|
+
* a question about it are different jobs, often from the same operator under different
|
|
85
|
+
* product tokens, and a policy that cannot tell them apart either feeds the trainers or
|
|
86
|
+
* breaks the citations. Here the training fleet is declined and everything else is
|
|
87
|
+
* served.
|
|
88
|
+
*
|
|
89
|
+
* **`robots.txt` is the primary mechanism, not this.** The crawlers named here honour
|
|
90
|
+
* it, and a rule that blocks a crawler nobody told is a rule that produces load and no
|
|
91
|
+
* compliance. Generate the file from this policy — `robotsFromRules(declineAiTraining())`
|
|
92
|
+
* — publish it, and treat these rules as what happens to the ones that ignore it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function declineAiTraining(): Rule[];
|
|
95
|
+
/**
|
|
96
|
+
* For a JSON API rather than a site.
|
|
97
|
+
*
|
|
98
|
+
* One difference drives the whole shape of this preset: **a challenge is useless
|
|
99
|
+
* here.** A proof-of-work interstitial is solved by a browser running JavaScript, and
|
|
100
|
+
* an API client is not one. Challenging your customers' integrations does not slow an
|
|
101
|
+
* attacker down; it breaks the integrations and leaves the attacker to solve it once
|
|
102
|
+
* in a headless browser. So the escalation ladder is rate limiting, and the terminal
|
|
103
|
+
* actions stay where they always are — on proof.
|
|
104
|
+
*
|
|
105
|
+
* The second difference is about what bot detection is *for* on an API. Your
|
|
106
|
+
* authentication is the control that matters, and it already knows who the caller is.
|
|
107
|
+
* This preset therefore tags everything, so your own handlers can combine a verdict
|
|
108
|
+
* with a key, a plan and a quota; it does not try to be the access control.
|
|
109
|
+
*
|
|
110
|
+
* A bare HTTP client is not suspicious here — it is the normal case, and the rule that
|
|
111
|
+
* challenges one on a content site is deliberately absent.
|
|
112
|
+
*/
|
|
113
|
+
export declare function protectApi(): Rule[];
|
|
114
|
+
/**
|
|
115
|
+
* For while it is happening.
|
|
116
|
+
*
|
|
117
|
+
* A deliberately impatient posture for an incident: a scrape in progress, a stuffing
|
|
118
|
+
* run, a scanner sweeping the range. Everything proven is refused, everything
|
|
119
|
+
* suspected is challenged at a much lower bar than usual, and *everyone* — including
|
|
120
|
+
* the people — is rate-limited, because a uniform ceiling is the one mitigation that
|
|
121
|
+
* cannot single anybody out.
|
|
122
|
+
*
|
|
123
|
+
* Three things to be clear about before turning it on.
|
|
124
|
+
*
|
|
125
|
+
* **It is temporary.** Nothing here is a good permanent policy: the low challenge
|
|
126
|
+
* threshold will interrupt real people on unusual browsers, and the uniform rate limit
|
|
127
|
+
* will interrupt your keenest readers. Turn it on during an incident, turn it off
|
|
128
|
+
* afterwards, and put it behind a switch you can flip without a deploy —
|
|
129
|
+
* `updatePolicy()` and the dashboard's editor exist for exactly this.
|
|
130
|
+
*
|
|
131
|
+
* **It still cannot deny anyone on a guess.** The guard applies here as everywhere
|
|
132
|
+
* else: the `challenge` on suspicion is a challenge because that is as far as
|
|
133
|
+
* unproven evidence may go, and asking for a block instead would simply be refused
|
|
134
|
+
* and recorded. An incident is precisely when people reach for `falsePositivePolicy:
|
|
135
|
+
* "aggressive"`, and precisely when the population getting caught is at its most
|
|
136
|
+
* unusual.
|
|
137
|
+
*
|
|
138
|
+
* **It is not DDoS protection.** This runs inside your process, after the connection
|
|
139
|
+
* has been accepted. Volume that hurts you at the network layer needs handling at the
|
|
140
|
+
* network layer; what this reduces is the *usefulness* of the traffic to whoever is
|
|
141
|
+
* sending it.
|
|
142
|
+
*
|
|
143
|
+
* And one practical warning the traffic corpus makes concrete: like `protect-auth`,
|
|
144
|
+
* this refuses proven automation, so **your own webhooks, health probes and
|
|
145
|
+
* server-side renderer are refused too** — thirteen of the corpus's infrastructure
|
|
146
|
+
* cases are, under this policy. Allowlist their addresses *before* you switch it on,
|
|
147
|
+
* not during the incident when you notice.
|
|
148
|
+
*/
|
|
149
|
+
export declare function underAttack(): Rule[];
|
|
150
|
+
/** Every preset by name, for config-driven setups. */
|
|
151
|
+
export declare const PRESETS: {
|
|
152
|
+
readonly "monitor-only": typeof monitorOnly;
|
|
153
|
+
readonly "allow-crawlers": typeof allowCrawlers;
|
|
154
|
+
readonly "protect-content": typeof protectContent;
|
|
155
|
+
readonly "decline-ai-training": typeof declineAiTraining;
|
|
156
|
+
readonly "protect-data": typeof protectData;
|
|
157
|
+
readonly "protect-api": typeof protectApi;
|
|
158
|
+
readonly "protect-auth": typeof protectAuth;
|
|
159
|
+
readonly "under-attack": typeof underAttack;
|
|
160
|
+
};
|
|
161
|
+
export type PresetName = keyof typeof PRESETS;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Assessment, BotClass, Verdict } from "../types.js";
|
|
2
|
+
import type { BotCategory } from "../detectors/known-bots.js";
|
|
3
|
+
/**
|
|
4
|
+
* What the engine does about an assessment.
|
|
5
|
+
*
|
|
6
|
+
* Ordered roughly by how much they cost a client that turns out to be a person:
|
|
7
|
+
*
|
|
8
|
+
* - `allow` — nothing at all. The verdict is still emitted for logging.
|
|
9
|
+
* - `tag` — serve normally, attach headers describing the verdict so a downstream
|
|
10
|
+
* service (your app, a cache, an edge worker) can decide for itself.
|
|
11
|
+
* - `log` — serve normally and raise a notification. Costs the client nothing.
|
|
12
|
+
* - `delay` — serve normally, but slowly. Invisible to a person, expensive at scale.
|
|
13
|
+
* - `rate-limit` — serve until a threshold, then reject with `429` and `Retry-After`.
|
|
14
|
+
* Recovers on its own.
|
|
15
|
+
* - `challenge` — withhold the response until the client passes a check. Recoverable
|
|
16
|
+
* by the client itself, which is what makes it the safe escalation. It does still
|
|
17
|
+
* shut out anyone without JavaScript, so it is not free.
|
|
18
|
+
* - `redirect` — send the client somewhere else.
|
|
19
|
+
* - `block` — refuse, with a status and a body explaining it.
|
|
20
|
+
* - `drop` — close the connection without a response.
|
|
21
|
+
*
|
|
22
|
+
* `redirect`, `block` and `drop` are **terminal**: the client is denied and cannot
|
|
23
|
+
* recover on its own. Those are the three the safety guard governs.
|
|
24
|
+
*/
|
|
25
|
+
export type ActionName = "allow" | "tag" | "log" | "delay" | "rate-limit" | "challenge" | "redirect" | "block" | "drop" | "custom";
|
|
26
|
+
/** Every action, ordered by how much it costs a client that turns out to be a person. */
|
|
27
|
+
export declare const ACTION_NAMES: readonly ActionName[];
|
|
28
|
+
/** Actions a client cannot recover from by itself. The guard's whole concern. */
|
|
29
|
+
export declare const TERMINAL_ACTIONS: ReadonlySet<ActionName>;
|
|
30
|
+
export interface ActionParams {
|
|
31
|
+
/** Status for `block`. Default 403. */
|
|
32
|
+
status?: number | undefined;
|
|
33
|
+
/** Response body for `block`. Defaults to a short plain-text explanation. */
|
|
34
|
+
body?: string | undefined;
|
|
35
|
+
contentType?: string | undefined;
|
|
36
|
+
/** Target for `redirect`. Must be same-origin or an absolute URL you control. */
|
|
37
|
+
location?: string | undefined;
|
|
38
|
+
/** Milliseconds for `delay`. */
|
|
39
|
+
delayMs?: number | undefined;
|
|
40
|
+
/** Ceiling for `rate-limit`. */
|
|
41
|
+
limit?: {
|
|
42
|
+
max: number;
|
|
43
|
+
windowMs: number;
|
|
44
|
+
} | undefined;
|
|
45
|
+
/** Extra response headers, merged after the engine's own. */
|
|
46
|
+
headers?: Record<string, string> | undefined;
|
|
47
|
+
/** Handler id for `custom`. */
|
|
48
|
+
handler?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
export interface Decision {
|
|
51
|
+
action: ActionName;
|
|
52
|
+
/** Id of the rule that produced it, or `"default"`. */
|
|
53
|
+
rule: string;
|
|
54
|
+
/** Sentence explaining the choice, safe to log and to show an operator. */
|
|
55
|
+
reason: string;
|
|
56
|
+
params: ActionParams;
|
|
57
|
+
/**
|
|
58
|
+
* Set when the safety guard replaced a stronger action. Both the original and the
|
|
59
|
+
* substitute are recorded, because a policy silently doing less than it says is
|
|
60
|
+
* worse than one that refuses loudly.
|
|
61
|
+
*/
|
|
62
|
+
downgradedFrom?: ActionName | undefined;
|
|
63
|
+
/** Why the downgrade happened. */
|
|
64
|
+
downgradeReason?: string | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Declarative matcher. Every field present must match; absent fields are ignored. */
|
|
67
|
+
export interface MatchSpec {
|
|
68
|
+
verdict?: Verdict | readonly Verdict[];
|
|
69
|
+
botClass?: BotClass | readonly BotClass[];
|
|
70
|
+
/** Established or claimed identity, e.g. `"googlebot"`. */
|
|
71
|
+
identity?: string | readonly string[];
|
|
72
|
+
/** Category of the matched signature, e.g. `"ai"`, `"seo"`. */
|
|
73
|
+
category?: BotCategory | readonly BotCategory[];
|
|
74
|
+
/**
|
|
75
|
+
* Require (or forbid) proven evidence.
|
|
76
|
+
*
|
|
77
|
+
* Note what this does *not* mean: `certain` is about the strength of the evidence,
|
|
78
|
+
* not its direction, so `{ certain: true }` also matches a proven **human** — a
|
|
79
|
+
* customer your application vouched for through `grantClearance`. A rule intended
|
|
80
|
+
* for automation must say so, either by putting an allow rule for `verdict: "human"`
|
|
81
|
+
* ahead of it or by naming the verdicts it means. Both shipped presets had this
|
|
82
|
+
* wrong until the traffic corpus caught it.
|
|
83
|
+
*/
|
|
84
|
+
certain?: boolean;
|
|
85
|
+
minScore?: number;
|
|
86
|
+
maxScore?: number;
|
|
87
|
+
/** Request path. Strings match as a prefix; regexes are tested as written. */
|
|
88
|
+
path?: string | RegExp | readonly (string | RegExp)[];
|
|
89
|
+
method?: string | readonly string[];
|
|
90
|
+
/** Fires when any evidence came from one of these detectors. */
|
|
91
|
+
detector?: string | readonly string[];
|
|
92
|
+
/** Fires only when the actor has been proven a bot at least this many times before. */
|
|
93
|
+
minPriorConfirmations?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Fires only when this actor has this many challenges outstanding — issued, and never
|
|
96
|
+
* answered.
|
|
97
|
+
*
|
|
98
|
+
* A rule rather than evidence, and the difference is the point. Somebody who abandons
|
|
99
|
+
* a challenge is somebody: a slow phone, a lost tab, a change of mind. It is only
|
|
100
|
+
* *repetition* that means anything, and how much it means depends on traffic the
|
|
101
|
+
* library cannot see — a checkout flow and a documentation site should read the same
|
|
102
|
+
* number differently. So the engine counts, and your policy decides.
|
|
103
|
+
*
|
|
104
|
+
* Solving one clears the count, so this never accumulates against a person who came
|
|
105
|
+
* back and proved it.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* { id: "persistent-refusers", match: { minUnsolvedChallenges: 3 }, action: "rate-limit" }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
minUnsolvedChallenges?: number;
|
|
112
|
+
}
|
|
113
|
+
export interface Rule {
|
|
114
|
+
/** Stable id. Appears in every decision and log line this rule produces. */
|
|
115
|
+
id: string;
|
|
116
|
+
/** Declarative spec, or a predicate for anything the spec cannot express. */
|
|
117
|
+
match: MatchSpec | ((assessment: Assessment) => boolean);
|
|
118
|
+
action: ActionName;
|
|
119
|
+
params?: ActionParams;
|
|
120
|
+
/** Overrides the generated explanation. */
|
|
121
|
+
reason?: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* How strictly the engine refuses to act on unproven evidence.
|
|
125
|
+
*
|
|
126
|
+
* - **`strict`** (default) — a terminal action requires `assessment.certain`. A rule
|
|
127
|
+
* asking to block on a probabilistic verdict is downgraded to `fallbackAction` and
|
|
128
|
+
* the substitution is recorded. This is the mode in which the library's central
|
|
129
|
+
* claim holds: nothing is ever denied service on the strength of a guess.
|
|
130
|
+
* - **`balanced`** — terminal actions additionally allowed when the score clears
|
|
131
|
+
* `terminalScoreThreshold` **and** at least two independent `strong` signals fired.
|
|
132
|
+
* Two independent strong signals is a much higher bar than a score alone, which any
|
|
133
|
+
* number of weak correlated observations can reach. It is still, unambiguously, a
|
|
134
|
+
* probabilistic decision — some real people will be caught.
|
|
135
|
+
* - **`aggressive`** — rules run exactly as written. Choose this only with a way to
|
|
136
|
+
* see who you turned away, and a way for them to reach you.
|
|
137
|
+
*/
|
|
138
|
+
export type FalsePositivePolicy = "strict" | "balanced" | "aggressive";
|
|
139
|
+
export interface PolicyOptions {
|
|
140
|
+
/** Evaluated in order; the first match wins. */
|
|
141
|
+
rules?: readonly Rule[];
|
|
142
|
+
/** Used when no rule matches. Default `allow`. */
|
|
143
|
+
defaultAction?: ActionName;
|
|
144
|
+
defaultParams?: ActionParams;
|
|
145
|
+
falsePositivePolicy?: FalsePositivePolicy;
|
|
146
|
+
/**
|
|
147
|
+
* Substituted when the guard blocks a terminal action. Default `challenge` if a
|
|
148
|
+
* challenge is configured, otherwise `tag`.
|
|
149
|
+
*/
|
|
150
|
+
fallbackAction?: ActionName;
|
|
151
|
+
/** Score needed for a terminal action under `balanced`. Default 85. */
|
|
152
|
+
terminalScoreThreshold?: number;
|
|
153
|
+
/** Called whenever the guard downgrades, so a mismatch between intent and effect is visible. */
|
|
154
|
+
onDowngrade?: ((decision: Decision, assessment: Assessment) => void) | undefined;
|
|
155
|
+
}
|