@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,714 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ActionName } from "../policy/types.js";
|
|
3
|
+
import type { Assessment, BotClass, Certainty, EvidenceDirection, Verdict } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* How a viewer proves they are allowed to read the dashboard.
|
|
6
|
+
*
|
|
7
|
+
* There is no default, and that is deliberate. This page shows client addresses,
|
|
8
|
+
* User-Agents, the paths people asked for, and — the part that matters most — exactly
|
|
9
|
+
* which detector fired and why. That last item is a tuning guide for anyone building a
|
|
10
|
+
* scraper against you: it tells them precisely which signal to fix next. So the server
|
|
11
|
+
* refuses to start on a non-loopback address unless you have said something explicit
|
|
12
|
+
* about who may read it.
|
|
13
|
+
*/
|
|
14
|
+
export type DashboardAuth =
|
|
15
|
+
/** HTTP Basic. Both halves are compared in constant time. */
|
|
16
|
+
{
|
|
17
|
+
username: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A shared secret, accepted as `Authorization: Bearer <token>` or as
|
|
22
|
+
* `?token=<token>` so a link can be opened directly. The query form puts the secret
|
|
23
|
+
* in browser history and in any proxy log on the way — fine for a laptop, a poor
|
|
24
|
+
* idea for a shared deployment.
|
|
25
|
+
*/
|
|
26
|
+
| {
|
|
27
|
+
token: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Your own check — a session cookie, an SSO header your gateway sets, an mTLS
|
|
31
|
+
* subject. Returning `false` produces a 401 with no hint about why.
|
|
32
|
+
*
|
|
33
|
+
* **Return a string to name the viewer.** Anything truthy admits them; a non-empty
|
|
34
|
+
* string additionally says who they are, and that name travels with every change
|
|
35
|
+
* they make — into the `warning` the handler raises, into `policy-change`,
|
|
36
|
+
* `guard-change`, `range-change` and `actor-change`, and into the dashboard's own
|
|
37
|
+
* notices. Without it the audit trail can say a rule set was replaced and cannot say
|
|
38
|
+
* by whom, which is half an audit trail.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* auth: { authorize: (req) => sessionFrom(req)?.email ?? false }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* An empty string is a refusal rather than an anonymous admission, so a lookup that
|
|
45
|
+
* returns `""` for "no such user" fails closed.
|
|
46
|
+
*/
|
|
47
|
+
| {
|
|
48
|
+
authorize: (request: IncomingMessage) => boolean | string | Promise<boolean | string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* No authentication at all. An explicit, greppable opt-out: write this when the
|
|
52
|
+
* dashboard is already behind something that authenticates, and never merely to
|
|
53
|
+
* make the startup error go away.
|
|
54
|
+
*/
|
|
55
|
+
| false;
|
|
56
|
+
/**
|
|
57
|
+
* What a caller the dashboard will not serve is told.
|
|
58
|
+
*
|
|
59
|
+
* The default is honest, and honest is usually right: `401` says "there is something
|
|
60
|
+
* here and you are not it", which is exactly what an operator who mistyped a password
|
|
61
|
+
* needs to read. But it also confirms, to anybody sweeping a port range, that this
|
|
62
|
+
* address runs an administrative page worth coming back to.
|
|
63
|
+
*
|
|
64
|
+
* Be clear about what changing it buys. **This is concealment, not access control.**
|
|
65
|
+
* A dashboard that answers `404` to the wrong credentials is exactly as reachable by
|
|
66
|
+
* someone holding the right ones as it was before, and exactly as exposed if the
|
|
67
|
+
* credentials leak. It raises the cost of *finding*, which is worth something against
|
|
68
|
+
* indiscriminate scanning and worth nearly nothing against somebody who already knows
|
|
69
|
+
* where to look. It is a layer on top of `auth`, never a substitute for it — and if
|
|
70
|
+
* reading this sentence makes you want to reach for it instead of authentication,
|
|
71
|
+
* that is the opposite of what it is for.
|
|
72
|
+
*/
|
|
73
|
+
export type DashboardRefusal =
|
|
74
|
+
/**
|
|
75
|
+
* `401`, with `WWW-Authenticate` when the scheme is Basic. The default: a person
|
|
76
|
+
* who got their password wrong is told so, and a browser can prompt.
|
|
77
|
+
*/
|
|
78
|
+
"unauthorized"
|
|
79
|
+
/**
|
|
80
|
+
* `404`, byte-identical to the answer for a path that does not exist here — so a
|
|
81
|
+
* probe cannot tell a guarded dashboard from a server that has never heard of it.
|
|
82
|
+
*
|
|
83
|
+
* The cost is that a browser will never prompt for credentials, because nothing
|
|
84
|
+
* asks it to. Pair it with `{ token }` or `{ authorize }` and a link people already
|
|
85
|
+
* hold; Basic auth is refused with this setting, because a login you cannot reach
|
|
86
|
+
* is not a login.
|
|
87
|
+
*/
|
|
88
|
+
| "not-found"
|
|
89
|
+
/**
|
|
90
|
+
* No response at all — the connection is destroyed. What a port behind a dropping
|
|
91
|
+
* firewall looks like.
|
|
92
|
+
*
|
|
93
|
+
* The quietest option and the most disruptive: some clients retry a dropped
|
|
94
|
+
* connection rather than give up, and you lose the ability to tell a refusal from a
|
|
95
|
+
* network fault in your own logs. Same Basic-auth caveat as `not-found`.
|
|
96
|
+
*/
|
|
97
|
+
| "close"
|
|
98
|
+
/**
|
|
99
|
+
* Send them somewhere else. Your SSO's sign-in page is the useful case — an
|
|
100
|
+
* operator who is simply not logged in yet ends up where they can fix that.
|
|
101
|
+
*
|
|
102
|
+
* `status` defaults to `302`. Note that a redirect is *not* concealment: it
|
|
103
|
+
* announces both that something is here and where its login lives.
|
|
104
|
+
*/
|
|
105
|
+
| {
|
|
106
|
+
redirect: string;
|
|
107
|
+
status?: 302 | 303 | 307 | 308;
|
|
108
|
+
};
|
|
109
|
+
/** What a viewer may do beyond looking. */
|
|
110
|
+
export interface DashboardControls {
|
|
111
|
+
/**
|
|
112
|
+
* Allow the policy editor: viewing the rules as JSON, previewing a change against
|
|
113
|
+
* recent traffic, and **applying it to the running handler**. Default false.
|
|
114
|
+
*
|
|
115
|
+
* What it can and cannot reach is the whole design. It replaces the rule list —
|
|
116
|
+
* which rules exist, what each matches, what action each asks for. It cannot touch
|
|
117
|
+
* `falsePositivePolicy`, `fallbackAction` or `terminalScoreThreshold`, so no edit
|
|
118
|
+
* made here can relax the guard that stops an unproven verdict from denying
|
|
119
|
+
* anybody. Loosening that stays a deploy.
|
|
120
|
+
*
|
|
121
|
+
* Every apply is validated first, swapped atomically, and announced through the
|
|
122
|
+
* handler's `warning` event so it lands wherever your startup warnings land. The
|
|
123
|
+
* server refuses to enable this at all on a non-loopback bind with `auth: false`:
|
|
124
|
+
* an unauthenticated editor on a public address is a stranger's bot policy.
|
|
125
|
+
*/
|
|
126
|
+
editPolicy?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Allow the guard editor: changing `falsePositivePolicy`, `fallbackAction`,
|
|
129
|
+
* `defaultAction`, `terminalScoreThreshold` and `suspectThreshold` on the running
|
|
130
|
+
* handler. Default false, and the default is the one to keep unless somebody has
|
|
131
|
+
* decided otherwise on purpose.
|
|
132
|
+
*
|
|
133
|
+
* **This is the setting that decides whether an unproven verdict can deny anybody.**
|
|
134
|
+
* `editPolicy` changes which rules exist; a rule that overreaches is still stopped
|
|
135
|
+
* by the guard, so the worst an editor can do is write a rule that gets downgraded.
|
|
136
|
+
* This flag changes the guard itself. Moving `falsePositivePolicy` to `aggressive`
|
|
137
|
+
* makes every probabilistic verdict terminal, and the people it turns away first are
|
|
138
|
+
* the ones with the most unusual and most legitimate setups.
|
|
139
|
+
*
|
|
140
|
+
* So it is a separate flag from `editPolicy` rather than part of it: the two are
|
|
141
|
+
* different powers, and a dashboard that hands out the first should not have to hand
|
|
142
|
+
* out the second. Grant it the way you grant any other privileged operation — to a
|
|
143
|
+
* listener that a role check stands in front of, and to nobody else. Two things
|
|
144
|
+
* remain impossible whatever this is set to: a terminal `fallbackAction`, which
|
|
145
|
+
* would make a downgrade deny the request it was protecting, and a
|
|
146
|
+
* `terminalScoreThreshold` outside 1–100.
|
|
147
|
+
*
|
|
148
|
+
* Every change is validated first, applied whole or not at all, announced through
|
|
149
|
+
* the handler's `warning` event and emitted as `guard-change` with both the before
|
|
150
|
+
* and the after — which is the audit trail this deserves. The server refuses to
|
|
151
|
+
* enable it at all on a non-loopback bind with `auth: false`.
|
|
152
|
+
*/
|
|
153
|
+
editGuard?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Allow the range editor and the per-actor operations: adding an address to the
|
|
156
|
+
* allowlist or any other range set, forgetting one actor's history, and granting an
|
|
157
|
+
* actor human clearance. Default false.
|
|
158
|
+
*
|
|
159
|
+
* The three of them are one flag because they are one job — acting on a specific
|
|
160
|
+
* client rather than on a class of request — and because the first is consequential
|
|
161
|
+
* enough to carry the other two. **An allowlisted address is not judged leniently; it
|
|
162
|
+
* is not judged at all.** Detection does not run, no evidence is produced, no rule
|
|
163
|
+
* sees it. That is the right answer for your own monitoring and the wrong answer for
|
|
164
|
+
* anything that might one day be somebody else's.
|
|
165
|
+
*
|
|
166
|
+
* Forgetting an actor is the mild one, and the reason this exists at all: a person
|
|
167
|
+
* whose actor key collected a `confirmed-bot` carries `priorConfirmations` for the
|
|
168
|
+
* rest of the window, and until now the only cure was Reset — throwing away every
|
|
169
|
+
* actor's history to fix one.
|
|
170
|
+
*
|
|
171
|
+
* Refused unauthenticated on a public bind, like the other editors. Unavailable when
|
|
172
|
+
* `redact.maskIp` is on, because a masked key names a network rather than the actor
|
|
173
|
+
* the registry is keyed by, and acting on the wrong key silently is worse than not
|
|
174
|
+
* offering the button.
|
|
175
|
+
*/
|
|
176
|
+
editRanges?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Allow the "Reset" button to clear the feed, the counters and the **actor
|
|
179
|
+
* registry**. Default false.
|
|
180
|
+
*
|
|
181
|
+
* Clearing the registry discards every actor's history, which is real state on a
|
|
182
|
+
* live system: rate observations, cadence series, prior confirmations and challenge
|
|
183
|
+
* clearances all go with it. That is exactly what you want while running the
|
|
184
|
+
* simulator and exactly what you do not want a bored browser tab doing to
|
|
185
|
+
* production.
|
|
186
|
+
*/
|
|
187
|
+
reset?: boolean;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Which parts of the dashboard exist on this listener.
|
|
191
|
+
*
|
|
192
|
+
* Everything defaults to on, and turning something off removes it from the page *and*
|
|
193
|
+
* from the server: the tab is gone, the panel is gone, the endpoint behind it answers
|
|
194
|
+
* 403, and fields a switched-off section would have shown are dropped before the data
|
|
195
|
+
* leaves the process. A viewer with devtools open sees exactly what the page sees.
|
|
196
|
+
*
|
|
197
|
+
* **There is no role model in here, deliberately.** `controls` says what a viewer may
|
|
198
|
+
* *do*; this says what a viewer may *see*; and both are fixed when the listener
|
|
199
|
+
* starts, which is what makes them cheap to reason about — no per-request evaluation,
|
|
200
|
+
* no session store, no second copy of your user table quietly disagreeing with the
|
|
201
|
+
* first. Roles are yours: run a listener per role, put your own `auth.authorize`
|
|
202
|
+
* predicate in front of each, and give each the sections and controls that role should
|
|
203
|
+
* have. `npm run demo:roles` is that arrangement, working.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* // An analyst sees traffic, not the reasons behind it — the evidence panel names
|
|
207
|
+
* // the exact signal that fired, which is a tuning guide for whoever is scraping you.
|
|
208
|
+
* await botHandler.serveDashboard({
|
|
209
|
+
* port: 9684,
|
|
210
|
+
* auth: { authorize: (req) => roleOf(req) === "analyst" },
|
|
211
|
+
* sections: { evidence: false, policy: false },
|
|
212
|
+
* redact: { maskIp: true },
|
|
213
|
+
* });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export interface DashboardSections {
|
|
217
|
+
/** The Live feed tab, and the `/api/feed` and `/api/stream` endpoints behind it. Default true. */
|
|
218
|
+
feed?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* The case for each verdict: the evidence list with its written basis, the request
|
|
221
|
+
* headers, the query parameters, the detector failures, and the buttons that turn a
|
|
222
|
+
* row into a replay line or a corpus case. Default true.
|
|
223
|
+
*
|
|
224
|
+
* The one worth thinking about before sharing a dashboard widely. This is the half
|
|
225
|
+
* of the page that says *which detector fired and why*, which is precisely what
|
|
226
|
+
* somebody building a scraper against you needs in order to know what to fix next.
|
|
227
|
+
* Switching it off leaves the feed — what happened, to whom, and what was done —
|
|
228
|
+
* and drops the evidence from the wire, not just from the screen.
|
|
229
|
+
*/
|
|
230
|
+
evidence?: boolean;
|
|
231
|
+
/** The actor drill-down, the busiest-actors panel, and per-request actor history. Default true. */
|
|
232
|
+
actors?: boolean;
|
|
233
|
+
/** The Statistics tab and the counter tiles above it. Default true. */
|
|
234
|
+
statistics?: boolean;
|
|
235
|
+
/** The traffic audit panel — the window against its baseline. Default true. */
|
|
236
|
+
audit?: boolean;
|
|
237
|
+
/** The notices panel: startup warnings, audit anomalies, detector errors. Default true. */
|
|
238
|
+
notices?: boolean;
|
|
239
|
+
/**
|
|
240
|
+
* The changes panel: what was applied at runtime, when, and by whom. Default true.
|
|
241
|
+
*
|
|
242
|
+
* The same list the traffic timeline marks. It is an audit trail rather than a log —
|
|
243
|
+
* bounded and in memory, gone with the process — so treat the panel as the convenient
|
|
244
|
+
* copy and `policy-change`, `guard-change`, `range-change` and `actor-change` as the
|
|
245
|
+
* durable one.
|
|
246
|
+
*/
|
|
247
|
+
changes?: boolean;
|
|
248
|
+
/**
|
|
249
|
+
* The Actors screen: the busiest actors the *registry* is holding, which is a far
|
|
250
|
+
* larger population than the feed's ring. Default true, and it follows `actors`.
|
|
251
|
+
*/
|
|
252
|
+
registry?: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* The request tester: paste a User-Agent, a curl command or a raw header block and
|
|
255
|
+
* see what the engine would make of it. Default true.
|
|
256
|
+
*
|
|
257
|
+
* It runs a dry-run assessment — nothing is recorded, no counter moves — so it
|
|
258
|
+
* answers "why is this client being challenged?" without waiting for that client to
|
|
259
|
+
* come back. It shows evidence, so it follows the `evidence` section too.
|
|
260
|
+
*/
|
|
261
|
+
tester?: boolean;
|
|
262
|
+
/** The Policy tab: the rules, the preview, the editor, the settings export. Default true. */
|
|
263
|
+
policy?: boolean;
|
|
264
|
+
/** The range sets panel, and the allowlist controls on it. Default true. */
|
|
265
|
+
ranges?: boolean;
|
|
266
|
+
/** The guard panel inside the Policy tab. Default true; see {@link DashboardControls.editGuard}. */
|
|
267
|
+
guard?: boolean;
|
|
268
|
+
/** The `robots.txt` the policy implies. Default true. */
|
|
269
|
+
robots?: boolean;
|
|
270
|
+
}
|
|
271
|
+
/** How much of each request the dashboard is allowed to show. */
|
|
272
|
+
export interface DashboardRedaction {
|
|
273
|
+
/**
|
|
274
|
+
* Replace client addresses with their network (`/24`, `/64`). Default false.
|
|
275
|
+
*
|
|
276
|
+
* Off by default because this is an operator's own tool and acting on a specific
|
|
277
|
+
* address is the point. Turn it on when the dashboard is shared more widely than
|
|
278
|
+
* the logs are — an address is personal data in most of the world, and the network
|
|
279
|
+
* is enough to recognise a pattern.
|
|
280
|
+
*/
|
|
281
|
+
maskIp?: boolean;
|
|
282
|
+
/** Keep only the first 48 characters of each User-Agent. Default false. */
|
|
283
|
+
truncateUserAgent?: boolean;
|
|
284
|
+
/**
|
|
285
|
+
* Replace query-string *values* with a placeholder, keeping the names. Default true.
|
|
286
|
+
*
|
|
287
|
+
* A query string is where password-reset tokens, invitation links and email
|
|
288
|
+
* addresses actually live. The names are what make a pattern legible — a burst of
|
|
289
|
+
* `?export=` is the thing worth seeing — and the values are what nobody needs on a
|
|
290
|
+
* screen that gets screenshotted into tickets.
|
|
291
|
+
*/
|
|
292
|
+
maskQuery?: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Show the request's headers in the row detail, credentials stripped. Default true.
|
|
295
|
+
*
|
|
296
|
+
* The header set in wire order is the single most useful thing when arguing with a
|
|
297
|
+
* false positive — it is what half the detectors are reading. `Cookie`,
|
|
298
|
+
* `Authorization` and their neighbours never appear whatever this is set to; see
|
|
299
|
+
* `CREDENTIAL_HEADERS`.
|
|
300
|
+
*/
|
|
301
|
+
headers?: boolean;
|
|
302
|
+
}
|
|
303
|
+
export interface DashboardOptions {
|
|
304
|
+
/** Port to listen on. Default 9674. Pass 0 for an ephemeral port and read it back from `url`. */
|
|
305
|
+
port?: number;
|
|
306
|
+
/**
|
|
307
|
+
* Address to bind. Default `"127.0.0.1"` — the loopback interface only.
|
|
308
|
+
*
|
|
309
|
+
* Binding anywhere else publishes the page to the network, so it requires an
|
|
310
|
+
* explicit `auth` (or an explicit `auth: false`); the server refuses to start
|
|
311
|
+
* otherwise rather than quietly exposing your evidence trail.
|
|
312
|
+
*/
|
|
313
|
+
host?: string;
|
|
314
|
+
auth?: DashboardAuth;
|
|
315
|
+
/**
|
|
316
|
+
* How a caller this dashboard will not serve is answered. Default `"unauthorized"`.
|
|
317
|
+
*
|
|
318
|
+
* Applies to every refusal made before routing — a failed `auth`, a `Host` outside
|
|
319
|
+
* `allowedHosts`, a cross-site write — so that all three look alike and none of them
|
|
320
|
+
* becomes an oracle. Under the default they keep their distinct, informative
|
|
321
|
+
* statuses (`401`, `421`, `403`), because an operator debugging their own setup is
|
|
322
|
+
* the likelier reader.
|
|
323
|
+
*/
|
|
324
|
+
refusal?: DashboardRefusal;
|
|
325
|
+
/** Path the dashboard is mounted under, e.g. `"/_bots"`. Default `"/"`. */
|
|
326
|
+
basePath?: string;
|
|
327
|
+
/** Name shown in the header and the document title. Default `"bothandlerjs"`. */
|
|
328
|
+
title?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Which process this is. Default the machine's hostname.
|
|
331
|
+
*
|
|
332
|
+
* Shown in the header, and it is there to answer a question the page otherwise
|
|
333
|
+
* invites you to get wrong. **A dashboard reports on one process.** Behind a load
|
|
334
|
+
* balancer with eight pods there are eight rings, eight sets of counters and eight
|
|
335
|
+
* actor registries, and the one you happen to have opened is showing you an eighth of
|
|
336
|
+
* your traffic. Naming the instance does not aggregate anything; it stops a partial
|
|
337
|
+
* picture from looking like a whole one.
|
|
338
|
+
*/
|
|
339
|
+
instance?: string;
|
|
340
|
+
/** Links shown in the header — your site, your runbook, whatever is useful. */
|
|
341
|
+
links?: ReadonlyArray<{
|
|
342
|
+
label: string;
|
|
343
|
+
href: string;
|
|
344
|
+
}>;
|
|
345
|
+
/** Requests kept in the live feed. Default 500, hard-capped at 5000. */
|
|
346
|
+
feedLimit?: number;
|
|
347
|
+
/**
|
|
348
|
+
* How long a request may stay in the feed, in milliseconds. Default one hour; `0`
|
|
349
|
+
* keeps them until the ring evicts them.
|
|
350
|
+
*
|
|
351
|
+
* `feedLimit` is a capacity bound and this is a retention one, which is a different
|
|
352
|
+
* question with a different answer. On a quiet service five hundred requests can be a
|
|
353
|
+
* fortnight of traffic, and every entry holds a client address, a User-Agent and a
|
|
354
|
+
* header set belonging to a person. "We keep the last five hundred requests" is a
|
|
355
|
+
* statement about memory; "we keep nothing older than an hour" is a promise you can
|
|
356
|
+
* make to somebody who asks.
|
|
357
|
+
*
|
|
358
|
+
* Eviction runs when a request is recorded *and* on the dashboard's own timer, so it
|
|
359
|
+
* holds on an idle process too — which is the only kind of process where it matters,
|
|
360
|
+
* since a busy one drops entries by count long before they reach this age.
|
|
361
|
+
*/
|
|
362
|
+
feedTtlMs?: number;
|
|
363
|
+
/** Concurrent event-stream viewers. Default 16. Beyond this the server answers 503. */
|
|
364
|
+
maxClients?: number;
|
|
365
|
+
/**
|
|
366
|
+
* Entries per second pushed to each viewer. Default 100; `0` removes the cap.
|
|
367
|
+
*
|
|
368
|
+
* A dashboard on a busy origin is a firehose: every assessment goes to every open
|
|
369
|
+
* browser, so a thousand requests a second is a couple of megabytes a second *per
|
|
370
|
+
* viewer*, and the only lever the page has is Pause. Above this rate the surplus is
|
|
371
|
+
* dropped from the stream — not from the ring, which still holds the last
|
|
372
|
+
* `feedLimit` requests for the preview and for anyone who reconnects — and the page
|
|
373
|
+
* shows how many were skipped, so a thinned feed never looks like a quiet one.
|
|
374
|
+
*
|
|
375
|
+
* The cap is per second and not smoothed: a burst inside one second is delivered
|
|
376
|
+
* whole up to the limit, because seeing the front of a burst is the point.
|
|
377
|
+
*/
|
|
378
|
+
maxEventsPerSecond?: number;
|
|
379
|
+
controls?: DashboardControls;
|
|
380
|
+
/** Which parts of the page exist on this listener. See {@link DashboardSections}. */
|
|
381
|
+
sections?: DashboardSections;
|
|
382
|
+
redact?: DashboardRedaction;
|
|
383
|
+
/**
|
|
384
|
+
* Client addresses this dashboard will answer at all, as addresses or CIDRs.
|
|
385
|
+
*
|
|
386
|
+
* The layer `allowedHosts` is not. That one checks the name in the `Host` header —
|
|
387
|
+
* what the client *asked for* — and closes DNS rebinding. This checks who is
|
|
388
|
+
* connecting, which is the question an operator means when they say "bind `0.0.0.0`,
|
|
389
|
+
* but only the VPN can reach it".
|
|
390
|
+
*
|
|
391
|
+
* ```ts
|
|
392
|
+
* host: "0.0.0.0",
|
|
393
|
+
* allowedClients: ["10.0.0.0/8", "192.168.0.0/16"],
|
|
394
|
+
* auth: { username: "ops", password: process.env.DASHBOARD_PASSWORD! },
|
|
395
|
+
* ```
|
|
396
|
+
*
|
|
397
|
+
* It is checked before authentication, so an address outside the list cannot even
|
|
398
|
+
* attempt a credential, and it is answered through `refusal` like every other
|
|
399
|
+
* pre-routing check. It is a *layer*, not a replacement for `auth`: addresses are
|
|
400
|
+
* spoofable at the network level in ways passwords are not, and a shared office range
|
|
401
|
+
* is not a person.
|
|
402
|
+
*
|
|
403
|
+
* The address compared is the one the handler's own proxy configuration resolves —
|
|
404
|
+
* so behind a load balancer you list your users' addresses rather than the balancer's
|
|
405
|
+
* — and a peer that is not one of your `trustedProxies` cannot claim to be somebody
|
|
406
|
+
* else, because the chain is only walked for peers that are.
|
|
407
|
+
*/
|
|
408
|
+
allowedClients?: readonly string[];
|
|
409
|
+
/**
|
|
410
|
+
* How a repeatedly-wrong credential is slowed down. Default: on.
|
|
411
|
+
*
|
|
412
|
+
* Both halves of a basic credential are compared in constant time, which stops one
|
|
413
|
+
* attack and does nothing about the obvious one — trying again. An administrative
|
|
414
|
+
* page reachable from anywhere with an unthrottled password prompt can be worked
|
|
415
|
+
* through at line rate.
|
|
416
|
+
*
|
|
417
|
+
* After `maxAttempts` failures from one address the next attempt is refused outright
|
|
418
|
+
* for a delay that doubles each time, up to `maxLockoutMs`. A success clears the
|
|
419
|
+
* count. Set `false` to switch it off, which is only sensible when something in front
|
|
420
|
+
* of the dashboard is already doing this.
|
|
421
|
+
*
|
|
422
|
+
* The refusal reuses whatever `refusal` says, so a dashboard configured to look like
|
|
423
|
+
* an empty server still looks like one: a `429` would tell a prober that there is a
|
|
424
|
+
* credential here worth guessing.
|
|
425
|
+
*/
|
|
426
|
+
authThrottle?: {
|
|
427
|
+
maxAttempts?: number;
|
|
428
|
+
lockoutMs?: number;
|
|
429
|
+
maxLockoutMs?: number;
|
|
430
|
+
} | false;
|
|
431
|
+
/**
|
|
432
|
+
* Sibling dashboards, for the header's instance switcher.
|
|
433
|
+
*
|
|
434
|
+
* A dashboard reports on one process — see {@link DashboardOptions.instance} — and
|
|
435
|
+
* this is the honest amount of help the page can give with that: a way to reach the
|
|
436
|
+
* other ones. It aggregates nothing, and deliberately: a feed and an actor registry
|
|
437
|
+
* summed across pods would be a different tool, and the counters are already
|
|
438
|
+
* aggregatable through Prometheus, which is a thing your monitoring already does.
|
|
439
|
+
*/
|
|
440
|
+
peers?: ReadonlyArray<{
|
|
441
|
+
label: string;
|
|
442
|
+
href: string;
|
|
443
|
+
}>;
|
|
444
|
+
/**
|
|
445
|
+
* Extra `Host` header values this dashboard will answer to.
|
|
446
|
+
*
|
|
447
|
+
* Only consulted on a loopback bind, where it defends against DNS rebinding: a name
|
|
448
|
+
* the attacker controls, pointed at 127.0.0.1, makes their page *same-origin* with
|
|
449
|
+
* this server and hands them the feed. Checking the `Host` header closes that,
|
|
450
|
+
* because the name they need you to send is not one you wrote down.
|
|
451
|
+
*
|
|
452
|
+
* The defaults — `localhost`, `127.0.0.1`, `[::1]` and the bound address — cover how
|
|
453
|
+
* a dashboard on loopback is actually opened. Add to them when you reach it through
|
|
454
|
+
* a name of your own, e.g. an SSH tunnel aliased in `/etc/hosts`. A non-loopback
|
|
455
|
+
* bind ignores this: rebinding buys an attacker nothing against an address they can
|
|
456
|
+
* already reach, and enforcing it there would break every reverse proxy.
|
|
457
|
+
*
|
|
458
|
+
* Entries are compared without the port. `"*"` disables the check.
|
|
459
|
+
*/
|
|
460
|
+
allowedHosts?: readonly string[];
|
|
461
|
+
/**
|
|
462
|
+
* Serve the Prometheus exposition at `<basePath>/metrics`, behind the same auth.
|
|
463
|
+
* Default false — a scraper usually wants its own unauthenticated endpoint on a
|
|
464
|
+
* different port, and `handler.prometheus()` gives you the text to serve there.
|
|
465
|
+
*/
|
|
466
|
+
exposePrometheus?: boolean;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Options for {@link createDashboardHandler}.
|
|
470
|
+
*
|
|
471
|
+
* The listening form's options minus the two that describe a socket it does not own,
|
|
472
|
+
* plus one requirement: `auth` is mandatory, including the explicit `auth: false`.
|
|
473
|
+
* `startDashboard` may skip it on `127.0.0.1` because the operating system is then the
|
|
474
|
+
* access control; mounted, there is no bind address to inspect, so nothing can be
|
|
475
|
+
* assumed and the assumption made is "public".
|
|
476
|
+
*/
|
|
477
|
+
export type DashboardHandlerOptions = Omit<DashboardOptions, "port" | "host"> & {
|
|
478
|
+
auth: DashboardAuth;
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* The dashboard as a request handler, for a server you already have.
|
|
482
|
+
*
|
|
483
|
+
* Callable as `(request, response)`. `close()` unsubscribes from the engine and ends
|
|
484
|
+
* every event stream; it does not close a server it does not own.
|
|
485
|
+
*/
|
|
486
|
+
export interface DashboardRequestHandler {
|
|
487
|
+
(request: IncomingMessage, response: ServerResponse): void;
|
|
488
|
+
/** Event-stream viewers currently connected. */
|
|
489
|
+
readonly clients: number;
|
|
490
|
+
close(): Promise<void>;
|
|
491
|
+
}
|
|
492
|
+
/** A running dashboard. */
|
|
493
|
+
export interface DashboardServer {
|
|
494
|
+
/** The address to open. Reflects the port actually bound, which matters when you passed 0. */
|
|
495
|
+
readonly url: string;
|
|
496
|
+
readonly port: number;
|
|
497
|
+
readonly host: string;
|
|
498
|
+
/** Event-stream viewers currently connected. */
|
|
499
|
+
readonly clients: number;
|
|
500
|
+
/** Stops listening, drops every stream, and unsubscribes from the engine. Idempotent. */
|
|
501
|
+
close(): Promise<void>;
|
|
502
|
+
}
|
|
503
|
+
/** One assessed request, as the dashboard shows it. */
|
|
504
|
+
export interface DashboardEntry {
|
|
505
|
+
seq: number;
|
|
506
|
+
requestId: string;
|
|
507
|
+
/** Epoch milliseconds, so the page can bucket by time without parsing anything. */
|
|
508
|
+
at: number;
|
|
509
|
+
method: string;
|
|
510
|
+
path: string;
|
|
511
|
+
actor: string;
|
|
512
|
+
userAgent: string;
|
|
513
|
+
verdict: Verdict;
|
|
514
|
+
botClass: BotClass;
|
|
515
|
+
identity?: string | undefined;
|
|
516
|
+
score: number;
|
|
517
|
+
certain: boolean;
|
|
518
|
+
durationMs: number;
|
|
519
|
+
bypass?: string | undefined;
|
|
520
|
+
/** Absent until the policy has decided — `assess()` on its own never produces one. */
|
|
521
|
+
action?: ActionName | undefined;
|
|
522
|
+
rule?: string | undefined;
|
|
523
|
+
downgradedFrom?: ActionName | undefined;
|
|
524
|
+
downgradeReason?: string | undefined;
|
|
525
|
+
evidence: DashboardEvidence[];
|
|
526
|
+
failures: Array<{
|
|
527
|
+
detector: string;
|
|
528
|
+
reason: string;
|
|
529
|
+
message: string;
|
|
530
|
+
}>;
|
|
531
|
+
/** Actor history at the time of the request. Drives the actor drill-down. */
|
|
532
|
+
actorStats: DashboardActor;
|
|
533
|
+
/** Query parameters, values masked unless `redact.maskQuery` is false. */
|
|
534
|
+
query: Record<string, string>;
|
|
535
|
+
/** Headers in wire order, credentials removed. Absent when `redact.headers` is false. */
|
|
536
|
+
headers?: Array<[name: string, value: string]> | undefined;
|
|
537
|
+
protocol?: string | undefined;
|
|
538
|
+
httpVersion?: string | undefined;
|
|
539
|
+
}
|
|
540
|
+
export interface DashboardEvidence {
|
|
541
|
+
detector: string;
|
|
542
|
+
summary: string;
|
|
543
|
+
certainty: Certainty;
|
|
544
|
+
direction: EvidenceDirection;
|
|
545
|
+
family?: string | undefined;
|
|
546
|
+
deterministicBasis?: string | undefined;
|
|
547
|
+
/**
|
|
548
|
+
* Identity, category and weight travel with the evidence for one reason beyond
|
|
549
|
+
* display: they are what the policy matcher reads, so the entry alone is enough to
|
|
550
|
+
* re-decide a request under a different rule set. See the policy preview.
|
|
551
|
+
*/
|
|
552
|
+
identity?: string | undefined;
|
|
553
|
+
category?: string | undefined;
|
|
554
|
+
weight?: number | undefined;
|
|
555
|
+
}
|
|
556
|
+
/** Per-actor history as it stood when the request was assessed. */
|
|
557
|
+
export interface DashboardActor {
|
|
558
|
+
requests: number;
|
|
559
|
+
distinctPaths: number;
|
|
560
|
+
priorConfirmations: number;
|
|
561
|
+
cleared: boolean;
|
|
562
|
+
firstSeen: number;
|
|
563
|
+
sinceLastMs?: number | undefined;
|
|
564
|
+
}
|
|
565
|
+
/** One runtime change, for the timeline markers and the change list. */
|
|
566
|
+
export interface DashboardChange {
|
|
567
|
+
at: number;
|
|
568
|
+
kind: "policy" | "guard" | "range" | "actor";
|
|
569
|
+
summary: string;
|
|
570
|
+
/** Who asked for it, when the dashboard's `authorize` was able to say. */
|
|
571
|
+
by?: string | undefined;
|
|
572
|
+
}
|
|
573
|
+
/** A warning or error the engine raised, for the notices panel. */
|
|
574
|
+
export interface DashboardNotice {
|
|
575
|
+
at: number;
|
|
576
|
+
kind: "warning" | "error";
|
|
577
|
+
source?: string | undefined;
|
|
578
|
+
message: string;
|
|
579
|
+
}
|
|
580
|
+
/** What a candidate rule set would have done to the traffic still in the window. */
|
|
581
|
+
export interface PolicyPreview {
|
|
582
|
+
/** Requests the preview was run over — the retained window, not all time. */
|
|
583
|
+
evaluated: number;
|
|
584
|
+
changed: number;
|
|
585
|
+
/** Action counts under the live policy and under the candidate. */
|
|
586
|
+
before: Record<string, number>;
|
|
587
|
+
after: Record<string, number>;
|
|
588
|
+
/** Rule ids in the candidate, with how many of those requests each would have taken. */
|
|
589
|
+
ruleHits: Array<{
|
|
590
|
+
rule: string;
|
|
591
|
+
hits: number;
|
|
592
|
+
}>;
|
|
593
|
+
/** A bounded sample of requests whose action would change. */
|
|
594
|
+
samples: Array<{
|
|
595
|
+
path: string;
|
|
596
|
+
verdict: string;
|
|
597
|
+
userAgent: string;
|
|
598
|
+
from: string;
|
|
599
|
+
to: string;
|
|
600
|
+
fromRule: string;
|
|
601
|
+
toRule: string;
|
|
602
|
+
}>;
|
|
603
|
+
/** Problems found in the candidate that do not stop it loading. */
|
|
604
|
+
warnings: string[];
|
|
605
|
+
/**
|
|
606
|
+
* True when the candidate would deny a request the live policy served.
|
|
607
|
+
*
|
|
608
|
+
* Called out on its own because it is the one direction of change that costs
|
|
609
|
+
* somebody their access, and it is easy to miss in a table of counts.
|
|
610
|
+
*/
|
|
611
|
+
newDenials: number;
|
|
612
|
+
}
|
|
613
|
+
/** Everything the page is given at load time, and again on every stats refresh. */
|
|
614
|
+
export interface DashboardSnapshot {
|
|
615
|
+
startedAt: number;
|
|
616
|
+
now: number;
|
|
617
|
+
/** Absent when the handler has `metrics: false`, or when the `statistics` section is off. */
|
|
618
|
+
metrics: import("../metrics.js").MetricsSnapshot | undefined;
|
|
619
|
+
detectors: Array<{
|
|
620
|
+
id: string;
|
|
621
|
+
description: string;
|
|
622
|
+
cost: string;
|
|
623
|
+
stage: string;
|
|
624
|
+
}>;
|
|
625
|
+
rules: readonly string[];
|
|
626
|
+
ranges: Array<{
|
|
627
|
+
name: string;
|
|
628
|
+
size: number;
|
|
629
|
+
}>;
|
|
630
|
+
/** Configuration a reader needs in order to interpret what they are looking at. */
|
|
631
|
+
policy: {
|
|
632
|
+
falsePositivePolicy: string;
|
|
633
|
+
fallbackAction: string;
|
|
634
|
+
suspectThreshold: number;
|
|
635
|
+
defaultAction: string;
|
|
636
|
+
terminalScoreThreshold: number;
|
|
637
|
+
challengeEnabled: boolean;
|
|
638
|
+
/** Whether this dashboard may change the rules. See {@link DashboardControls.editPolicy}. */
|
|
639
|
+
editable: boolean;
|
|
640
|
+
/** Whether this dashboard may change the guard. See {@link DashboardControls.editGuard}. */
|
|
641
|
+
guardEditable: boolean;
|
|
642
|
+
};
|
|
643
|
+
/** Which process this is, and how long it has been up. See {@link DashboardOptions.instance}. */
|
|
644
|
+
instance: string;
|
|
645
|
+
/**
|
|
646
|
+
* Runtime changes, newest last: policy replacements, guard edits, range updates and
|
|
647
|
+
* per-actor operations.
|
|
648
|
+
*
|
|
649
|
+
* Drawn as markers on the traffic timeline, which is what turns a preview from a
|
|
650
|
+
* prediction into something you can check. "44 of 151 requests would be treated
|
|
651
|
+
* differently" is a claim; a line on the chart at the moment it was applied, with the
|
|
652
|
+
* traffic either side of it, is the answer.
|
|
653
|
+
*/
|
|
654
|
+
changes: readonly DashboardChange[];
|
|
655
|
+
/** Feed entries dropped from the stream by the rate cap, since start. See {@link DashboardOptions.maxEventsPerSecond}. */
|
|
656
|
+
skipped: number;
|
|
657
|
+
/** Startup warnings, audit anomalies, and anything else the engine has raised. */
|
|
658
|
+
notices: readonly DashboardNotice[];
|
|
659
|
+
/**
|
|
660
|
+
* The audit's two windows as they stand, and the checks installed.
|
|
661
|
+
*
|
|
662
|
+
* `undefined` when the audit was switched off with `audit: false`.
|
|
663
|
+
*/
|
|
664
|
+
audit?: {
|
|
665
|
+
window: import("../audit.js").AuditWindow;
|
|
666
|
+
baseline: import("../audit.js").AuditWindow;
|
|
667
|
+
checks: Array<{
|
|
668
|
+
id: string;
|
|
669
|
+
description: string;
|
|
670
|
+
}>;
|
|
671
|
+
} | undefined;
|
|
672
|
+
}
|
|
673
|
+
/** The rule set as the dashboard shows it, plus what its editor needs to build itself. */
|
|
674
|
+
export interface PolicyDocument {
|
|
675
|
+
rules: Array<{
|
|
676
|
+
id: string;
|
|
677
|
+
index: number;
|
|
678
|
+
editable: boolean;
|
|
679
|
+
rule?: import("../policy/types.js").Rule;
|
|
680
|
+
}>;
|
|
681
|
+
robots: string;
|
|
682
|
+
robotsNotes: Array<{
|
|
683
|
+
rule: string;
|
|
684
|
+
reason: string;
|
|
685
|
+
}>;
|
|
686
|
+
editable: boolean;
|
|
687
|
+
/** Whether the guard form is live on this dashboard. See {@link DashboardControls.editGuard}. */
|
|
688
|
+
guardEditable: boolean;
|
|
689
|
+
/** The guard as it stands, in the shape the guard form submits back. */
|
|
690
|
+
guard: {
|
|
691
|
+
falsePositivePolicy: string;
|
|
692
|
+
fallbackAction: string;
|
|
693
|
+
defaultAction: string;
|
|
694
|
+
terminalScoreThreshold: number;
|
|
695
|
+
suspectThreshold: number;
|
|
696
|
+
};
|
|
697
|
+
vocabulary: {
|
|
698
|
+
verdicts: readonly string[];
|
|
699
|
+
botClasses: readonly string[];
|
|
700
|
+
categories: readonly string[];
|
|
701
|
+
actions: readonly string[];
|
|
702
|
+
/** Actions a downgrade may land on: everything except the terminal ones. */
|
|
703
|
+
fallbackActions: readonly string[];
|
|
704
|
+
falsePositivePolicies: readonly string[];
|
|
705
|
+
methods: readonly string[];
|
|
706
|
+
detectors: readonly string[];
|
|
707
|
+
presets: readonly string[];
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/** Assessment plus the decision that followed it, as the feed records it. */
|
|
711
|
+
export interface DashboardRecord {
|
|
712
|
+
assessment: Assessment;
|
|
713
|
+
action?: ActionName | undefined;
|
|
714
|
+
}
|