@osqd/bothandlerjs 0.3.0 → 0.4.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +157 -1
  2. package/README.md +1 -1
  3. package/dist/challenge/index.d.ts +18 -0
  4. package/dist/challenge/interaction.d.ts +215 -0
  5. package/dist/challenge/page.d.ts +18 -0
  6. package/dist/cli.cjs +653 -51
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +653 -51
  9. package/dist/cli.js.map +1 -1
  10. package/dist/core.d.ts +15 -0
  11. package/dist/corpus/index.cjs +20 -0
  12. package/dist/corpus/index.cjs.map +1 -1
  13. package/dist/corpus/index.js +20 -0
  14. package/dist/corpus/index.js.map +1 -1
  15. package/dist/dashboard/client/actions.d.ts +17 -0
  16. package/dist/dashboard/client/actor.d.ts +5 -0
  17. package/dist/dashboard/client/api.d.ts +15 -0
  18. package/dist/dashboard/client/app.d.ts +37 -0
  19. package/dist/dashboard/client/bars.d.ts +4 -0
  20. package/dist/dashboard/client/boot.d.ts +5 -0
  21. package/dist/dashboard/client/charts.d.ts +25 -0
  22. package/dist/dashboard/client/css.d.ts +11 -0
  23. package/dist/dashboard/client/dom.d.ts +55 -0
  24. package/dist/dashboard/client/draft.d.ts +46 -0
  25. package/dist/dashboard/client/feed.d.ts +6 -0
  26. package/dist/dashboard/client/format.d.ts +19 -0
  27. package/dist/dashboard/client/guard.d.ts +3 -0
  28. package/dist/dashboard/client/index.d.ts +1 -0
  29. package/dist/dashboard/client/outcome.d.ts +33 -0
  30. package/dist/dashboard/client/panels.d.ts +40 -0
  31. package/dist/dashboard/client/policy.d.ts +25 -0
  32. package/dist/dashboard/client/query.d.ts +42 -0
  33. package/dist/dashboard/client/ranges.d.ts +2 -0
  34. package/dist/dashboard/client/registry.d.ts +4 -0
  35. package/dist/dashboard/client/replay.d.ts +14 -0
  36. package/dist/dashboard/client/result.d.ts +10 -0
  37. package/dist/dashboard/client/store.d.ts +68 -0
  38. package/dist/dashboard/client/stream.d.ts +14 -0
  39. package/dist/dashboard/client/tester.d.ts +1 -0
  40. package/dist/dashboard/client/types.d.ts +69 -0
  41. package/dist/dashboard/client.generated.d.ts +1 -1
  42. package/dist/dashboard/page.d.ts +25 -0
  43. package/dist/dashboard/sections.d.ts +92 -0
  44. package/dist/dashboard/types.d.ts +2 -82
  45. package/dist/element/config.d.ts +135 -0
  46. package/dist/element/index.cjs +4844 -0
  47. package/dist/element/index.cjs.map +1 -0
  48. package/dist/element/index.d.ts +137 -0
  49. package/dist/element/index.js +4825 -0
  50. package/dist/element/index.js.map +1 -0
  51. package/dist/index.cjs +644 -51
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +635 -51
  55. package/dist/index.js.map +1 -1
  56. package/dist/metrics.d.ts +27 -0
  57. package/dist/policy/index.d.ts +1 -1
  58. package/dist/policy/presets.d.ts +47 -0
  59. package/docs/challenge/index.md +1 -0
  60. package/docs/challenge/interaction.md +325 -0
  61. package/docs/course/11-the-challenge.md +36 -1
  62. package/docs/course/13-operating-it.md +20 -2
  63. package/docs/index.md +2 -0
  64. package/docs/operations/dashboard.md +2 -0
  65. package/docs/operations/embedding.md +348 -0
  66. package/docs/operations/index.md +1 -0
  67. package/docs/policy/presets.md +53 -0
  68. package/docs/start/choosing-a-policy.md +1 -0
  69. package/package.json +8 -3
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The three things you can do to one client, rather than to a class of request.
3
+ *
4
+ * Shared by the actor drill-down and the Actors screen, which offer the same three and
5
+ * would otherwise offer them slightly differently.
6
+ *
7
+ * Allowlisting is the consequential one and it is the one that asks twice. **An
8
+ * allowlisted address is not judged leniently; it is not judged at all** — detection
9
+ * does not run, no evidence is produced, no rule sees it — so the button states the
10
+ * address it is about to exempt and waits for a second click. That is deliberately not
11
+ * a dialog: a confirmation you can dismiss without reading is a click with extra steps,
12
+ * whereas a button that changes into the sentence it is about to enact has to be read
13
+ * to be pressed.
14
+ */
15
+ export type AfterAction = () => void;
16
+ export declare function isConfirming(): boolean;
17
+ export declare function actorActions(key: string, after: AfterAction): HTMLElement[];
@@ -0,0 +1,5 @@
1
+ /** Wires the panel's own Close button. Called once, and only when the panel exists. */
2
+ export declare function initActor(): void;
3
+ /** Opens the drill-down above the feed and scrolls it into view. */
4
+ export declare function openActor(key: string): void;
5
+ export declare function drawActor(): void;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The page's two ways of talking to its server.
3
+ *
4
+ * Every write is JSON with a JSON content type, and that is not a style choice: the
5
+ * server refuses a body it did not get as `application/json` precisely because an HTML
6
+ * form cannot send one, which is what stops somebody else's page from posting here
7
+ * with an operator's credentials attached.
8
+ */
9
+ export declare function getJson<T>(path: string): Promise<T>;
10
+ export interface PostResult<T> {
11
+ ok: boolean;
12
+ data: T;
13
+ error?: string;
14
+ }
15
+ export declare function postJson<T>(path: string, body: unknown): Promise<PostResult<T>>;
@@ -0,0 +1,37 @@
1
+ import type { TabName } from "./types.js";
2
+ /**
3
+ * The handful of things every module needs to be able to do to the page as a whole.
4
+ *
5
+ * A mutable record filled in by `index.ts` at start-up, rather than each module
6
+ * importing the module that owns `draw`. The feed opens the actor panel, the actor
7
+ * panel drafts a rule into the policy editor, the policy editor switches to its own
8
+ * tab: written as direct imports that is a cycle, and a cycle in a bundle is a
9
+ * half-initialised module waiting to be discovered at run time. One indirection costs
10
+ * a property lookup and makes the shape obvious.
11
+ */
12
+ export interface App {
13
+ /** Redraws the active view on the next frame. Coalesced; safe to call per event. */
14
+ draw: () => void;
15
+ /** Redraws immediately, for the cases where a click has to be reflected before the next frame. */
16
+ drawNow: () => void;
17
+ showTab: (tab: TabName, options?: {
18
+ focus?: boolean;
19
+ replace?: boolean;
20
+ push?: boolean;
21
+ }) => void;
22
+ /** Puts the feed's filter state into the URL, so a view can be sent to somebody. */
23
+ syncUrl: () => void;
24
+ }
25
+ export declare const app: App;
26
+ /**
27
+ * A transient message in the corner.
28
+ *
29
+ * Lives here rather than in the policy editor because the guard form, the feed's
30
+ * exporter and the stream all raise them now, and three copies of a six-line function
31
+ * is how they end up looking different from each other.
32
+ */
33
+ export declare function toast(kind: "ok" | "bad" | "warn", title: string, detail?: string): void;
34
+ /** Hands the viewer a file. Used by the settings export and the feed export. */
35
+ export declare function download(text: string, filename: string, type: string): void;
36
+ /** Today, as a filename fragment. Every export the page produces is stamped with it. */
37
+ export declare function today(): string;
@@ -0,0 +1,4 @@
1
+ /** A horizontal bar list, sorted by value, capped at what a panel can show without scrolling. */
2
+ export declare function drawBars(target: HTMLElement, rows: Iterable<[string, number]>, emptyText: string): void;
3
+ /** A record of counters as bar rows. */
4
+ export declare function pairs(record: Record<string, number> | undefined): Array<[string, number]>;
@@ -0,0 +1,5 @@
1
+ import type { Boot } from "./types.js";
2
+ export declare const BOOT: Boot;
3
+ /** Every request the page makes is relative to the mount path. */
4
+ export declare const API: string;
5
+ export declare const SECTIONS: Required<import("./types.js").DashboardSections>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Stacked bars, two series, plus a marker row for denials.
3
+ *
4
+ * Two series rather than three because served/mitigated/denied as three fills would
5
+ * need a third hue that survives colour-vision simulation beside these two, and none
6
+ * does; a denial is a status rather than a series, so it gets a status marker and a
7
+ * label instead.
8
+ */
9
+ export declare function drawTraffic(): void;
10
+ /**
11
+ * The score distribution: how close ordinary traffic runs to the line.
12
+ *
13
+ * Two populations, and the panel now says which one it is drawing rather than leaving
14
+ * the reader to assume. **Since start** comes from the same counters as the Prometheus
15
+ * endpoint, so it covers the whole run and agrees with whatever your alerting says.
16
+ * **This window** counts the few hundred requests the page is still holding, which is
17
+ * the right answer to "what is happening right now" and the wrong answer to "where
18
+ * should the threshold be" — and it was the only answer available before, sitting
19
+ * beside panels counting since start under an identical grey subtitle.
20
+ *
21
+ * Proven requests are excluded from both rather than piled into the last bucket: their
22
+ * score is 100 by definition and plays no part in any decision.
23
+ */
24
+ export declare function drawScores(): void;
25
+ export declare function drawLatency(): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Selector escaping, on its own so both halves can have it.
3
+ *
4
+ * `dom.ts` is the browser client's entry into the document and carries module state that
5
+ * has to be initialised in order; the embeddable element imports it lazily for exactly
6
+ * that reason. But the element also assembles selectors from developer-supplied panel ids
7
+ * *synchronously*, before any of that has happened — so the escape lives here, in a leaf
8
+ * with nothing behind it, rather than being duplicated or dragging the client graph in
9
+ * early.
10
+ */
11
+ export declare function cssEscape(value: string): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The page's only way of putting something in the document.
3
+ *
4
+ * `textContent`, always. Every string that reaches this file — a User-Agent, a path, a
5
+ * detector's summary, a rule id somebody typed into the editor — was written by
6
+ * somebody else, and one `innerHTML` anywhere below turns a bot's User-Agent into
7
+ * script running in an operator's browser. A test asserts the served page contains
8
+ * none of the four ways to do that, and `scripts/build-client.mjs` refuses to bundle
9
+ * one.
10
+ */
11
+ import { cssEscape } from "./css.js";
12
+ export declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string | null, value?: string | number | null): HTMLElementTagNameMap[K];
13
+ export declare function svgEl(name: string, attributes?: Record<string, string | number>): SVGElement;
14
+ export declare function svgText(attributes: Record<string, string | number>, text: string | number): SVGElement;
15
+ export declare function clear(node: Node): void;
16
+ /**
17
+ * Ids are base64url today, which needs no escaping — but a selector assembled from a
18
+ * value is only safe until somebody widens the alphabet, and `CSS.escape` costs
19
+ * nothing. It lives in its own module because the embeddable element needs it without
20
+ * loading this one; re-exported here so the rest of the client reads unchanged.
21
+ */
22
+ export { cssEscape };
23
+ /** Points the client at a shadow root. Call it before the rest of the client loads. */
24
+ export declare function setRoot(node: Document | ShadowRoot | HTMLElement, host?: HTMLElement): void;
25
+ /** Updates the element the theme hangs off, without disturbing the rest. */
26
+ export declare function setThemeHost(host: HTMLElement): void;
27
+ export declare function isEmbedded(): boolean;
28
+ /**
29
+ * What global-ish events are listened on.
30
+ *
31
+ * The document when this owns the page. The dashboard's own subtree when it does not, so
32
+ * a digit pressed on the host's page is the host's business and not a tab change here.
33
+ */
34
+ export declare function eventTarget(): EventTarget;
35
+ /** The node every lookup runs against. */
36
+ export declare function rootNode(): Document | ShadowRoot;
37
+ /** The element carrying `data-theme` and the custom properties. */
38
+ export declare function themeElement(): HTMLElement;
39
+ export declare function $(id: string): HTMLElement;
40
+ export declare function byId<T extends HTMLElement>(id: string): T;
41
+ /** A CSS custom property's value, for the charts — which draw in the theme's own colours. */
42
+ export declare function css(name: string): string;
43
+ /**
44
+ * Ties a `<label>` to the control it names.
45
+ *
46
+ * A label sitting next to an input is a label to a reader who can see the layout and
47
+ * nothing at all to a screen reader: it announces "edit, blank" and moves on. Both form
48
+ * builders on this page had that defect, and both were caught by the automated
49
+ * accessibility pass rather than by anybody looking — which is the argument for having
50
+ * one.
51
+ *
52
+ * Only for the controls a label can point at. A group of chips or a segmented control is
53
+ * several buttons, and those carry their own names.
54
+ */
55
+ export declare function label(text: string, control: HTMLElement | HTMLElement[]): HTMLLabelElement;
@@ -0,0 +1,46 @@
1
+ import type { DashboardEntry } from "./types.js";
2
+ /** A rule as the editor holds it: plain JSON, plus the card's open/closed state. */
3
+ export interface EditorRule {
4
+ id: string;
5
+ match: Record<string, unknown>;
6
+ action: string;
7
+ params?: Record<string, unknown>;
8
+ reason?: string;
9
+ /** Editor-only: whether this card is expanded. Stripped before anything is submitted. */
10
+ _open?: boolean;
11
+ }
12
+ export interface Draft {
13
+ rule: EditorRule;
14
+ /** One line saying what the draft matched on, and why that and not something else. */
15
+ because: string;
16
+ }
17
+ /**
18
+ * Turns a request you are looking at into a rule you can preview.
19
+ *
20
+ * This is the half of the loop the dashboard was missing. The row detail could already
21
+ * turn a request into a replay line and a corpus case — the offline loop, where a
22
+ * verdict you disagree with becomes a test. The online loop ran the other way and had
23
+ * no help at all: you saw an actor worth acting on, then went and hand-wrote a rule in
24
+ * a different tab, guessing at which field would catch it.
25
+ *
26
+ * Three decisions in here, and all three are about not being clever on somebody's
27
+ * behalf:
28
+ *
29
+ * **The action is always `tag`.** Never `block`, never `challenge`, whatever the
30
+ * request looks like. A drafted rule is a starting point that has not been reviewed by
31
+ * anyone, and the dashboard picking a terminal action for a request that annoyed you is
32
+ * exactly the reflex this library exists to interrupt. The operator picks the action;
33
+ * the guard still checks it afterwards either way.
34
+ *
35
+ * **It matches on the strongest thing the request actually proves**, in a fixed order:
36
+ * a claimed-and-checked identity, else the detectors whose evidence was proven, else
37
+ * the verdict with a score floor. Each of those is a fact about the client. A path
38
+ * would narrow it further and is deliberately left out — the request happens to have
39
+ * one, which is not the same as the rule being about it.
40
+ *
41
+ * **It goes last.** Appending is the only position that cannot change what any
42
+ * existing rule does, because first match wins. A rule that never fires shows up as
43
+ * "never matched" in the preview, which is a better way to learn it is shadowed than
44
+ * discovering it silently took over from something else.
45
+ */
46
+ export declare function draftRule(entry: DashboardEntry, existingIds?: readonly string[]): Draft;
@@ -0,0 +1,6 @@
1
+ export declare function initFeed(): void;
2
+ /** Applies filter state that arrived in the URL rather than from a click. */
3
+ export declare function reflectFilterButtons(): void;
4
+ export declare function drawFeed(): void;
5
+ /** Drops every cached node. Used when the feed is cleared under the page's feet. */
6
+ export declare function resetFeedCache(): void;
@@ -0,0 +1,19 @@
1
+ /** Number, duration and window formatting. Pure, so the awkward cases can be tested. */
2
+ export declare function n(value: number | undefined): string;
3
+ export declare function pct(part: number, whole: number): string;
4
+ export declare function ms(value: number): string;
5
+ export declare function uptime(milliseconds: number): string;
6
+ export declare function rangeLabel(milliseconds: number): string;
7
+ /** Wall-clock time for a feed row. Local, seconds included, because a feed moves in seconds. */
8
+ export declare function clockTime(at: number): string;
9
+ /**
10
+ * What "this window" actually holds, said out loud.
11
+ *
12
+ * Half the panels on the Statistics screen count the retained ring and half count
13
+ * since the process started, and until now both wore the same quiet grey subtitle.
14
+ * They are different populations — on a busy server the ring can be ninety seconds of
15
+ * a three-week run — so comparing a panel from one against a panel from the other is a
16
+ * mistake the page was inviting. Every "this window" label now says how much window
17
+ * there is.
18
+ */
19
+ export declare function windowLabel(count: number, oldestAt: number | undefined, now: number): string;
@@ -0,0 +1,3 @@
1
+ export declare function drawGuard(): void;
2
+ /** Forgets an in-progress edit, so a reload of the policy document is reflected. */
3
+ export declare function resetGuardDraft(): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import type { DashboardEntry } from "./types.js";
2
+ /**
3
+ * One request has one of four outcomes, and every part of the page agrees on which.
4
+ *
5
+ * Served, mitigated (something happened that a legitimate client can still get past),
6
+ * denied, or pending — assessed, with no decision recorded, which is what `assess()`
7
+ * on its own produces and what a row looks like for the moment between the two events.
8
+ *
9
+ * Pure, and exported on its own, because the table, the left edges, the timeline, the
10
+ * actor mix and the statistics panels all classify the same way. Two of them disagreeing
11
+ * about what a denial is would be a bug nobody could see.
12
+ */
13
+ export type Outcome = "allow" | "mitigate" | "deny" | "pending";
14
+ export declare function outcome(entry: Pick<DashboardEntry, "action">): Outcome;
15
+ /** The same classification for a bare action name, which is what the rule editor has. */
16
+ export declare function actionKind(action: string | undefined): Outcome;
17
+ /** The verdict badge: its class, and the word on it. */
18
+ export declare function verdictBadge(entry: Pick<DashboardEntry, "verdict" | "bypass">): [className: string, label: string];
19
+ /**
20
+ * How many assessments proved a *bot*.
21
+ *
22
+ * Not `metrics.proven`, which is what the tile used to show. That counter is honestly
23
+ * named for what it holds — assessments resting on at least one piece of proven evidence —
24
+ * and evidence has a direction: a client holding an operator or interaction clearance
25
+ * produces *certain human* evidence, so it lands there too. A dashboard watching nothing
26
+ * but cleared humans therefore read "Proven bots: 100% of traffic", and the same requests
27
+ * were counted again under Unremarkable.
28
+ *
29
+ * The two proven-bot verdicts are the exact answer. Only the proven path can reach them —
30
+ * the probabilistic path produces `suspected-bot`, `human` or `unknown` and nothing else —
31
+ * so this is a rename of the truth rather than an approximation of it.
32
+ */
33
+ export declare function provenBots(verdicts: Record<string, number>): number;
@@ -0,0 +1,40 @@
1
+ export declare function drawTiles(force?: boolean): void;
2
+ /**
3
+ * The configuration facts at the end of the tab strip.
4
+ *
5
+ * Label first, value second, every time — "suspect at 60", not "60 suspect at" — and
6
+ * as plain text, which is the one treatment nothing else on the page uses for a
7
+ * control.
8
+ */
9
+ export declare function drawChips(): void;
10
+ /**
11
+ * Every panel that counts the retained ring says how much ring there is.
12
+ *
13
+ * Half the Statistics screen counts the window and half counts since the process
14
+ * started; they were wearing the same grey subtitle, which invited exactly the
15
+ * comparison that does not hold.
16
+ */
17
+ export declare function updateWindowLabels(): void;
18
+ export declare function drawLivePanels(): void;
19
+ export declare function drawStatsPanels(): void;
20
+ /**
21
+ * The audit panel: the window against its baseline.
22
+ *
23
+ * Both, side by side, because that is the only way the audit's output means anything.
24
+ * A bot share of 60% is a number; a bot share of 60% against a baseline of 12% is an
25
+ * incident.
26
+ */
27
+ export declare function drawAudit(): void;
28
+ /** The count on the Policy tab. Cheap, so it runs on every frame; the panel itself does not. */
29
+ export declare function drawNoticeBadge(): void;
30
+ export declare function drawNotices(): void;
31
+ /**
32
+ * What somebody did to this process, and when.
33
+ *
34
+ * The same list the traffic timeline marks, written out — because a marker answers
35
+ * "was there a change here?" and a person auditing wants "what were they, in order".
36
+ * Bounded and in memory, which the panel says: the durable copy is the change events.
37
+ */
38
+ export declare function drawChanges(): void;
39
+ /** Sibling instances, when the operator listed any. */
40
+ export declare function drawPeers(): void;
@@ -0,0 +1,25 @@
1
+ import type { DashboardEntry } from "./types.js";
2
+ /**
3
+ * The rule editor works on a model — an array of plain rule objects — and both views
4
+ * render from it. The GUI mutates the model in place on each keystroke, which is what
5
+ * keeps focus and cursor position while somebody types; only structural changes (add,
6
+ * remove, move, a different action) re-render a card.
7
+ */
8
+ export declare function loadPolicy(): Promise<void>;
9
+ export declare function drawPolicyTab(): void;
10
+ /**
11
+ * Starts a rule from a request the operator was looking at.
12
+ *
13
+ * Appended, expanded, and previewed straight away — the three things somebody would
14
+ * otherwise do by hand between noticing an actor and having a rule they can judge.
15
+ * Nothing is applied: this fills the editor, exactly like an import.
16
+ *
17
+ * It loads the policy document first, and that `await` is load-bearing rather than
18
+ * tidy. Drafting is reachable from the Live tab, which somebody can use for an hour
19
+ * without ever opening the Policy one — and appending to an editor that has not been
20
+ * filled yet leaves it holding the draft *and nothing else*, while marking it dirty so
21
+ * the load that follows will not correct it. Apply that and the running policy becomes
22
+ * one drafted rule.
23
+ */
24
+ export declare function draftIntoEditor(entry: DashboardEntry): Promise<void>;
25
+ export declare function initPolicy(): void;
@@ -0,0 +1,42 @@
1
+ import type { DashboardEntry } from "./types.js";
2
+ /**
3
+ * The feed's search.
4
+ *
5
+ * It used to be one `indexOf` over every field of a request joined into a string,
6
+ * which answers "does this word appear anywhere" and nothing else. That is the wrong
7
+ * question in the two cases somebody actually reaches for the box: an address that
8
+ * also appears inside a User-Agent, and a path that is a prefix of ten others.
9
+ *
10
+ * So terms can name a field, and can be negated:
11
+ *
12
+ * ```text
13
+ * actor:203.0.113.4 -path:/health that address, except its health checks
14
+ * rule:no-scrapers action:tag the rule that fired, and what it settled on
15
+ * score:>70 -certain probabilistic traffic close to the line
16
+ * "GET /api/v2/orders" a phrase, spaces and all
17
+ * ```
18
+ *
19
+ * Every term must match — this is an `AND`, because that is what narrowing means and
20
+ * a filter box that quietly `OR`s is a filter box that lies. Anything that does not
21
+ * parse as a field term is matched against the whole request, so a plain word still
22
+ * behaves exactly as it did.
23
+ *
24
+ * Kept free of the DOM so it can be unit-tested, which is the point: this is the piece
25
+ * of the page most likely to be wrong in a way nobody notices.
26
+ */
27
+ export interface Term {
28
+ /** The field named before the colon, already resolved to a canonical name. */
29
+ field: string | undefined;
30
+ value: string;
31
+ negated: boolean;
32
+ /** For numeric fields: how `value` should be compared. */
33
+ compare?: "<" | ">" | "=" | undefined;
34
+ }
35
+ export declare function parseQuery(input: string): Term[];
36
+ /** Everything about a request, as one lower-case string. What a free term is matched against. */
37
+ export declare function searchableText(entry: DashboardEntry): string;
38
+ /** True when every term is satisfied. An empty query matches everything. */
39
+ export declare function matchesQuery(terms: readonly Term[], entry: DashboardEntry, haystack: string): boolean;
40
+ /** The named filter buttons, which are a second, independent narrowing. */
41
+ export type FilterName = "all" | "proven" | "suspected" | "human" | "guard" | "deny" | "mitigate" | "allow";
42
+ export declare function matchesFilter(filter: FilterName, entry: DashboardEntry): boolean;
@@ -0,0 +1,2 @@
1
+ export declare function loadRanges(): Promise<void>;
2
+ export declare function drawRanges(): void;
@@ -0,0 +1,4 @@
1
+ export declare function loadActors(): Promise<void>;
2
+ /** Polls while the screen is showing, and stops the moment it is not. */
3
+ export declare function trackActors(): void;
4
+ export declare function drawActors(): void;
@@ -0,0 +1,14 @@
1
+ import type { DashboardEntry } from "./types.js";
2
+ /**
3
+ * A request as a line `bothandlerjs replay` can read.
4
+ *
5
+ * The point of the button this backs: a verdict you disagree with on screen becomes a
6
+ * fixture you can re-run offline, and then a corpus case that stops it coming back.
7
+ * Values the dashboard redacted stay redacted — the shape is what replays, and a
8
+ * session cookie is not part of the shape.
9
+ */
10
+ export declare function replayLine(entry: DashboardEntry): string;
11
+ /** Every shown request as replay JSONL, oldest first — the whole filtered window at once. */
12
+ export declare function replayFile(entries: readonly DashboardEntry[]): string;
13
+ /** The same request as a traffic-corpus case, ready to paste into a fixture file. */
14
+ export declare function corpusCase(entry: DashboardEntry): string;
@@ -0,0 +1,10 @@
1
+ import type { Preview } from "./types.js";
2
+ /** The result box under the editor. One box, whichever of the two forms wrote to it. */
3
+ export declare function showResult(kind: "ok" | "bad" | "warn", build: (box: HTMLElement) => void): void;
4
+ /**
5
+ * What a candidate would have done to the traffic still in the window.
6
+ *
7
+ * `newDenials` is called out on its own because it is the one direction of change that
8
+ * costs somebody their access, and it is easy to miss in a table of counts.
9
+ */
10
+ export declare function renderPreview(preview: Preview, notes?: readonly string[]): void;
@@ -0,0 +1,68 @@
1
+ import type { EditorRule } from "./draft.js";
2
+ import type { FilterName, Term } from "./query.js";
3
+ import type { ActorRow, DashboardEntry, Policy, Row, Snapshot, TabName } from "./types.js";
4
+ /** Rows built into the table at once. Beyond this a feed is scrolled past, not read. */
5
+ export declare const FEED_LIMIT = 300;
6
+ export interface State {
7
+ rows: Row[];
8
+ byId: Map<string, Row>;
9
+ snapshot: Snapshot | undefined;
10
+ policy: Policy | undefined;
11
+ /** The Actors screen's list, fetched rather than streamed. See `registry.ts`. */
12
+ actors: ActorRow[];
13
+ actorsTracked: number;
14
+ paused: boolean;
15
+ filter: FilterName;
16
+ search: string;
17
+ terms: Term[];
18
+ tab: TabName;
19
+ open: Set<string>;
20
+ actor: string | undefined;
21
+ rangeMs: number;
22
+ /** Which population the score distribution is drawn from. See `charts.drawScores`. */
23
+ scoreScope: "run" | "window";
24
+ editorRules: EditorRule[];
25
+ editorDirty: boolean;
26
+ editorMode: "gui" | "json";
27
+ guardDirty: boolean;
28
+ /** Rows that arrived while paused, so the button can say what resuming will show. */
29
+ bufferedWhilePaused: number;
30
+ /** Frames the server dropped because *this* connection was too slow to take them. */
31
+ laggedDrops: number;
32
+ }
33
+ export declare const state: State;
34
+ /**
35
+ * Files one entry into the ring.
36
+ *
37
+ * The same request arrives twice — once when it is assessed and again when the
38
+ * decision lands, and again after a reconnect that replays what was missed — so this
39
+ * is an upsert keyed by request id, and `rev` is what tells the renderer that a row on
40
+ * screen is now stale. Rebuilding a row nothing changed about is what used to wipe a
41
+ * text selection every time a request came in.
42
+ */
43
+ export declare function ingest(entry: DashboardEntry): void;
44
+ export declare function clearFeed(): void;
45
+ export declare function setSearch(value: string): void;
46
+ export declare function matches(row: Row): boolean;
47
+ /** The rows the feed would draw, newest first. Also what "export what I am looking at" means. */
48
+ export declare function visibleRows(limit?: number): Row[];
49
+ export declare function matchingCount(): number;
50
+ export declare function oldestAt(): number | undefined;
51
+ export interface Aggregates {
52
+ detectors: Map<string, number>;
53
+ actors: Map<string, number>;
54
+ identities: Map<string, number>;
55
+ paths: Map<string, number>;
56
+ deniedPaths: Map<string, number>;
57
+ guardStops: Map<string, number>;
58
+ ruleHits: Map<string, number>;
59
+ bypassed: Map<string, number>;
60
+ }
61
+ /**
62
+ * Everything the window-scoped panels count, in one pass over the ring.
63
+ *
64
+ * One pass rather than eight, and — more to the point — it is now called only by the
65
+ * screen that draws them. It used to run on every frame of the live feed to fill in
66
+ * panels on a tab nobody was looking at.
67
+ */
68
+ export declare function aggregate(rows: readonly Row[]): Aggregates;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Closes the stream, keeping everything the page has drawn.
3
+ *
4
+ * Reconnecting resumes from where this left off: the server is told the last id seen and
5
+ * sends only what was missed, so suspending across a route change costs a reconnect rather
6
+ * than a reload.
7
+ */
8
+ export declare function suspendStream(): void;
9
+ export declare function connectStream(): void;
10
+ /**
11
+ * A first fetch, so the page has counters before the stream opens — and so a browser
12
+ * with `EventSource` blocked still shows something useful.
13
+ */
14
+ export declare function loadInitialSnapshot(): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare function initTester(): void;
@@ -0,0 +1,69 @@
1
+ import type { DashboardEntry, DashboardSections, DashboardSnapshot, PolicyDocument, PolicyPreview } from "../types.js";
2
+ export type { DashboardChange, DashboardEntry, DashboardEvidence, DashboardNotice, DashboardSections, DashboardSnapshot, PolicyDocument, PolicyPreview } from "../types.js";
3
+ /** What the server stamps into the page for the client to read at start-up. */
4
+ export interface Boot {
5
+ /** The mount prefix, or `""` at the root. Every request the page makes is relative to it. */
6
+ base: string;
7
+ title: string;
8
+ allowReset: boolean;
9
+ allowEdit: boolean;
10
+ allowGuardEdit: boolean;
11
+ allowActing: boolean;
12
+ peers: ReadonlyArray<{
13
+ label: string;
14
+ href: string;
15
+ }>;
16
+ sections: Required<DashboardSections>;
17
+ links: ReadonlyArray<{
18
+ label: string;
19
+ href: string;
20
+ }>;
21
+ }
22
+ /**
23
+ * One feed entry, plus what the page knows about it that the server does not.
24
+ *
25
+ * A wrapper rather than extra fields on the entry, for two reasons. The entry is a
26
+ * copy of what the server sent and anything added to it would end up in a replay line
27
+ * or a corpus case; and `rev` has to survive being compared against a rendered row,
28
+ * which is a question about this page rather than about the request.
29
+ */
30
+ export interface Row {
31
+ entry: DashboardEntry;
32
+ /**
33
+ * Bumped whenever the entry changes, which is how the feed knows a rendered row is
34
+ * stale. A request is written twice — once when it is assessed, again when the
35
+ * decision lands — and only the second write should cost a rebuild.
36
+ */
37
+ rev: number;
38
+ /** The searchable text, built on demand and thrown away when the entry changes. */
39
+ text?: string | undefined;
40
+ }
41
+ /** The views, in the order the tab strip and the digit shortcuts use. */
42
+ export type TabName = "live" | "actors" | "stats" | "policy";
43
+ export interface Preview extends PolicyPreview {
44
+ }
45
+ export interface Policy extends PolicyDocument {
46
+ }
47
+ export interface Snapshot extends DashboardSnapshot {
48
+ }
49
+ /**
50
+ * One row of the Actors screen.
51
+ *
52
+ * Here rather than in `registry.ts`, which renders it: this is plain data, and the store
53
+ * holds a list of it. A pure module importing a type from a module that speaks DOM pulls
54
+ * that module into every program the pure one appears in — which is how a Node-side test
55
+ * of the store came to fail on `HTMLInputElement`.
56
+ */
57
+ export interface ActorRow {
58
+ key: string;
59
+ requests: number;
60
+ recentRate: number;
61
+ distinctPaths: number;
62
+ distinctUserAgents: number;
63
+ cadenceCv: number | undefined;
64
+ priorConfirmations: number;
65
+ unsolvedChallenges: number;
66
+ cleared: boolean;
67
+ firstSeen: number;
68
+ lastSeen: number;
69
+ }